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

This commit is contained in:
2026-06-19 11:13:37 +02:00
parent 89d8ad114b
commit aad528d15b
18 changed files with 2991 additions and 2845 deletions
+26 -940
View File
@@ -4,6 +4,18 @@ require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php'; require_once __DIR__ . '/includes/i18n.php';
require_login('/login.php'); require_login('/login.php');
// ============================================================================
// DIAGNOSTIC ONBOARDING (Vérifie si le budget est prêt à être utilisé)
// ============================================================================
$currentYear = date('Y');
$hasCategories = ((int)$pdo->query("SELECT COUNT(*) FROM pf_budget_categories")->fetchColumn() > 0);
$hasSalaries = ((int)$pdo->query("SELECT COUNT(*) FROM pf_salary_config WHERE year = $currentYear")->fetchColumn() > 0);
$hasAccounts = ((int)$pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn() > 0);
$hasBudgetItems = ((int)$pdo->query("SELECT COUNT(*) FROM pf_budget_items")->fetchColumn() > 0);
// Le minimum vital pour que ça tourne
$isBudgetSetupOk = ($hasCategories && $hasSalaries && $hasAccounts);
// Gestion de l'onglet actif (par défaut 'suivi') // Gestion de l'onglet actif (par défaut 'suivi')
$tab = $_GET['tab'] ?? 'suivi'; $tab = $_GET['tab'] ?? 'suivi';
@@ -15,11 +27,15 @@ 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 budget-hero">
<div style="display: flex; justify-content: center; align-items: center; gap: 15px; margin-bottom: 20px;"> <div class="budget-title-group">
<h1 style="margin: 0;"><?= tr('budget_main_header') ?></h1> <h1><?= tr('budget_main_header') ?></h1>
<button class="btn-settings-gear" onclick="openBudgetSettings()" title="<?= tr('settings') ?>"> <!-- Bouton avec séparation claire entre l'engrenage animable et l'alerte fixe -->
⚙️ <button class="btn-header-settings" onclick="openBudgetSettings()" title="<?= tr('settings') ?>">
<span class="gear-icon-anim">⚙️</span>
<?php if (!$isBudgetSetupOk || !$hasBudgetItems): ?>
<span class="alert-dot">!</span>
<?php endif; ?>
</button> </button>
</div> </div>
@@ -48,7 +64,6 @@ require __DIR__ . '/header.php';
<section class="pf-section"> <section class="pf-section">
<?php <?php
// ROUTAGE DYNAMIQUE
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev']; $allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev'];
if (in_array($tab, $allowedTabs)) { if (in_array($tab, $allowedTabs)) {
@@ -57,7 +72,7 @@ require __DIR__ . '/header.php';
if (file_exists($viewPath)) { if (file_exists($viewPath)) {
include $viewPath; include $viewPath;
} else { } else {
echo "<div style='text-align:center; padding:50px; color:#ef4444;'>"; echo "<div class='budget-error-msg'>";
echo "<h3>" . tr('budget_err_file_not_found') . "</h3>"; echo "<h3>" . tr('budget_err_file_not_found') . "</h3>";
echo "<p>" . sprintf(tr('budget_err_file_detail'), htmlspecialchars($tab)) . "</p>"; echo "<p>" . sprintf(tr('budget_err_file_detail'), htmlspecialchars($tab)) . "</p>";
echo "</div>"; echo "</div>";
@@ -68,937 +83,8 @@ 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>
<button class="bs-tab-btn" onclick="switchBsTab('csv', this)"><?= tr('bs_tab_csv') ?></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> <?php
require __DIR__ . '/modules/budget/views/settings_modal.php';
<hr style="margin: 1.5rem 0; border:0; border-bottom:1px solid var(--border-light);"> require __DIR__ . '/footer.php';
?>
<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 id="pane-csv" class="bs-pane">
<h4 class="bs-section-title"><?= tr('bs_csv_title') ?></h4>
<p class="pf-muted-tiny" style="margin-top:0; margin-bottom:15px;"><?= tr('bs_csv_desc') ?></p>
<div id="csv-drop-zone" style="border: 2px dashed var(--border-light, #ccc); border-radius: var(--radius); padding: 30px; text-align: center; cursor: pointer; margin-bottom: 25px; transition: all 0.2s;">
<div style="font-size: 2rem; margin-bottom: 10px;">📥</div>
<h5 style="margin: 0 0 5px 0;"><?= tr('bs_csv_drop_title') ?></h5>
<p class="pf-muted-tiny" style="margin: 0;"><?= tr('bs_csv_drop_desc') ?></p>
<input type="file" id="csv_file_input" accept=".csv" style="display: none;">
</div>
<div id="csv-preview-container" style="display:none; background: var(--bg-soft, #f8fafc); padding: 15px; border-radius: var(--radius); border: 1px dashed var(--primary); margin-bottom: 25px;">
</div>
<h5 style="margin: 0 0 15px 0; border-bottom: 1px solid var(--border-light); padding-bottom: 5px;">
<?= tr('bs_csv_settings_title') ?>
</h5>
<form id="form-csv-mapping" onsubmit="saveCsvMapping(event)">
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
<div>
<label class="pf-label"><?= tr('bs_csv_delimiter') ?></label>
<select name="csv_delimiter" id="csv_delimiter" class="pf-input">
<option value=";"><?= tr('bs_csv_delim_semi') ?></option>
<option value=","><?= tr('bs_csv_delim_comma') ?></option>
<option value="\t"><?= tr('bs_csv_delim_tab') ?></option>
</select>
</div>
<div>
<label class="pf-label"><?= tr('bs_csv_date_format') ?></label>
<select name="csv_date_format" id="csv_date_format" class="pf-input">
<option value="d/m/Y">JJ/MM/AAAA</option>
<option value="Y-m-d">AAAA-MM-JJ</option>
</select>
</div>
</div>
<h5 style="margin: 15px 0 10px; font-size: 0.95rem;">
<?= tr('bs_csv_col_index') ?>
<small style="color:var(--text-muted); font-weight:normal;"><?= tr('bs_csv_col_index_help') ?></small>
</h5>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
<div>
<label class="pf-label"><?= tr('bs_csv_col_date') ?></label>
<input type="number" min="0" name="csv_col_date" id="csv_col_date" class="pf-input" value="0" required>
</div>
<div>
<label class="pf-label"><?= tr('bs_csv_col_label') ?></label>
<input type="number" min="0" name="csv_col_label" id="csv_col_label" class="pf-input" value="1" required>
</div>
</div>
<div style="background: var(--bg-panel); padding: 10px; border-radius: var(--radius); border: 1px solid var(--border-light); margin-bottom: 15px;">
<label class="pf-label"><?= tr('bs_csv_amount_mgmt') ?></label>
<select name="csv_amount_type" id="csv_amount_type" class="pf-input" style="margin-bottom: 10px;" onchange="toggleCsvAmountCols(this.value)">
<option value="single"><?= tr('bs_csv_amount_single') ?></option>
<option value="split"><?= tr('bs_csv_amount_split') ?></option>
</select>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<div>
<label class="pf-label" id="lbl_col_debit"><?= tr('bs_csv_col_amount') ?></label>
<input type="number" min="0" name="csv_col_debit" id="csv_col_debit" class="pf-input" value="8" required>
</div>
<div id="wrapper_col_credit" style="display:none;">
<label class="pf-label"><?= tr('bs_csv_col_credit') ?></label>
<input type="number" min="0" name="csv_col_credit" id="csv_col_credit" class="pf-input" value="9">
</div>
</div>
</div>
<div style="margin-bottom: 20px;">
<label class="pf-label"><?= tr('bs_csv_col_ref') ?></label>
<input type="number" min="0" name="csv_col_ref" id="csv_col_ref" class="pf-input" value="3">
</div>
<div style="text-align: right;">
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save_format') ?></button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
// --- AUTO-SCROLL DES ONGLETS MOBILES ---
document.addEventListener('DOMContentLoaded', () => {
const tabsContainer = document.querySelector('.budget-tabs-container');
const activeTab = document.querySelector('.tab-item.active');
if (tabsContainer && activeTab) {
if (tabsContainer.scrollWidth > tabsContainer.clientWidth) {
const scrollPos = activeTab.offsetLeft - (tabsContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2);
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);
}
}
// --- GESTION DU MAPPING CSV ---
window.I18N = {
...(window.I18N || {}),
'bs_csv_col_amount': <?= json_encode(tr('bs_csv_col_amount')) ?>,
'bs_csv_col_debit': <?= json_encode(tr('bs_csv_col_debit')) ?>,
'bs_csv_saved': <?= json_encode(tr('bs_csv_saved')) ?>,
'error_occured': <?= json_encode(tr('error_occured')) ?>
};
function toggleCsvAmountCols(type) {
if (type === 'split') {
document.getElementById('wrapper_col_credit').style.display = 'block';
document.getElementById('lbl_col_debit').innerText = window.I18N['bs_csv_col_debit'];
} else {
document.getElementById('wrapper_col_credit').style.display = 'none';
document.getElementById('lbl_col_debit').innerText = window.I18N['bs_csv_col_amount'];
}
}
function renderCsvMapping(mapping) {
if (!mapping) return;
document.getElementById('csv_delimiter').value = mapping.delimiter || ';';
document.getElementById('csv_date_format').value = mapping.date_format || 'd/m/Y';
document.getElementById('csv_col_date').value = mapping.col_date ?? 0;
document.getElementById('csv_col_label').value = mapping.col_label ?? 1;
document.getElementById('csv_col_ref').value = mapping.col_ref ?? 3;
const amountType = mapping.amount_type || 'split';
document.getElementById('csv_amount_type').value = amountType;
document.getElementById('csv_col_debit').value = mapping.col_debit ?? 8;
document.getElementById('csv_col_credit').value = mapping.col_credit ?? 9;
toggleCsvAmountCols(amountType);
}
async function saveCsvMapping(e) {
e.preventDefault();
const form = e.target;
const btn = form.querySelector('button[type="submit"]');
const oldText = btn.innerText;
btn.innerText = '⏳...';
btn.disabled = true;
try {
const formData = new FormData(form);
formData.append('action', 'save_csv_mapping');
const response = await pachaFetch('/modules/budget/includes/api/settings.php', {
method: 'POST',
body: formData
});
if (!response.success) throw new Error(response.error || window.I18N['error_occured']);
if (typeof showToast === 'function') showToast(window.I18N['bs_csv_saved']);
else alert(window.I18N['bs_csv_saved']);
} catch (err) {
alert(window.I18N['error_occured'] + " : " + err.message);
} finally {
btn.innerText = oldText;
btn.disabled = false;
}
}
// --- GESTION DE LA DROP ZONE CSV ET FILEREADER ---
// Ajout des nouvelles clés I18N magiques
window.I18N = {
...(window.I18N || {}),
'bs_csv_err_type': <?= json_encode(tr('bs_csv_err_type')) ?>,
'bs_csv_file_ok': <?= json_encode(tr('bs_csv_file_ok')) ?>,
'bs_csv_preview_title': <?= json_encode(tr('bs_csv_preview_title')) ?>,
'bs_csv_empty_col': <?= json_encode(tr('bs_csv_empty_col')) ?>,
'bs_csv_no_data': <?= json_encode(tr('bs_csv_no_data')) ?>
};
const dropZone = document.getElementById('csv-drop-zone');
const fileInput = document.getElementById('csv_file_input');
if (dropZone && fileInput) {
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.backgroundColor = 'rgba(0, 123, 255, 0.05)';
dropZone.style.borderColor = 'var(--primary-color, #007bff)';
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.style.backgroundColor = '';
dropZone.style.borderColor = 'var(--border-light, #ccc)';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.backgroundColor = '';
dropZone.style.borderColor = 'var(--border-light, #ccc)';
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
handleCsvUpload(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', function() {
if (this.files.length) {
handleCsvUpload(this.files[0]);
}
});
}
function handleCsvUpload(file) {
if (!file.name.endsWith('.csv')) {
alert(window.I18N['bs_csv_err_type']);
return;
}
if (typeof showToast === 'function') {
showToast(window.I18N['bs_csv_file_ok'] + ' : ' + file.name);
}
const reader = new FileReader();
reader.onload = function(e) {
const text = e.target.result;
const delimiter = document.getElementById('csv_delimiter').value || ';';
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
if (lines.length > 0) {
// Extraction des en-têtes (Ligne 0)
const headers = lines[0].split(delimiter).map(h => h.replace(/^"|"$/g, '').trim());
// Extraction de max 2 lignes de données (Ligne 1 et 2)
const sampleRows = [];
for (let i = 1; i < Math.min(lines.length, 3); i++) {
const rowData = lines[i].split(delimiter).map(d => d.replace(/^"|"$/g, '').trim());
sampleRows.push(rowData);
}
renderCsvTablePreview(headers, sampleRows);
}
};
reader.readAsText(file, 'ISO-8859-1');
}
function renderCsvTablePreview(headers, sampleRows) {
const container = document.getElementById('csv-preview-container');
let html = `<h5 style="margin: 0 0 10px 0; color: var(--primary); font-size: 0.95rem;">👁️ ${window.I18N['bs_csv_preview_title']}</h5>`;
// Conteneur avec scroll horizontal si le tableau est trop large
html += `<div style="overflow-x: auto; max-width: 100%; border: 1px solid var(--border-light); border-radius: 8px; background: white;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.85rem; text-align: left; white-space: nowrap;">
<thead style="background: var(--bg-page, #f1f5f9); border-bottom: 2px solid var(--border-light);">
<tr>`;
// Rendu des en-têtes de colonnes (avec le numéro)
headers.forEach((header, index) => {
const cleanHeader = header || `(${window.I18N['bs_csv_empty_col']})`;
html += `<th style="padding: 10px 15px; font-weight: 600; color: var(--text-main); border-right: 1px solid var(--border-light);">
<div style="font-size: 0.75rem; color: var(--primary); margin-bottom: 2px;">N° ${index}</div>
<div>${cleanHeader}</div>
</th>`;
});
html += ` </tr>
</thead>
<tbody>`;
// Rendu des données
if (sampleRows.length === 0) {
html += `<tr><td colspan="${headers.length}" style="padding: 15px; text-align: center; color: var(--text-muted); font-style: italic;">${window.I18N['bs_csv_no_data']}</td></tr>`;
} else {
sampleRows.forEach(row => {
html += `<tr style="border-bottom: 1px solid #e2e8f0;">`;
// On boucle sur la taille des headers pour s'assurer que l'affichage ne casse pas si la ligne de données est incomplète
for (let i = 0; i < headers.length; i++) {
const cellData = row[i] !== undefined ? row[i] : '';
// Raccourcir la donnée si elle est vraiment très longue (ex: libellé de 100 caractères)
const displayData = cellData.length > 40 ? cellData.substring(0, 40) + '...' : cellData;
html += `<td style="padding: 10px 15px; color: var(--text-muted); border-right: 1px dashed #e2e8f0;">${displayData}</td>`;
}
html += `</tr>`;
});
}
html += `</tbody></table></div>`;
container.innerHTML = html;
container.style.display = 'block';
}
</script>
<?php require __DIR__ . '/footer.php'; ?>
+125 -66
View File
@@ -7,6 +7,8 @@ CREATE TABLE IF NOT EXISTS pf_foyer_settings (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
currency VARCHAR(10) NOT NULL DEFAULT '', currency VARCHAR(10) NOT NULL DEFAULT '',
zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C', zone_scolaire VARCHAR(5) NOT NULL DEFAULT 'C',
csv_mapping TEXT DEFAULT NULL,
cares_modes TEXT DEFAULT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -44,12 +46,14 @@ CREATE TABLE IF NOT EXISTS pf_users (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Personnes ──────────────────────────────────────────────────────────────── -- ─── Personnes ────────────────────────────────────────────────────────────────
CREATE TABLE `pf_people` ( CREATE TABLE IF NOT EXISTS `pf_people` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL, `name` varchar(100) NOT NULL,
`user_id` int(11) DEFAULT NULL, `user_id` int(11) DEFAULT NULL,
`role` varchar(50) DEFAULT NULL, `role` varchar(50) DEFAULT NULL,
`care_modes` varchar(255) DEFAULT NULL,
`color` varchar(7) DEFAULT '#0891b2', `color` varchar(7) DEFAULT '#0891b2',
`is_active` TINYINT(1) DEFAULT 1,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -88,10 +92,29 @@ CREATE TABLE IF NOT EXISTS pf_leave_snapshots (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_person_leave_meta ( CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
id INT AUTO_INCREMENT PRIMARY KEY, id INT(11) AUTO_INCREMENT PRIMARY KEY,
person_id INT NOT NULL UNIQUE, person_id INT(11) NOT NULL,
anniversary_date DATE leave_type VARCHAR(50) COLLATE utf8mb4_unicode_ci NOT NULL,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; method VARCHAR(20) COLLATE utf8mb4_unicode_ci DEFAULT 'FIXED',
allowance DECIMAL(5,2) DEFAULT 0.00,
anniversary_date DATE DEFAULT NULL,
INDEX (person_id),
INDEX (leave_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ─── Types de Congés (Référentiel) ─────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_leave_types (
id INT(11) AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) COLLATE utf8mb4_general_ci NOT NULL,
label VARCHAR(100) COLLATE utf8mb4_general_ci NOT NULL,
default_allowance DECIMAL(5,2) DEFAULT 0.00,
reset_month INT(11) DEFAULT 1,
allow_carry_over TINYINT(1) DEFAULT 0,
UNIQUE KEY code (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT IGNORE INTO pf_leave_types (code, label, default_allowance, reset_month, allow_carry_over) VALUES
('CP', 'Congés Payés', 25.00, 6, 0);
CREATE TABLE IF NOT EXISTS pf_calendar_weeks ( CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@@ -108,87 +131,101 @@ CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ─── Budget ─────────────────────────────────────────────────────────────────── -- ─── Budget ───────────────────────────────────────────────────────────────────
-- NOUVEAU : Table des comptes bancaires
CREATE TABLE IF NOT EXISTS pf_bank_accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
account_type ENUM('checking', 'savings') DEFAULT 'checking',
owner_person_id INT DEFAULT NULL,
is_default TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (owner_person_id) REFERENCES pf_people(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_budget_items ( CREATE TABLE IF NOT EXISTS pf_budget_items (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
amount DECIMAL(10,2) DEFAULT 0, amount DECIMAL(10,2) DEFAULT 0,
category VARCHAR(100), category VARCHAR(100),
type VARCHAR(50), type VARCHAR(50),
payment_day INT DEFAULT NULL, payment_day INT DEFAULT NULL,
is_estimate TINYINT(1) DEFAULT 0, is_estimate TINYINT(1) DEFAULT 0,
reg_month VARCHAR(7) DEFAULT NULL, reg_month VARCHAR(7) DEFAULT NULL,
mapping_keywords TEXT DEFAULT NULL, mapping_keywords TEXT DEFAULT NULL,
holiday_id INT DEFAULT NULL, holiday_id INT DEFAULT NULL,
is_checked TINYINT(1) DEFAULT 0, is_checked TINYINT(1) DEFAULT 0,
sort_order INT DEFAULT 0 sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_expenses ( CREATE TABLE IF NOT EXISTS pf_expenses (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
date_exp DATE NOT NULL, date_exp DATE NOT NULL,
gestion_month VARCHAR(10) NOT NULL, gestion_month VARCHAR(10) NOT NULL,
category VARCHAR(100), category VARCHAR(100),
label VARCHAR(255), label VARCHAR(255),
amount DECIMAL(10,2) DEFAULT 0, amount DECIMAL(10,2) DEFAULT 0,
import_ref VARCHAR(255) DEFAULT NULL, import_ref VARCHAR(255) DEFAULT NULL,
budget_item_id INT DEFAULT NULL, budget_item_id INT DEFAULT NULL,
holiday_id INT DEFAULT NULL holiday_id INT DEFAULT NULL,
salary_id INT DEFAULT NULL -- 🔥 AJOUT : Lien vers le salaire configuré
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_categories ( CREATE TABLE IF NOT EXISTS pf_alloc_categories (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
target DECIMAL(10,2) DEFAULT 0.00, target DECIMAL(10,2) DEFAULT 0.00,
transfer_dest VARCHAR(50) DEFAULT NULL, transfer_dest VARCHAR(50) DEFAULT NULL,
holiday_id INT DEFAULT NULL, holiday_id INT DEFAULT NULL,
sort_order INT DEFAULT 0 sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_alloc_values ( CREATE TABLE IF NOT EXISTS pf_alloc_values (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
month_date VARCHAR(10) NOT NULL, month_date VARCHAR(10) NOT NULL,
cat_id INT NOT NULL, cat_id INT NOT NULL,
person_id INT NOT NULL, person_id INT NOT NULL,
amount DECIMAL(10,2) DEFAULT 0.00, amount DECIMAL(10,2) DEFAULT 0.00,
UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id), UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_savings ( CREATE TABLE IF NOT EXISTS pf_savings (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
owner VARCHAR(50) NOT NULL, owner VARCHAR(50) NOT NULL,
month_date VARCHAR(10) NOT NULL, month_date VARCHAR(10) NOT NULL,
category VARCHAR(100) NOT NULL, category VARCHAR(100) NOT NULL,
amount DECIMAL(10,2) DEFAULT 0, amount DECIMAL(10,2) DEFAULT 0,
holiday_id INT DEFAULT NULL holiday_id INT DEFAULT NULL,
UNIQUE KEY uq_savings (owner, month_date, category)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_bank_snapshots ( CREATE TABLE IF NOT EXISTS pf_bank_snapshots (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
snapshot_date DATE NOT NULL, snapshot_date DATE NOT NULL,
amount DECIMAL(12,2) DEFAULT 0 amount DECIMAL(12,2) DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_salary_config ( CREATE TABLE IF NOT EXISTS pf_salary_config (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
year INT NOT NULL, year INT NOT NULL,
person VARCHAR(50) NOT NULL, person VARCHAR(50) NOT NULL,
salary DECIMAL(10,2) DEFAULT 0, salary DECIMAL(10,2) DEFAULT 0,
mensualite DECIMAL(10,2) DEFAULT 0, mensualite DECIMAL(10,2) DEFAULT 0,
frais_func DECIMAL(10,2) DEFAULT 0, frais_func DECIMAL(10,2) DEFAULT 0,
eco_perso DECIMAL(10,2) DEFAULT 0, eco_perso DECIMAL(10,2) DEFAULT 0,
eco_family DECIMAL(10,2) DEFAULT 0, eco_family DECIMAL(10,2) DEFAULT 0,
UNIQUE KEY uq_salary (year, person) UNIQUE KEY uq_salary (year, person)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS pf_import_rules ( CREATE TABLE IF NOT EXISTS pf_import_rules (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
keyword VARCHAR(255) NOT NULL UNIQUE, keyword VARCHAR(255) NOT NULL UNIQUE,
category VARCHAR(100) NOT NULL, category VARCHAR(100) NOT NULL,
budget_item_id INT(11) NULL DEFAULT NULL budget_item_id INT(11) NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `pf_advances` ( CREATE TABLE IF NOT EXISTS `pf_advances` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`advance_date` date NOT NULL, `advance_date` date NOT NULL,
@@ -214,11 +251,7 @@ CREATE TABLE IF NOT EXISTS pf_budget_categories (
-- Insertion des catégories de base pour toute nouvelle famille -- Insertion des catégories de base pour toute nouvelle famille
INSERT IGNORE INTO pf_budget_categories (code, label, type, color, icon) VALUES INSERT IGNORE INTO pf_budget_categories (code, label, type, color, icon) VALUES
('INCOME', 'Revenus', 'Income', '#10b981', '💵'), ('INCOME', 'Revenus', 'Income', '#10b981', '💵'),
('FMCG', 'Alimentation & Courses', 'Expense', '#3b82f6', '🛒'), ('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢');
('FUEL', 'Carburant', 'Expense', '#f59e0b', ''),
('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢'),
('SCHOOL', 'École & Garde', 'Expense', '#a855f7', '🎒'),
('SAVINGS', 'Épargne', 'Expense', '#8b5cf6', '🐷');
-- ─── Notes / Memo ───────────────────────────────────────────────────────────── -- ─── Notes / Memo ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_notes ( CREATE TABLE IF NOT EXISTS pf_notes (
@@ -308,16 +341,42 @@ CREATE TABLE IF NOT EXISTS pf_geocode_cache (
-- ─── Cadeaux ────────────────────────────────────────────────────────────────── -- ─── Cadeaux ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_gifts ( CREATE TABLE IF NOT EXISTS pf_gifts (
id INT AUTO_INCREMENT PRIMARY KEY, id INT(11) AUTO_INCREMENT PRIMARY KEY,
year INT NOT NULL, year INT(11) NOT NULL,
adult_name VARCHAR(100) NOT NULL, adult_name VARCHAR(100) NOT NULL,
payer_name VARCHAR(100), payer_name VARCHAR(100) DEFAULT NULL,
child_name VARCHAR(100) NOT NULL, child_name VARCHAR(100) NOT NULL,
occasion VARCHAR(50) NOT NULL, occasion VARCHAR(50) NOT NULL,
gift_description TEXT NOT NULL, gift_description TEXT NOT NULL,
product_link VARCHAR(500) DEFAULT NULL, product_link VARCHAR(500) DEFAULT NULL,
amount DECIMAL(8,2) DEFAULT 0 amount DECIMAL(8,2) DEFAULT 0.00
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS pf_gift_occasions (
id INT(11) AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
icon VARCHAR(20) DEFAULT '🎁',
month_date VARCHAR(5) DEFAULT NULL,
is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT IGNORE INTO pf_gift_occasions (code, name, icon, month_date, is_active, sort_order) VALUES
('XMAS', 'Noël', '🎄', '12-25', 1, 1),
('BIRTHDAY', 'Anniversaire', '🎂', NULL, 1, 2),
('OTHER', 'Autre', '🎁', NULL, 1, 99);
-- Table des règles (Qui offre à Qui)
CREATE TABLE IF NOT EXISTS pf_gift_rules (
id INT(11) AUTO_INCREMENT PRIMARY KEY,
adult_person_id INT(11) NOT NULL,
child_person_id INT(11) NOT NULL,
occasion_id INT(11) NOT NULL,
INDEX (adult_person_id),
INDEX (child_person_id),
INDEX (occasion_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ─── Garage Manager ─────────────────────────────────────────────────────────── -- ─── Garage Manager ───────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS pf_vehicles ( CREATE TABLE IF NOT EXISTS pf_vehicles (
+146 -41
View File
@@ -29,7 +29,7 @@ foreach ($familyPeople as $p) {
} }
// 2. Modes de garde (globaux du foyer, pour synchroniser avec le JS) // 2. Modes de garde (globaux du foyer, pour synchroniser avec le JS)
$stmtFoyer = $pdo->query("SELECT care_modes FROM pf_foyer_settings LIMIT 1"); $stmtFoyer = $pdo->query("SELECT care_modes, zone_scolaire FROM pf_foyer_settings LIMIT 1");
$foyerData = $stmtFoyer->fetch(PDO::FETCH_ASSOC); $foyerData = $stmtFoyer->fetch(PDO::FETCH_ASSOC);
$activeCareModes = json_decode($foyerData['care_modes'] ?? '[]', true); $activeCareModes = json_decode($foyerData['care_modes'] ?? '[]', true);
if (!is_array($activeCareModes)) $activeCareModes = []; if (!is_array($activeCareModes)) $activeCareModes = [];
@@ -45,6 +45,38 @@ foreach ($dbLeaves as $l) {
} }
$allLeaveTypes = array_keys($allLeaveTypes); $allLeaveTypes = array_keys($allLeaveTypes);
// ==========================================
// VÉRIFICATIONS D'INTÉGRITÉ (ALERTES)
// ==========================================
$calendarAlerts = [];
// 1. Aucun membre adulte/parent configuré
if (empty($parents)) {
$calendarAlerts[] = "<strong>Aucun adulte configuré.</strong> Rendez-vous dans les Paramètres Généraux de HouseHub pour ajouter les membres du foyer.";
}
// 2. Enfants présents, mais aucun mode de garde global défini
if (!empty($kids) && empty($activeCareModes)) {
$calendarAlerts[] = "<strong>Modes de garde manquants.</strong> Vous avez des enfants, mais aucun mode de garde n'est défini. Cliquez sur l'icône ⚙️ (Paramètres du calendrier) pour les ajouter.";
}
// 3. Référentiel des types de congés vide
try {
$countLeaveTypes = $pdo->query("SELECT COUNT(*) FROM pf_leave_types")->fetchColumn();
if ($countLeaveTypes == 0) {
$calendarAlerts[] = "<strong>Types de congés manquants.</strong> Le catalogue des congés (CP, RTT...) est vide. Ajoutez-les dans les paramètres généraux.";
}
} catch (Exception $e) {
// Si la table n'existe pas encore
$calendarAlerts[] = "<strong>Table des congés manquante.</strong> Veuillez mettre à jour la base de données.";
}
// 4. Zone scolaire manquante (si tu l'as stockée dans pf_foyer_settings)
$zoneScolaire = $foyerData['zone_scolaire'] ?? null;
if (empty($zoneScolaire)) {
$calendarAlerts[] = "<strong>Zone scolaire non définie.</strong> Cliquez sur l'icône ⚙️ pour configurer votre zone afin d'afficher les vacances scolaires.";
}
$pageTitle = tr('fc_page_title'); $pageTitle = tr('fc_page_title');
$activePage = "family-calendar"; $activePage = "family-calendar";
$mainClass = "pf-family-calendar"; $mainClass = "pf-family-calendar";
@@ -77,6 +109,20 @@ require __DIR__ . '/header.php';
</div> </div>
</div> </div>
<?php if (!empty($calendarAlerts)): ?>
<div class="pf-alert pf-alert-warning" style="background: var(--bg-warning, #fffbeb); border: 1px solid var(--border-warning, #fcd34d); color: var(--text-warning, #92400e); padding: 16px; border-radius: 8px; margin-bottom: 24px; display: flex; gap: 12px; align-items: flex-start;">
<div style="font-size: 1.5rem; line-height: 1;">⚠️</div>
<div>
<h4 style="margin: 0 0 8px 0; font-size: 1.05rem;">Configuration incomplète</h4>
<ul style="margin: 0; padding-left: 18px; font-size: 0.95rem; line-height: 1.5;">
<?php foreach ($calendarAlerts as $alert): ?>
<li style="margin-bottom: 4px;"><?= $alert ?></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif; ?>
<div id="modalHolidays" class="pf-modal"> <div id="modalHolidays" class="pf-modal">
<div class="pf-modal-content"> <div class="pf-modal-content">
<div class="pf-modal-header"> <div class="pf-modal-header">
@@ -155,61 +201,120 @@ require __DIR__ . '/header.php';
</div> </div>
<div id="modalCalendarSettings" class="pf-modal"> <div id="modalCalendarSettings" class="pf-modal">
<div class="pf-modal-content" style="max-width: 600px; width: 90%;"> <div class="pf-modal-content" style="max-width: 650px; width: 95%;">
<div class="pf-modal-header">
<h3 class="pf-modal-title">⚙️ <?= tr('fc_modal_settings_title') ?></h3> <div class="pf-modal-header">
<button type="button" class="pf-modal-close" onclick="closeCalendarSettings()">&times;</button> <h3 class="pf-modal-title">⚙️ <?= tr('fc_modal_settings_title') ?></h3>
</div> <button type="button" class="pf-modal-close" onclick="closeCalendarSettings()">&times;</button>
</div>
<div style="display: flex; gap: 4px; background: var(--bg-page); padding: 4px; border-radius: 8px; margin: 1rem;"> <div style="display: flex; gap: 4px; background: var(--bg-page); padding: 4px; border-radius: 8px; margin: 1rem;">
<button type="button" id="tab-btn-foyer" class="bs-tab-btn active" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('foyer')">🏡 Foyer</button> <button type="button" id="tab-btn-foyer" class="bs-tab-btn active" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('foyer')">
<button type="button" id="tab-btn-membres" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('membres')">👥 Membres & Droits</button> 🏡 <?= tr('fc_tab_foyer') ?>
</div> </button>
<button type="button" id="tab-btn-membres" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('membres')">
👥 <?= tr('fc_tab_members') ?>
</button>
</div>
<div class="pf-modal-body" style="max-height: 60vh; overflow-y: auto; padding-top: 0;"> <div class="pf-modal-body" style="max-height: 65vh; overflow-y: auto; padding-top: 0; padding-bottom: 1rem;">
<div id="cal-pane-foyer" class="cal-settings-pane"> <div id="cal-pane-foyer" class="cal-settings-pane">
<form id="formCalFoyer" onsubmit="submitCalFoyer(event)">
<div class="pf-form-group"> <form id="formCalFoyer" onsubmit="submitCalFoyer(event)">
<label class="pf-label"><?= tr('fc_zone_label') ?></label>
<select id="setZoneScolaire" name="zone_scolaire" class="pf-input" required style="width: 100%;"> <div class="pf-form-group">
<option value="A">Zone A</option> <label class="pf-label"><?= tr('fc_zone_label') ?></label>
<option value="B">Zone B</option> <select id="setZoneScolaire" name="zone_scolaire" class="pf-input" required style="width: 100%;">
<option value="C">Zone C</option> <option value="A"><?= tr('fc_zone_a') ?></option>
<option value="Autre">Autre / Hors France</option> <option value="B"><?= tr('fc_zone_b') ?></option>
</select> <option value="C"><?= tr('fc_zone_c') ?></option>
<small class="pf-muted-note"><?= tr('fc_zone_desc') ?></small> <option value="Autre"><?= tr('fc_zone_other') ?></option>
</div> </select>
<small class="pf-muted-note"><?= tr('fc_zone_desc') ?></small>
</div>
<div class="pf-form-group" style="margin-top: 1.5rem;"> <div class="pf-form-group" style="margin-top: 1.5rem;">
<label class="pf-label"><?= tr('fc_care_modes_label') ?></label> <label class="pf-label"><?= tr('fc_care_modes_label') ?></label>
<?php if (!empty($kids)): ?>
<div id="careModesContainer" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; min-height: 38px; padding: 8px; background: var(--bg-page); border-radius: var(--radius); border: 1px solid var(--border-light);"></div> <div id="careModesContainer" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; min-height: 38px; padding: 8px; background: var(--bg-page); border-radius: var(--radius); border: 1px solid var(--border-light);"></div>
<div style="display: flex; gap: 8px;"> <div style="display: flex; gap: 8px;">
<input type="text" id="inputNewCareMode" class="pf-input" placeholder="<?= tr('fc_add_mode_placeholder') ?>" style="flex: 1;"> <input type="text" id="inputNewCareMode" class="pf-input" placeholder="<?= tr('fc_add_mode_placeholder') ?>" style="flex: 1;">
<button type="button" class="btn btn-secondary" style="padding: 0 16px; border-radius: 6px;" onclick="addCareModeTag()"></button> <button type="button" class="pf-btn btn-secondary" style="padding: 0 16px; border-radius: 6px;" onclick="addCareModeTag()"></button>
</div> </div>
<small class="pf-muted-note"><?= tr('fc_care_modes_desc') ?></small> <small class="pf-muted-note"><?= tr('fc_care_modes_desc') ?></small>
</div> <?php else: ?>
<div style="background: var(--bg-subtle); padding: 12px; border-radius: 8px; border: 1px dashed var(--border-main); text-align: center;">
<div style="margin-top: 2rem; display: flex; justify-content: flex-end; gap: 8px;"> <p style="margin: 0 0 10px 0; color: var(--text-muted); font-size: 0.9rem;">
<button type="button" class="pf-btn pf-btn-secondary" onclick="closeCalendarSettings()"><?= tr('btn_cancel') ?></button> <?= tr('fc_empty_kids_msg') ?>
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('save') ?></button> </p>
</div> <a href="/settings.php" class="pf-btn btn-secondary" style="text-decoration: none; display: inline-block; font-size: 0.85rem;">
</form> ⚙️ <?= tr('fc_link_settings') ?>
</div> </a>
</div>
<div id="cal-pane-membres" class="cal-settings-pane" style="display: none;"> <?php endif; ?>
<div style="margin-bottom: 1.2rem;">
<label class="pf-label">Sélectionner un membre de la famille :</label>
<select id="selectCalMember" class="pf-input" style="width: 100%; font-weight: 600;" onchange="loadMemberConfigView()"></select>
</div> </div>
<div id="memberConfigZone" style="background: var(--bg-page); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); min-height: 100px;"> <?php if (!empty($kids)): ?>
</div> <div style="margin-top: 1rem; display: flex; justify-content: flex-end;">
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('save') ?></button>
</div>
<?php endif; ?>
</form>
<div class="pf-form-group" style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border-light);">
<h4 style="margin: 0 0 10px 0; color: var(--text-main);">🏖️ <?= tr('fc_leave_catalog_title') ?></h4>
<p style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 15px;">
<?= tr('fc_leave_catalog_desc') ?>
</p>
<div id="leaveTypesContainer" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px;"></div>
<div style="background: var(--bg-subtle); padding: 12px; border-radius: 8px; border: 1px solid var(--border-light);">
<h5 id="leaveTypeFormTitle" style="margin: 0 0 10px 0;">+ <?= tr('fc_add_leave_type') ?></h5>
<form id="formLeaveType" style="display: flex; flex-direction: column; gap: 10px;">
<input type="hidden" id="lt-mode" value="add">
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<div style="flex: 0 0 80px;">
<label class="pf-label"><?= tr('fc_col_code') ?> *</label>
<input type="text" id="lt-code" class="pf-input" maxlength="4" style="text-transform: uppercase;" required>
<small class="pf-muted-note" id="lt-code-note"><?= tr('fc_code_irreversible') ?></small>
</div>
<div style="flex: 1; min-width: 150px;">
<label class="pf-label"><?= tr('fc_col_label') ?> *</label>
<input type="text" id="lt-label" class="pf-input" placeholder="<?= tr('fc_leave_label_ph') ?>" required>
</div>
</div>
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 5px;">
<button type="button" class="pf-btn btn-secondary" onclick="resetLeaveTypeForm()"><?= tr('btn_cancel') ?></button>
<button type="button" class="pf-btn pf-btn-primary" onclick="saveLeaveType()"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
</div> </div>
</div> </div>
<div id="cal-pane-membres" class="cal-settings-pane" style="display: none;">
<div style="margin-bottom: 1.2rem;">
<label class="pf-label"><?= tr('fc_select_member') ?></label>
<select id="selectCalMember" class="pf-input" style="width: 100%; font-weight: 600;" onchange="loadMemberConfigView()">
</select>
</div>
<div id="memberConfigZone" style="background: var(--bg-page); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); min-height: 150px;">
</div>
</div>
</div> </div>
</div> </div>
</div>
<div class="fc-month-calendar-wrapper"> <div class="fc-month-calendar-wrapper">
<div class="fc-month-header"> <div class="fc-month-header">
+493 -221
View File
@@ -15,27 +15,24 @@ $activePage = "gift-list";
$mainClass = "pf-gift-list"; $mainClass = "pf-gift-list";
$pageCss = "/modules/gift-list/gift-list.css"; $pageCss = "/modules/gift-list/gift-list.css";
// --- 2. RÉCUPÉRATION DYNAMIQUE DES DONNÉES (Multi-tenant) --- // --- 2. RÉCUPÉRATION DYNAMIQUE DES DONNÉES ---
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role = 'enfant' AND is_active = 1 ORDER BY name ASC"); // On inclut les enfants du foyer ET les proches enfants
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role IN ('enfant', 'proche_enfant') ORDER BY name ASC");
$children = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: ['Aucun enfant configuré']; $children = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: ['Aucun enfant configuré'];
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role NOT IN ('enfant', 'nounou') AND is_active = 1 ORDER BY name ASC"); // On inclut les adultes du foyer ET les proches adultes (On exclut nounou et les enfants)
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role NOT IN ('enfant', 'proche_enfant', 'nounou') ORDER BY name ASC");
$allAdultsList = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: ['Aucun adulte configuré']; $allAdultsList = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: ['Aucun adulte configuré'];
$stmt = $pdo->query("SELECT code, name, month_date FROM pf_gift_occasions WHERE is_active = 1 ORDER BY month_date ASC, id ASC"); // On récupère toutes les occasions (avec l'icône)
$stmt = $pdo->query("SELECT code, name, icon, month_date FROM pf_gift_occasions WHERE is_active = 1 ORDER BY sort_order ASC, month_date ASC, id ASC");
$activeOccasions = $stmt->fetchAll(PDO::FETCH_ASSOC); $activeOccasions = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($activeOccasions)) { if (empty($activeOccasions)) {
$activeOccasions = [['code' => 'DEMO', 'name' => 'Fête par défaut', 'month_date' => null]]; $activeOccasions = [['code' => 'DEMO', 'name' => 'Fête par défaut', 'icon' => '🎁', 'month_date' => null]];
} }
$allowedOccasionCodes = array_column($activeOccasions, 'code'); $allowedOccasionCodes = array_column($activeOccasions, 'code');
$occasionIcons = [
'TIO' => '/modules/gift-list/assets/img/tio.png', 'NOEL' => '/modules/gift-list/assets/img/santa.png',
'ROIS' => '/modules/gift-list/assets/img/reis.png', 'ANNIV' => '/modules/gift-list/assets/img/corona.png',
'SANT' => '/modules/gift-list/assets/img/sant.png'
];
// --- 3. CHARGEMENT DES CADEAUX --- // --- 3. CHARGEMENT DES CADEAUX ---
$inMarks = implode(',', array_fill(0, count($allowedOccasionCodes), '?')); $inMarks = implode(',', array_fill(0, count($allowedOccasionCodes), '?'));
$sql = "SELECT * FROM pf_gifts WHERE year = ? AND occasion IN ($inMarks) ORDER BY occasion ASC, child_name ASC, adult_name ASC"; $sql = "SELECT * FROM pf_gifts WHERE year = ? AND occasion IN ($inMarks) ORDER BY occasion ASC, child_name ASC, adult_name ASC";
@@ -43,8 +40,8 @@ $stmt = $pdo->prepare($sql);
$stmt->execute(array_merge([$year], $allowedOccasionCodes)); $stmt->execute(array_merge([$year], $allowedOccasionCodes));
$gifts = $stmt->fetchAll(PDO::FETCH_ASSOC); $gifts = $stmt->fetchAll(PDO::FETCH_ASSOC);
$data = []; $data = [];
$adultsInView = []; $adultsInView = [];
foreach ($gifts as $g) { foreach ($gifts as $g) {
$data[$g['occasion']][$g['child_name']]['gifts'][] = $g; $data[$g['occasion']][$g['child_name']]['gifts'][] = $g;
@@ -78,7 +75,7 @@ for ($i = 0; $i < $countPeople; $i++) {
for ($j = $i + 1; $j < $countPeople; $j++) { for ($j = $i + 1; $j < $countPeople; $j++) {
$a = $people[$i]; $b = $people[$j]; $a = $people[$i]; $b = $people[$j];
$net = $matrix[$a][$b] - $matrix[$b][$a]; $net = $matrix[$a][$b] - $matrix[$b][$a];
if ($net > 0.01) { $settlements[] = ['from' => $a, 'to' => $b, 'amount' => $net]; } if ($net > 0.01) { $settlements[] = ['from' => $a, 'to' => $b, 'amount' => $net]; }
elseif ($net < -0.01) { $settlements[] = ['from' => $b, 'to' => $a, 'amount' => -$net]; } elseif ($net < -0.01) { $settlements[] = ['from' => $b, 'to' => $a, 'amount' => -$net]; }
} }
} }
@@ -87,22 +84,26 @@ require __DIR__ . '/header.php';
?> ?>
<div class="pf-container cl-view-dynamic"> <div class="pf-container cl-view-dynamic">
<div class="cl-titlebar"> <div class="cl-titlebar">
<h1>🎁 <?= sprintf(tr('gift_main_title'), $year) ?></h1> <div style="display: flex; align-items: center; gap: 12px;">
<button class="btn btn-ghost btn-icon" id="btn-open-gift-settings" title="<?= tr('settings') ?>">⚙️</button> <h1 style="margin: 0;">🎁 <?= sprintf(tr('gift_main_title'), $year) ?></h1>
<button id="btn-open-gift-settings" class="btn-settings-gear" title="<?= tr('gift_settings') ?>" style="font-size: 1.4rem; padding: 4px;">
⚙️
</button>
</div>
</div> </div>
<div class="pf-filter-bar"> <div class="pf-filter-bar">
<span class="pf-filter-icon">🔍</span> <span class="pf-filter-icon">🔍</span>
<div class="pf-multi-select" id="ms-child"> <div class="pf-multi-select" id="ms-child">
<div class="pf-ms-trigger" onclick="toggleMS('ms-child-list', this)"> <div class="pf-ms-trigger" onclick="toggleMS('ms-child-list', this)">
👦 <span id="ms-child-label"><?= tr('gift_filter_all_children') ?></span> 👦 <span id="ms-child-label"><?= tr('gift_filter_all_children') ?></span>
</div> </div>
<div class="pf-ms-dropdown" id="ms-child-list"> <div class="pf-ms-dropdown" id="ms-child-list">
<label class="pf-ms-option is-all"> <label class="pf-ms-option is-all">
<input type="checkbox" value="all" checked onchange="handleMSChange(this, 'child')"> <input type="checkbox" value="all" checked onchange="handleMSChange(this, 'child')">
<?= tr('gift_filter_all_children') ?> <?= tr('gift_filter_all_children') ?>
</label> </label>
<?php foreach($children as $c): ?> <?php foreach($children as $c): ?>
@@ -117,7 +118,7 @@ require __DIR__ . '/header.php';
</div> </div>
<div class="pf-ms-dropdown" id="ms-adult-list"> <div class="pf-ms-dropdown" id="ms-adult-list">
<label class="pf-ms-option is-all"> <label class="pf-ms-option is-all">
<input type="checkbox" value="all" checked onchange="handleMSChange(this, 'adult')"> <input type="checkbox" value="all" checked onchange="handleMSChange(this, 'adult')">
<?= tr('gift_filter_all_adults') ?> <?= tr('gift_filter_all_adults') ?>
</label> </label>
<?php foreach($allAdultsList as $a): ?> <?php foreach($allAdultsList as $a): ?>
@@ -128,29 +129,27 @@ require __DIR__ . '/header.php';
</div> </div>
<section class="pf-section"> <section class="pf-section">
<?php foreach ($activeOccasions as $occ): <?php foreach ($activeOccasions as $occ):
$occCode = $occ['code']; $occCode = $occ['code'];
// Affichage Brut de la BDD pour le nom de l'occasion
$occName = $occ['name']; $occName = $occ['name'];
$occIcon = $occ['icon'] ?: '🎁';
?> ?>
<div class="js-occ-section"> <div class="js-occ-section">
<h2 class="cl-occasion-title"> <h2 class="cl-occasion-title">
<?php if(!empty($occasionIcons[$occCode])): ?> <span class="cl-occasion-icon"><?= htmlspecialchars($occIcon) ?></span>
<img src="<?= $occasionIcons[$occCode] ?>" class="cl-occasion-icon" alt="">
<?php else: ?>
<span>🎀</span>
<?php endif; ?>
<?= htmlspecialchars($occName) ?> <?= htmlspecialchars($occName) ?>
</h2> </h2>
<?php foreach ($children as $child): <?php foreach ($children as $child):
$childData = $data[$occCode][$child] ?? ['gifts' => [], 'totals' => []]; $childData = $data[$occCode][$child] ?? ['gifts' => [], 'totals' => []];
?> ?>
<div class="pf-child-section js-child" data-name="<?= htmlspecialchars($child) ?>"> <div class="pf-child-section js-child" data-name="<?= htmlspecialchars($child) ?>">
<div class="pf-child-header"> <div class="pf-child-header">
<h3>👦 <?= htmlspecialchars($child) ?></h3> <h3>👦 <?= htmlspecialchars($child) ?></h3>
<button class="pf-btn pf-btn-small btn-add-gift" <button class="pf-btn pf-btn-small btn-add-gift"
data-child="<?= htmlspecialchars($child) ?>" data-child="<?= htmlspecialchars($child) ?>"
data-occ="<?= htmlspecialchars($occCode) ?>" data-occ="<?= htmlspecialchars($occCode) ?>"
data-adults="<?= htmlspecialchars(json_encode(array_values($allAdultsList))) ?>"> data-adults="<?= htmlspecialchars(json_encode(array_values($allAdultsList))) ?>">
<?= tr('gift_add_gift') ?> <?= tr('gift_add_gift') ?>
</button> </button>
@@ -171,7 +170,7 @@ require __DIR__ . '/header.php';
<div class="pf-gift-card-compact js-gift-card" data-adult="<?= htmlspecialchars($g['adult_name']) ?>"> <div class="pf-gift-card-compact js-gift-card" data-adult="<?= htmlspecialchars($g['adult_name']) ?>">
<div> <div>
<h4 class="pf-gift-title"> <h4 class="pf-gift-title">
<?= htmlspecialchars($g['gift_description']) ?> <?= htmlspecialchars($g['gift_description']) ?>
<?php if($g['product_link']): ?><a href="<?= htmlspecialchars($g['product_link']) ?>" target="_blank" class="pf-gift-link">🔗</a><?php endif; ?> <?php if($g['product_link']): ?><a href="<?= htmlspecialchars($g['product_link']) ?>" target="_blank" class="pf-gift-link">🔗</a><?php endif; ?>
</h4> </h4>
<span class="pf-pill-adult">👤 <?= htmlspecialchars($g['adult_name']) ?></span> <span class="pf-pill-adult">👤 <?= htmlspecialchars($g['adult_name']) ?></span>
@@ -197,15 +196,15 @@ require __DIR__ . '/header.php';
</section> </section>
<section class="pf-section pf-section--panel gift-tricount-section"> <section class="pf-section pf-section--panel gift-tricount-section">
<h2 class="gift-tricount-title">⚖️ <?= tr('gift_liquidations') ?? 'Bilan & Remboursements' ?></h2> <h2 class="gift-tricount-title">⚖️ <?= tr('gift_liquidations') ?></h2>
<?php if (empty($settlements)): ?> <?php if (empty($settlements)): ?>
<p class="gift-tricount-success">✅ <?= tr('gift_no_debt') ?></p> <p class="gift-tricount-success">✅ <?= tr('gift_no_debt') ?></p>
<?php else: ?> <?php else: ?>
<ul class="pf-tricount-list"> <ul class="pf-tricount-list">
<?php foreach ($settlements as $s): ?> <?php foreach ($settlements as $s): ?>
<li class="pf-tricount-item"> <li class="pf-tricount-item">
<span><strong><?= htmlspecialchars($s['from']) ?></strong> <?= tr('gift_owes') ?> à <?= htmlspecialchars($s['to']) ?></span> <span><strong><?= htmlspecialchars($s['from']) ?></strong> <?= tr('gift_owes') ?> <?= tr('gift_to') ?> <?= htmlspecialchars($s['to']) ?></span>
<strong class="gift-tricount-debt-amount"><?= number_format($s['amount'], 2, ',', ' ') ?> €</strong> <strong class="gift-tricount-debt-amount"><?= number_format($s['amount'], 2, ',', ' ') ?> €</strong>
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
@@ -226,7 +225,7 @@ require __DIR__ . '/header.php';
<?php foreach ($people as $debtor): ?> <?php foreach ($people as $debtor): ?>
<tr> <tr>
<th class="sticky-col bg-body"><?= htmlspecialchars($debtor) ?></th> <th class="sticky-col bg-body"><?= htmlspecialchars($debtor) ?></th>
<?php foreach ($people as $creditor): <?php foreach ($people as $creditor):
$val = $matrix[$debtor][$creditor] ?? 0; $val = $matrix[$debtor][$creditor] ?? 0;
$display = ($debtor === $creditor) || $val == 0 ? '—' : number_format($val, 2, ',', ' ') . ' €'; $display = ($debtor === $creditor) || $val == 0 ? '—' : number_format($val, 2, ',', ' ') . ' €';
$cellClass = $val > 0 ? 'gift-cell-danger' : 'gift-cell-muted'; $cellClass = $val > 0 ? 'gift-cell-danger' : 'gift-cell-muted';
@@ -242,26 +241,27 @@ require __DIR__ . '/header.php';
</section> </section>
</div> </div>
<!-- 1. Modale CRUD Cadeau -->
<div id="pf-gift-modal" class="pf-modal"> <div id="pf-gift-modal" class="pf-modal">
<div class="pf-modal-content gift-modal-custom-content"> <div class="pf-modal-content gift-modal-custom-content">
<div class="gift-modal-custom-header"> <div class="gift-modal-custom-header">
<h3 id="modalTitle" class="pf-modal-title gift-modal-custom-title">Cadeau</h3> <h3 id="modalTitle" class="pf-modal-title gift-modal-custom-title">Cadeau</h3>
<button type="button" class="btn-modal-close gift-modal-close-btn">&times;</button> <button type="button" class="btn-modal-close gift-modal-close-btn">&times;</button>
</div> </div>
<form method="post" action="/modules/gift-list/save-gift.php" id="giftForm"> <form method="post" action="/modules/gift-list/save-gift.php" id="giftForm">
<input type="hidden" name="year" value="<?= $year ?>"> <input type="hidden" name="year" value="<?= $year ?>">
<input type="hidden" name="child_name" id="modalChild"> <input type="hidden" name="child_name" id="modalChild">
<input type="hidden" name="occasion" id="modalOccasion"> <input type="hidden" name="occasion" id="modalOccasion">
<input type="hidden" name="action" id="modalAction"> <input type="hidden" name="action" id="modalAction">
<input type="hidden" name="gift_id" id="modalGiftId"> <input type="hidden" name="gift_id" id="modalGiftId">
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_col_adult') ?></label><select name="adult_name" id="modalAdult" class="pf-input" required></select></div> <div class="pf-form-group"><label class="pf-label"><?= tr('gift_col_adult') ?></label><select name="adult_name" id="modalAdult" class="pf-input" required></select></div>
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_payer') ?></label><select name="payer_name" id="modalPayer" class="pf-input" required></select></div> <div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_payer') ?></label><select name="payer_name" id="modalPayer" class="pf-input" required></select></div>
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_gift_name') ?></label><input type="text" name="gift_description" id="modalDesc" class="pf-input" required></div> <div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_gift_name') ?></label><input type="text" name="gift_description" id="modalDesc" class="pf-input" required></div>
<div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_price') ?></label><input type="number" step="0.01" name="amount" id="modalAmount" class="pf-input"></div> <div class="pf-form-group"><label class="pf-label"><?= tr('gift_modal_price') ?></label><input type="number" step="0.01" name="amount" id="modalAmount" class="pf-input"></div>
<div class="pf-form-group gift-form-group-spaced"><label class="pf-label"><?= tr('gift_modal_link') ?></label><input type="url" name="product_link" id="modalLink" class="pf-input"></div> <div class="pf-form-group gift-form-group-spaced"><label class="pf-label"><?= tr('gift_modal_link') ?></label><input type="url" name="product_link" id="modalLink" class="pf-input"></div>
<div class="modal-footer gift-modal-custom-footer"> <div class="modal-footer gift-modal-custom-footer">
<button type="button" class="pf-btn btn-secondary btn-modal-close"><?= tr('btn_cancel') ?></button> <button type="button" class="pf-btn btn-secondary btn-modal-close"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button> <button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
@@ -270,33 +270,86 @@ require __DIR__ . '/header.php';
</div> </div>
</div> </div>
<!-- 2. Modale Paramètres Fêtes --> <!-- 2. Modale Paramètres (Occasions & Proches) -->
<div class="gift-settings-backdrop" id="modal-gift-settings"> <div id="modal-gift-settings" class="pf-modal">
<div class="gift-settings-modal"> <div class="pf-modal-content" style="max-width: 600px; width: 90%;">
<div class="gift-settings-header">
<h3>⚙️ <?= tr('gift_settings_title') ?? 'Configuration' ?></h3> <div class="pf-modal-header" style="display: flex; justify-content: space-between; align-items: center;">
<button class="gift-settings-close" id="btn-close-gift-settings">×</button> <h3 class="pf-modal-title" style="margin: 0;">⚙️ <?= tr('gift_settings') ?></h3>
<button type="button" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer;" onclick="closeGiftSettings()">&times;</button>
</div> </div>
<div class="gift-settings-body">
<div style="display: flex; gap: 4px; background: var(--bg-page); padding: 4px; border-radius: 8px; margin: 1rem 0;">
<button type="button" id="tab-btn-occasions" class="bs-tab-btn active" style="flex: 1; padding: 8px 12px; font-weight: 600; border-radius: 6px; border: none; cursor: pointer; transition: 0.2s;" onclick="switchGLTab('occasions')"><?= tr('gift_tab_occasions') ?></button>
<button type="button" id="tab-btn-proches" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600; border-radius: 6px; border: none; cursor: pointer; transition: 0.2s; background: transparent;" onclick="switchGLTab('proches')"><?= tr('gift_tab_proches') ?></button>
</div>
<div class="pf-modal-body" style="min-height: 450px; max-height: 60vh; overflow-y: auto; padding: 0 1rem 1rem 1rem; display: flex; flex-direction: column;">
<h4 class="gift-add-subtitle"><?= tr('gift_settings_add_title') ?? '+ Ajouter' ?></h4> <div id="gl-pane-occasions" class="gl-pane active" style="display: block; animation: fadeIn 0.3s ease;">
<form id="form-add-occasion" class="gift-add-form"> <div id="occasions-list" style="margin-bottom: 20px;">⏳ <?= tr('loading') ?></div>
<input type="text" class="pf-input" name="name" placeholder="<?= tr('gift_settings_name_placeholder') ?? 'Nom' ?>" required style="flex: 1; min-width: 150px; padding: 0.4rem 0.75rem;">
<input type="text" class="pf-input" name="month_date" placeholder="<?= tr('gift_settings_date_placeholder') ?? 'MM-JJ' ?>" style="width: 120px; padding: 0.4rem 0.75rem;">
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?? 'Ajouter' ?></button>
</form>
<small class="gift-help-text"><?= tr('gift_settings_date_help') ?? 'Optionnel' ?></small>
<hr style="border:0; border-bottom:1px solid var(--border-light); margin: 1rem 0;">
<form id="form-save-toggles">
<div id="occasions-list-container" class="gift-occasions-list"></div>
<div class="modal-footer gift-modal-custom-footer" style="padding-top: 1rem; border-top: 1px solid var(--border-light);"> <hr style="border:0; border-top: 1px solid var(--border-light); margin: 20px 0;">
<button type="button" class="btn btn-secondary" id="btn-cancel-settings"><?= tr('btn_cancel') ?? 'Annuler' ?></button>
<button type="submit" class="btn btn-primary"><?= tr('btn_save') ?? 'Enregistrer' ?></button> <h5 style="margin: 0 0 15px 0; font-size: 1rem; color: var(--text-main);"><?= tr('gift_settings_add_occ') ?></h5>
</div> <form id="form-add-occasion" style="display: flex; flex-direction: column; gap: 16px;">
</form> <div>
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_settings_icon') ?></label>
<input type="hidden" name="icon" id="add-occ-icon" value="🎁">
<div class="emoji-picker" style="display: flex; gap: 8px; flex-wrap: wrap;">
<div class="emoji-option selected" onclick="selectEmoji(this, 'add-occ-icon', '🎁')">🎁</div>
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎄')">🎄</div>
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎂')">🎂</div>
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎉')">🎉</div>
<div class="emoji-option" onclick="selectEmoji(this, 'add-occ-icon', '🎓')">🎓</div>
</div>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 150px;">
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_modal_gift_name') ?> *</label>
<input type="text" name="name" id="add-occ-name" class="pf-input" style="width: 100%;" required>
</div>
<div style="width: 110px;">
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_settings_date') ?></label>
<input type="text" name="month_date" id="add-occ-date" class="pf-input" style="width: 100%;" placeholder="MM-DD">
</div>
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px;">
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('form-add-occasion').reset()"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('btn_add') ?></button>
</div>
</form>
</div>
<div id="gl-pane-proches" class="gl-pane" style="display: none; animation: fadeIn 0.3s ease;">
<div id="proches-list" style="margin-bottom: 20px;">⏳ <?= tr('loading') ?></div>
<hr style="border:0; border-top: 1px solid var(--border-light); margin: auto 0 20px 0;">
<h5 style="margin: 0 0 15px 0; font-size: 1rem; color: var(--text-main);"><?= tr('gift_settings_add_proche') ?></h5>
<form id="form-add-proche" style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 150px;">
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_ph_proche_name') ?> *</label>
<input type="text" name="name" id="add-proche-name" class="pf-input" style="width: 100%;" required>
</div>
<div style="flex: 1; min-width: 150px;">
<label class="pf-label" style="display: block; margin-bottom: 6px;"><?= tr('gift_role_label') ?> *</label>
<select name="role" id="add-proche-role" class="pf-input" style="width: 100%;" required>
<option value="proche_adulte"><?= tr('gift_role_proche_adulte') ?></option>
<option value="proche_enfant"><?= tr('gift_role_proche_enfant') ?></option>
</select>
</div>
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 5px;">
<button type="button" class="pf-btn btn-secondary" onclick="document.getElementById('form-add-proche').reset()"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('btn_add') ?></button>
</div>
</form>
</div>
</div> </div>
</div> </div>
@@ -306,6 +359,7 @@ require __DIR__ . '/header.php';
// ========================================== // ==========================================
// 1. GESTION DU COMPOSANT MULTI-SELECT VANILLA // 1. GESTION DU COMPOSANT MULTI-SELECT VANILLA
// ========================================== // ==========================================
function toggleMS(listId, triggerEl) { function toggleMS(listId, triggerEl) {
document.querySelectorAll('.pf-ms-dropdown').forEach(el => { if (el.id !== listId) el.classList.remove('open'); }); document.querySelectorAll('.pf-ms-dropdown').forEach(el => { if (el.id !== listId) el.classList.remove('open'); });
document.querySelectorAll('.pf-ms-trigger').forEach(el => { if (el !== triggerEl) el.classList.remove('active'); }); document.querySelectorAll('.pf-ms-trigger').forEach(el => { if (el !== triggerEl) el.classList.remove('active'); });
@@ -324,7 +378,7 @@ document.addEventListener('click', e => {
function handleMSChange(cb, type) { function handleMSChange(cb, type) {
const container = document.getElementById('ms-' + type + '-list'); const container = document.getElementById('ms-' + type + '-list');
if (!container) return; if (!container) return;
if (cb.value === 'all' && cb.checked) { if (cb.value === 'all' && cb.checked) {
container.querySelectorAll('input[type="checkbox"]:not([value="all"])').forEach(i => i.checked = false); container.querySelectorAll('input[type="checkbox"]:not([value="all"])').forEach(i => i.checked = false);
} else if (cb.checked) { } else if (cb.checked) {
@@ -337,10 +391,10 @@ function handleMSChange(cb, type) {
if (allCb) allCb.checked = true; if (allCb) allCb.checked = true;
} }
} }
const checkedSpecifics = container.querySelectorAll('input[type="checkbox"]:checked:not([value="all"])'); const checkedSpecifics = container.querySelectorAll('input[type="checkbox"]:checked:not([value="all"])');
const labelEl = document.getElementById('ms-' + type + '-label'); const labelEl = document.getElementById('ms-' + type + '-label');
if (labelEl) { if (labelEl) {
if (checkedSpecifics.length === 0) { if (checkedSpecifics.length === 0) {
let defaultText = 'Tous'; let defaultText = 'Tous';
@@ -362,23 +416,23 @@ function applyGiftFilters() {
const cList = document.getElementById('ms-child-list'); const cList = document.getElementById('ms-child-list');
const aList = document.getElementById('ms-adult-list'); const aList = document.getElementById('ms-adult-list');
if (!cList || !aList) return; if (!cList || !aList) return;
const allChildCb = cList.querySelector('input[value="all"]'); const allChildCb = cList.querySelector('input[value="all"]');
const cAll = allChildCb ? allChildCb.checked : true; const cAll = allChildCb ? allChildCb.checked : true;
const cVals = Array.from(cList.querySelectorAll('input[type="checkbox"]:checked:not([value="all"])')).map(i => i.value); const cVals = Array.from(cList.querySelectorAll('input[type="checkbox"]:checked:not([value="all"])')).map(i => i.value);
const allAdultCb = aList.querySelector('input[value="all"]'); const allAdultCb = aList.querySelector('input[value="all"]');
const aAll = allAdultCb ? allAdultCb.checked : true; const aAll = allAdultCb ? allAdultCb.checked : true;
const aVals = Array.from(aList.querySelectorAll('input[type="checkbox"]:checked:not([value="all"])')).map(i => i.value); const aVals = Array.from(aList.querySelectorAll('input[type="checkbox"]:checked:not([value="all"])')).map(i => i.value);
document.querySelectorAll('.js-occ-section').forEach(occSec => { document.querySelectorAll('.js-occ-section').forEach(occSec => {
let occHasVisible = false; let occHasVisible = false;
occSec.querySelectorAll('.js-child').forEach(childSec => { occSec.querySelectorAll('.js-child').forEach(childSec => {
const cName = childSec.getAttribute('data-name'); const cName = childSec.getAttribute('data-name');
const matchChild = cAll || (cVals.indexOf(cName) !== -1); const matchChild = cAll || (cVals.indexOf(cName) !== -1);
let childHasVisibleCard = false; let childHasVisibleCard = false;
if (matchChild) { if (matchChild) {
childSec.querySelectorAll('.js-gift-card').forEach(card => { childSec.querySelectorAll('.js-gift-card').forEach(card => {
const aName = card.getAttribute('data-adult'); const aName = card.getAttribute('data-adult');
@@ -386,23 +440,23 @@ function applyGiftFilters() {
card.style.display = matchAdult ? 'flex' : 'none'; card.style.display = matchAdult ? 'flex' : 'none';
if (matchAdult) childHasVisibleCard = true; if (matchAdult) childHasVisibleCard = true;
}); });
childSec.querySelectorAll('.js-pill-adult').forEach(pill => { childSec.querySelectorAll('.js-pill-adult').forEach(pill => {
const aName = pill.getAttribute('data-adult'); const aName = pill.getAttribute('data-adult');
const matchAdult = aAll || (aVals.indexOf(aName) !== -1); const matchAdult = aAll || (aVals.indexOf(aName) !== -1);
pill.style.opacity = matchAdult ? '1' : '0.2'; pill.style.opacity = matchAdult ? '1' : '0.2';
}); });
const empty = childSec.querySelector('.js-empty-state'); const empty = childSec.querySelector('.js-empty-state');
if (empty) empty.style.display = childHasVisibleCard ? 'none' : 'block'; if (empty) empty.style.display = childHasVisibleCard ? 'none' : 'block';
childSec.style.display = 'block'; childSec.style.display = 'block';
occHasVisible = true; occHasVisible = true;
} else { } else {
childSec.style.display = 'none'; childSec.style.display = 'none';
} }
}); });
occSec.style.display = occHasVisible ? 'block' : 'none'; occSec.style.display = occHasVisible ? 'block' : 'none';
}); });
} }
@@ -449,16 +503,16 @@ document.body.addEventListener('click', async function(e) {
const occCode = btnAdd.dataset.occ; const occCode = btnAdd.dataset.occ;
const adultsList = JSON.parse(btnAdd.dataset.adults || '[]'); const adultsList = JSON.parse(btnAdd.dataset.adults || '[]');
document.getElementById('modalAction').value = 'create'; document.getElementById('modalAction').value = 'create';
document.getElementById('modalGiftId').value = ''; document.getElementById('modalGiftId').value = '';
document.getElementById('modalChild').value = childName; document.getElementById('modalChild').value = childName;
document.getElementById('modalOccasion').value = occCode; document.getElementById('modalOccasion').value = occCode;
document.getElementById('modalDesc').value = ''; document.getElementById('modalDesc').value = '';
document.getElementById('modalAmount').value = ''; document.getElementById('modalAmount').value = '';
document.getElementById('modalLink').value = ''; document.getElementById('modalLink').value = '';
populateSelects(adultsList); populateSelects(adultsList);
if (adultSelect && payerSelect) { if (adultSelect && payerSelect) {
if (adultsList.indexOf('Laia') !== -1) { if (adultsList.indexOf('Laia') !== -1) {
adultSelect.value = 'Laia'; payerSelect.value = 'Laia'; adultSelect.value = 'Laia'; payerSelect.value = 'Laia';
@@ -469,7 +523,7 @@ document.body.addEventListener('click', async function(e) {
const titleStr = window.I18N && window.I18N['gift_modal_title_add'] ? window.I18N['gift_modal_title_add'] : 'Ajouter pour %s'; const titleStr = window.I18N && window.I18N['gift_modal_title_add'] ? window.I18N['gift_modal_title_add'] : 'Ajouter pour %s';
document.getElementById('modalTitle').textContent = titleStr.replace('%s', childName); document.getElementById('modalTitle').textContent = titleStr.replace('%s', childName);
if (modal) { modal.classList.add('open'); document.body.classList.add('no-scroll'); } if (modal) { modal.classList.add('open'); document.body.classList.add('no-scroll'); }
return; return;
} }
@@ -478,11 +532,11 @@ document.body.addEventListener('click', async function(e) {
if (btnEdit) { if (btnEdit) {
const data = JSON.parse(btnEdit.dataset.gift || '{}'); const data = JSON.parse(btnEdit.dataset.gift || '{}');
document.getElementById('modalAction').value = 'update'; document.getElementById('modalAction').value = 'update';
document.getElementById('modalGiftId').value = data.id; document.getElementById('modalGiftId').value = data.id;
document.getElementById('modalChild').value = data.child_name; document.getElementById('modalChild').value = data.child_name;
document.getElementById('modalOccasion').value = data.occasion; document.getElementById('modalOccasion').value = data.occasion;
document.getElementById('modalDesc').value = data.gift_description; document.getElementById('modalDesc').value = data.gift_description;
document.getElementById('modalAmount').value = data.amount; document.getElementById('modalAmount').value = data.amount;
document.getElementById('modalLink').value = data.product_link; document.getElementById('modalLink').value = data.product_link;
@@ -491,16 +545,16 @@ document.body.addEventListener('click', async function(e) {
let adultExists = false, payerExists = false; let adultExists = false, payerExists = false;
for(let i=0; i<adultSelect.options.length; i++) { if(adultSelect.options[i].value === data.adult_name) adultExists = true; } for(let i=0; i<adultSelect.options.length; i++) { if(adultSelect.options[i].value === data.adult_name) adultExists = true; }
for(let i=0; i<payerSelect.options.length; i++) { if(payerSelect.options[i].value === payerVal) payerExists = true; } for(let i=0; i<payerSelect.options.length; i++) { if(payerSelect.options[i].value === payerVal) payerExists = true; }
if (!adultExists) adultSelect.appendChild(new Option(data.adult_name, data.adult_name)); if (!adultExists) adultSelect.appendChild(new Option(data.adult_name, data.adult_name));
if (!payerExists) payerSelect.appendChild(new Option(payerVal, payerVal)); if (!payerExists) payerSelect.appendChild(new Option(payerVal, payerVal));
adultSelect.value = data.adult_name; adultSelect.value = data.adult_name;
payerSelect.value = payerVal; payerSelect.value = payerVal;
} }
document.getElementById('modalTitle').textContent = window.I18N && window.I18N['gift_modal_title_edit'] ? window.I18N['gift_modal_title_edit'] : 'Modifier le cadeau'; document.getElementById('modalTitle').textContent = window.I18N && window.I18N['gift_modal_title_edit'] ? window.I18N['gift_modal_title_edit'] : 'Modifier le cadeau';
if (modal) { modal.classList.add('open'); document.body.classList.add('no-scroll'); } if (modal) { modal.classList.add('open'); document.body.classList.add('no-scroll'); }
return; return;
} }
@@ -510,11 +564,11 @@ document.body.addEventListener('click', async function(e) {
const giftId = btnDel.dataset.id; const giftId = btnDel.dataset.id;
const msg = window.I18N && window.I18N['gift_confirm_delete'] ? window.I18N['gift_confirm_delete'] : 'Supprimer ce cadeau ?'; const msg = window.I18N && window.I18N['gift_confirm_delete'] ? window.I18N['gift_confirm_delete'] : 'Supprimer ce cadeau ?';
if (!confirm(msg)) return; if (!confirm(msg)) return;
const fd = new FormData(); const fd = new FormData();
fd.append('action', 'delete'); fd.append('action', 'delete');
fd.append('gift_id', giftId); fd.append('gift_id', giftId);
try { try {
await fetch('/modules/gift-list/save-gift.php', { method: 'POST', body: fd }); await fetch('/modules/gift-list/save-gift.php', { method: 'POST', body: fd });
window.location.reload(); window.location.reload();
@@ -531,7 +585,7 @@ if (giftForm) {
e.preventDefault(); e.preventDefault();
const btn = e.target.querySelector('button[type="submit"]'); const btn = e.target.querySelector('button[type="submit"]');
const oldText = btn.innerText; const oldText = btn.innerText;
btn.innerText = '...'; btn.innerText = '...';
btn.disabled = true; btn.disabled = true;
try { try {
@@ -539,164 +593,382 @@ if (giftForm) {
window.location.reload(); window.location.reload();
} catch(err) { } catch(err) {
console.error(err); console.error(err);
btn.innerText = oldText; btn.innerText = oldText;
btn.disabled = false; btn.disabled = false;
} }
}); });
} }
// ========================================== // ==========================================
// GESTION DES PARAMÈTRES (MODALE FÊTES ⚙️) // 3. GESTION DES PARAMÈTRES (MODALE FÊTES & PROCHES)
// ========================================== // ==========================================
// 1. Fonctions d'ouverture / fermeture propres
function closeGiftSettings() { function closeGiftSettings() {
document.getElementById('modal-gift-settings').classList.remove('show'); document.getElementById('modal-gift-settings').classList.remove('open');
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
} }
const btnOpenSettings = document.getElementById('btn-open-gift-settings'); document.getElementById('btn-open-gift-settings')?.addEventListener('click', async (e) => {
if (btnOpenSettings) { e.preventDefault();
btnOpenSettings.addEventListener('click', async (e) => { document.getElementById('modal-gift-settings').classList.add('open');
e.preventDefault(); document.body.classList.add('no-scroll');
const modalSettings = document.getElementById('modal-gift-settings'); await loadSettingsData();
if (modalSettings) { });
modalSettings.classList.add('show');
document.body.classList.add('no-scroll');
await loadGiftOccasions();
}
});
}
document.getElementById('btn-close-gift-settings')?.addEventListener('click', closeGiftSettings); document.querySelectorAll('.btn-close-settings').forEach(btn => btn.addEventListener('click', closeGiftSettings));
document.getElementById('btn-cancel-settings')?.addEventListener('click', closeGiftSettings);
// Fermeture au clic sur le fond (backdrop) function switchGLTab(tabId) {
const modalGiftSettings = document.getElementById('modal-gift-settings'); // Gestion du contenu (Afficher/Cacher)
if (modalGiftSettings) { document.getElementById('gl-pane-occasions').style.display = (tabId === 'occasions') ? 'block' : 'none';
modalGiftSettings.addEventListener('click', (e) => { document.getElementById('gl-pane-proches').style.display = (tabId === 'proches') ? 'block' : 'none';
if (e.target === modalGiftSettings) {
closeGiftSettings();
}
});
}
// 2. Chargement dynamique de la liste // Gestion du style des boutons d'onglet
async function loadGiftOccasions() { const btnOcc = document.getElementById('tab-btn-occasions');
const container = document.getElementById('occasions-list-container'); const btnProche = document.getElementById('tab-btn-proches');
if (!container) return;
const txtLoading = window.I18N && window.I18N['loading'] ? window.I18N['loading'] : '⏳';
const txtEmpty = window.I18N && window.I18N['gift_settings_empty'] ? window.I18N['gift_settings_empty'] : 'Vide';
const txtActive = window.I18N && window.I18N['gift_settings_active'] ? window.I18N['gift_settings_active'] : 'Actif';
container.innerHTML = `<div class="gift-loader">${txtLoading}</div>`; if (tabId === 'occasions') {
btnOcc.style.background = 'var(--bg-panel)';
try { btnOcc.style.color = 'var(--primary)';
const res = await fetch('/modules/gift-list/api-settings.php?action=get_occasions'); btnOcc.style.boxShadow = 'var(--shadow-sm)';
const text = await res.text();
const data = JSON.parse(text); btnProche.style.background = 'transparent';
btnProche.style.color = 'var(--text-muted)';
if (!data.ok) throw new Error(data.error); btnProche.style.boxShadow = 'none';
} else {
if (data.data.length === 0) { btnProche.style.background = 'var(--bg-panel)';
container.innerHTML = `<em>${txtEmpty}</em>`; btnProche.style.color = 'var(--primary)';
return; btnProche.style.boxShadow = 'var(--shadow-sm)';
}
btnOcc.style.background = 'transparent';
let html = '<div class="gift-occasions-grid">'; btnOcc.style.color = 'var(--text-muted)';
data.data.forEach(occ => { btnOcc.style.boxShadow = 'none';
const isChecked = occ.is_active == 1 ? 'checked' : '';
const dateBadge = occ.month_date ? `<span class="gift-date-badge">${occ.month_date}</span>` : '';
html += `
<div class="pf-card gift-occasion-card">
<div>
<strong>${occ.name}</strong> ${dateBadge}
</div>
<label class="gift-occasion-toggle">
<input type="checkbox" class="occ-toggle-cb" data-id="${occ.id}" ${isChecked}>
<small>${txtActive}</small>
</label>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
} catch (e) {
container.innerHTML = `<div class="gift-error-msg">Erreur : ${e.message}</div>`;
} }
} }
// 3. Soumission du formulaire d'AJOUT let hasSettingsChanged = false;
const formAddOccasion = document.getElementById('form-add-occasion');
if (formAddOccasion) {
formAddOccasion.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = formAddOccasion.querySelector('button[type="submit"]');
const oldText = btn.innerText;
btn.innerText = '⏳';
btn.disabled = true;
const formData = new FormData(formAddOccasion); async function loadSettingsData() {
formData.append('action', 'add_occasion'); try {
const res = await pachaFetch('/modules/gift-list/api-settings.php?action=get_all');
if (!res.success) throw new Error(res.error);
renderOccasions(res.data.occasions);
renderProches(res.data.proches);
} catch (e) {
document.getElementById('occasions-list').innerHTML = `<div class="pf-alert pf-alert--error">Erreur : ${e.message}</div>`;
document.getElementById('proches-list').innerHTML = `<div class="pf-alert pf-alert--error">Erreur : ${e.message}</div>`;
}
}
try { function renderOccasions(occasions) {
const res = await fetch('/modules/gift-list/api-settings.php', { method: 'POST', body: formData }); const container = document.getElementById('occasions-list');
const text = await res.text(); if (!occasions.length) { container.innerHTML = `<em class="gift-empty-state">Vide</em>`; return; }
const data = JSON.parse(text);
if (!data.ok) throw new Error(data.error || 'Erreur'); let html = '';
occasions.forEach(occ => {
const isChecked = occ.is_active == 1 ? 'checked' : '';
// ⚠️ Remarque l'ajout de id="row-occ-${occ.id}" ici :
html += `
<div class="gl-list-item" id="row-occ-${occ.id}">
<div>
<span style="font-size:1.2rem; margin-right:8px;">${occ.icon || '🎁'}</span>
<strong>${occ.name}</strong>
<small style="color:var(--text-muted); margin-left:8px;">${occ.month_date || ''}</small>
</div>
<div class="pf-flex-gap-8 align-center">
<label style="display:flex; align-items:center; cursor:pointer;">
<input type="checkbox" class="pf-checkbox-lg" ${isChecked} onchange="toggleOccasion(${occ.id}, this.checked)">
</label>
<button type="button" class="btn-icon-action edit" onclick="transformRowToEditOccasion('row-occ-${occ.id}', ${occ.id}, '${occ.name.replace(/'/g, "\\'")}', '${occ.icon}', '${occ.month_date || ''}')" title="${tr('btn_edit')}">✏️</button>
<button type="button" class="btn-icon-action delete" onclick="deleteOccasion(${occ.id})" title="${tr('btn_delete')}">🗑️</button>
</div>
</div>
`;
});
container.innerHTML = html;
}
function renderProches(proches) {
const container = document.getElementById('proches-list');
if (!proches.length) { container.innerHTML = `<em class="gift-empty-state">Aucun proche configuré.</em>`; return; }
let html = '';
proches.forEach(p => {
const roleLabel = p.role === 'proche_adulte' ? (tr('gift_role_proche_adulte') || 'Adulte') : (tr('gift_role_proche_enfant') || 'Enfant');
html += `
<div class="gl-list-item" id="row-proche-${p.id}">
<div><strong>${p.name}</strong> <small style="color:var(--text-muted);">— ${roleLabel}</small></div>
<div class="pf-flex-gap-8 align-center">
<button type="button" class="btn-icon-action edit" onclick="transformRowToEditProche('row-proche-${p.id}', ${p.id}, '${p.name.replace(/'/g, "\\'")}', '${p.role}')" title="${tr('btn_edit')}">✏️</button>
<button type="button" class="btn-icon-action delete" onclick="deleteProche(${p.id})" title="${tr('btn_delete')}">🗑️</button>
</div>
</div>
`;
});
container.innerHTML = html;
}
// ==========================================
// CRUD : OCCASIONS
// ==========================================
// 1. Ajouter une occasion
document.getElementById('form-add-occasion')?.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = e.target.querySelector('button[type="submit"]');
btn.disabled = true;
hasSettingsChanged = true;
const fd = new FormData(e.target);
fd.append('action', 'save_occasion');
try {
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
// On vide le formulaire
e.target.reset();
// On remet l'émoji par défaut visuellement
document.querySelectorAll('#form-add-occasion .emoji-option').forEach(el => el.classList.remove('selected'));
document.querySelector('#form-add-occasion .emoji-option').classList.add('selected');
document.getElementById('add-occ-icon').value = '🎁';
showToast("Fête ajoutée avec succès !", "success");
loadSettingsData(); // Recharge la liste
} catch (err) {
showToast("Erreur lors de l'ajout", "error");
console.error(err);
} finally {
btn.disabled = false;
}
});
// 2. Sauvegarder la modification (In-place)
async function saveInlineOccasion(id) {
hasSettingsChanged = true;
const fd = new FormData();
fd.append('action', 'save_occasion');
fd.append('id', id);
fd.append('icon', document.getElementById(`inline-occ-icon-${id}`).value);
fd.append('name', document.getElementById(`inline-occ-name-${id}`).value);
fd.append('month_date', document.getElementById(`inline-occ-date-${id}`).value);
try {
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
showToast("Fête modifiée avec succès !", "success");
loadSettingsData();
} catch (err) {
showToast("Erreur lors de la modification", "error");
console.error(err);
}
}
// 3. Supprimer une occasion
async function deleteOccasion(id) {
if (!confirm(tr('confirm_delete') || 'Supprimer définitivement ?')) return;
hasSettingsChanged = true;
const fd = new FormData();
fd.append('action', 'delete_occasion');
fd.append('id', id);
try {
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
showToast("Fête supprimée.", "success");
loadSettingsData();
} catch (err) {
showToast("Erreur lors de la suppression", "error");
}
}
// 4. Activer / Désactiver (Checkbox)
async function toggleOccasion(id, state) {
hasSettingsChanged = true;
const fd = new FormData();
fd.append('action', 'toggle_occasion');
fd.append('id', id);
fd.append('state', state ? 1 : 0);
try {
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
showToast(state ? "Fête activée" : "Fête masquée", "success");
} catch (err) {
showToast("Erreur de mise à jour", "error");
}
}
// ==========================================
// CRUD : PROCHES
// ==========================================
// 1. Ajouter un proche
document.getElementById('form-add-proche')?.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = e.target.querySelector('button[type="submit"]');
btn.disabled = true;
hasSettingsChanged = true;
const fd = new FormData(e.target);
fd.append('action', 'save_proche');
try {
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
e.target.reset();
showToast("Proche ajouté avec succès !", "success");
loadSettingsData();
} catch (err) {
showToast("Erreur lors de l'ajout", "error");
} finally {
btn.disabled = false;
}
});
// 2. Sauvegarder la modification (In-place)
async function saveInlineProche(id) {
hasSettingsChanged = true;
const fd = new FormData();
fd.append('action', 'save_proche');
fd.append('id', id);
fd.append('name', document.getElementById(`inline-proche-name-${id}`).value);
fd.append('role', document.getElementById(`inline-proche-role-${id}`).value);
try {
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
showToast("Proche mis à jour !", "success");
loadSettingsData();
} catch (err) {
showToast("Erreur lors de la mise à jour", "error");
}
}
// 3. Supprimer un proche
async function deleteProche(id) {
if (!confirm(tr('confirm_delete') || 'Supprimer ce proche ?')) return;
hasSettingsChanged = true;
const fd = new FormData();
fd.append('action', 'delete_proche');
fd.append('id', id);
try {
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
showToast("Proche retiré de la liste.", "success");
loadSettingsData();
} catch (err) {
showToast("Erreur lors de la suppression", "error");
}
}
// Rechargement intelligent lors de la fermeture si modifications
document.getElementById('modal-gift-settings').addEventListener('click', (e) => {
if (e.target.id === 'modal-gift-settings' || e.target.classList.contains('btn-close-settings')) {
if (hasSettingsChanged) {
showToast(tr('gift_settings_saved') || 'Paramètres mis à jour !', 'success');
setTimeout(() => window.location.reload(), 1000);
} else {
closeGiftSettings();
}
}
});
// Fonction pour gérer la sélection dans la grille d'émojis (formulaire d'ajout)
function selectEmoji(element, targetInputId, emoji) {
document.getElementById(targetInputId).value = emoji;
// Mise à jour visuelle de la sélection
const siblings = element.parentNode.querySelectorAll('.emoji-option');
siblings.forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
}
// ==========================================
// LOGIQUE D'ÉDITION IN-PLACE (OCCASIONS)
// ==========================================
// On crée une mémoire pour stocker le HTML d'origine de chaque ligne
const rowStates = {};
function transformRowToEditOccasion(rowId, id, name, icon, date) {
const row = document.getElementById(rowId);
// On sauvegarde l'état proprement en JS (fini les bugs d'affichage !)
rowStates[rowId] = row.innerHTML;
row.innerHTML = `
<div style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center; width: 100%; background: var(--bg-subtle); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);">
<select id="inline-occ-icon-${id}" class="pf-input" style="width: 60px; padding: 6px; margin: 0;">
<option value="🎁" ${icon==='🎁'?'selected':''}>🎁</option>
<option value="🎄" ${icon==='🎄'?'selected':''}>🎄</option>
<option value="🎂" ${icon==='🎂'?'selected':''}>🎂</option>
<option value="🎉" ${icon==='🎉'?'selected':''}>🎉</option>
<option value="🎓" ${icon==='🎓'?'selected':''}>🎓</option>
</select>
<input type="text" id="inline-occ-name-${id}" value="${name}" class="pf-input" style="flex: 1; min-width: 100px; padding: 6px; margin: 0;">
<input type="text" id="inline-occ-date-${id}" value="${date}" class="pf-input" style="width: 70px; padding: 6px; margin: 0;" placeholder="MM-JJ">
formAddOccasion.reset(); <div style="display: flex; gap: 4px; margin-left: auto;">
await loadGiftOccasions(); // Rafraîchit juste la liste dans la modale ! <button type="button" onclick="saveInlineOccasion(${id})" class="pf-btn pf-btn-primary" style="padding: 6px 10px;">💾</button>
<button type="button" onclick="cancelInlineEdit('${rowId}')" class="pf-btn btn-secondary" style="padding: 6px 10px;">❌</button>
} catch (err) { </div>
alert("Erreur: " + err.message); </div>
} finally { `;
btn.innerText = oldText;
btn.disabled = false;
}
});
} }
// 4. Soumission GLOBALE (Bouton Enregistrer) pour les Checkboxes async function saveInlineOccasion(id) {
const formSaveToggles = document.getElementById('form-save-toggles'); hasSettingsChanged = true;
if (formSaveToggles) { const fd = new FormData();
formSaveToggles.addEventListener('submit', async (e) => { fd.append('action', 'save_occasion');
e.preventDefault(); fd.append('id', id);
const btn = formSaveToggles.querySelector('button[type="submit"]'); fd.append('icon', document.getElementById(`inline-occ-icon-${id}`).value);
const oldText = btn.innerText; fd.append('name', document.getElementById(`inline-occ-name-${id}`).value);
btn.innerText = '⏳'; fd.append('month_date', document.getElementById(`inline-occ-date-${id}`).value);
btn.disabled = true;
// On récolte l'état de toutes les checkboxes // On met à jour en BDD puis on recharge la liste !
const states = []; await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
formSaveToggles.querySelectorAll('.occ-toggle-cb').forEach(cb => { loadSettingsData();
states.push({ id: cb.dataset.id, state: cb.checked ? 1 : 0 });
});
const formData = new FormData();
formData.append('action', 'save_toggles');
formData.append('states', JSON.stringify(states));
try {
const res = await fetch('/modules/gift-list/api-settings.php', { method: 'POST', body: formData });
const text = await res.text();
const data = JSON.parse(text);
if (!data.ok) throw new Error(data.error);
// Succès : on rafraîchit la page pour afficher/masquer les onglets
window.location.reload();
} catch (err) {
alert("Erreur: " + err.message);
btn.innerText = oldText;
btn.disabled = false;
}
});
} }
// ==========================================
// LOGIQUE D'ÉDITION IN-PLACE (PROCHES)
// ==========================================
function transformRowToEditProche(rowId, id, name, role) {
const row = document.getElementById(rowId);
rowStates[rowId] = row.innerHTML;
const roleAdulte = window.I18N ? window.I18N['gift_role_proche_adulte'] : 'Adulte';
const roleEnfant = window.I18N ? window.I18N['gift_role_proche_enfant'] : 'Enfant';
row.innerHTML = `
<div style="display: flex; flex-wrap: wrap; gap: 8px; align-items: center; width: 100%; background: var(--bg-subtle); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);">
<input type="text" id="inline-proche-name-${id}" value="${name}" class="pf-input" style="flex: 1; min-width: 100px; padding: 6px; margin: 0;">
<select id="inline-proche-role-${id}" class="pf-input" style="width: 130px; padding: 6px; margin: 0;">
<option value="proche_adulte" ${role==='proche_adulte'?'selected':''}>${roleAdulte}</option>
<option value="proche_enfant" ${role==='proche_enfant'?'selected':''}>${roleEnfant}</option>
</select>
<div style="display: flex; gap: 4px; margin-left: auto;">
<button type="button" onclick="saveInlineProche(${id})" class="pf-btn pf-btn-primary" style="padding: 6px 10px;">💾</button>
<button type="button" onclick="cancelInlineEdit('${rowId}')" class="pf-btn btn-secondary" style="padding: 6px 10px;">❌</button>
</div>
</div>
`;
}
async function saveInlineProche(id) {
hasSettingsChanged = true;
const fd = new FormData();
fd.append('action', 'save_proche');
fd.append('id', id);
fd.append('name', document.getElementById(`inline-proche-name-${id}`).value);
fd.append('role', document.getElementById(`inline-proche-role-${id}`).value);
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
loadSettingsData();
}
// Fonction pour restaurer la ligne (sans casser le HTML)
function cancelInlineEdit(rowId) {
document.getElementById(rowId).innerHTML = rowStates[rowId];
}
</script> </script>
<?php require __DIR__ . '/footer.php'; ?> <?php require __DIR__ . '/footer.php'; ?>
+87 -1
View File
@@ -280,6 +280,56 @@ return [
'btn_save_rights' => 'Guardar', 'btn_save_rights' => 'Guardar',
// ==========================================
// MODAL DE CONFIGURACIÓ DEL CALENDARI
// ==========================================
'fc_modal_settings_title' => 'Configuració del calendari',
'fc_tab_foyer' => 'Llar',
'fc_tab_members' => 'Membres i Drets',
// Llar: Zona escolar
'fc_zone_label' => 'Zona escolar',
'fc_zone_a' => 'Zona A',
'fc_zone_b' => 'Zona B',
'fc_zone_c' => 'Zona C',
'fc_zone_other' => 'Altra / Fora de França',
'fc_zone_desc' => 'Permet mostrar correctament les vacances escolars.',
// Llar: Opcions de cura (Modes de garde)
'fc_care_modes_label' => 'Modalitats de cura',
'fc_add_mode_placeholder' => 'Ex: Mainadera, Llar d\'infants...',
'fc_care_modes_desc' => 'Afegeix les diferents modalitats de cura per als teus fills.',
'fc_empty_kids_msg' => 'Actualment no hi ha cap infant configurat a la teva família.',
'fc_link_settings' => 'Anar a la configuració general',
// Llar: Catàleg de permisos (Congés)
'fc_leave_catalog_title' => 'Catàleg de permisos',
'fc_leave_catalog_desc' => 'Defineix els tipus de permisos possibles per a tota la família.',
'fc_add_leave_type' => 'Afegir un tipus de permís',
'fc_col_code' => 'Codi',
'fc_code_irreversible' => 'Irreversible',
'fc_col_label' => 'Etiqueta',
'fc_leave_label_ph' => 'Ex: Vacances, Assumptes propis...',
'fc_quota_default' => 'Quota per defecte',
'fc_reset_month' => 'Mes de renovació',
// Membres
'fc_select_member' => 'Selecciona un membre:',
// Mesos
'month_jan' => 'Gener',
'month_feb' => 'Febrer',
'month_mar' => 'Març',
'month_apr' => 'Abril',
'month_may' => 'Maig',
'month_jun' => 'Juny',
'month_jul' => 'Juliol',
'month_aug' => 'Agost',
'month_sep' => 'Setembre',
'month_oct' => 'Octubre',
'month_nov' => 'Novembre',
'month_dec' => 'Desembre',
'leg_presence' => 'Presència Pep', 'leg_presence' => 'Presència Pep',
'leg_school_holidays' => 'Vacances Escolars', 'leg_school_holidays' => 'Vacances Escolars',
'leg_public_holiday' => 'Festiu', 'leg_public_holiday' => 'Festiu',
@@ -592,8 +642,23 @@ return [
'bud_bar_max_cap' => "Capacitat Màx.", 'bud_bar_max_cap' => "Capacitat Màx.",
'bud_edit_title' => 'Modificar la transacció', 'bud_edit_title' => 'Modificar la transacció',
'bud_to_define_js' => 'per definir', 'bud_to_define_js' => 'per definir',
'bud_select_beneficiary' => 'Triar un ingrés esperat', // 👈 À ajouter ici 'bud_select_beneficiary' => 'Triar un ingrés esperat', /
'bud_bar_actual' => 'Actual', 'bud_bar_actual' => 'Actual',
// ONBOARDING BUDGET
'bs_onb_title' => 'Benvinguts! Configurem el teu pressupost:',
'bs_onb_desc' => 'Completa aquests passos perquè l\'aplicació pugui calcular el teu pressupost disponible:',
'bs_onb_cat_link' => 'Categories',
'bs_onb_cat_desc' => 'Crea almenys una categoria de despeses.',
'bs_onb_acc_link' => 'Comptes Bancaris',
'bs_onb_acc_desc' => 'Afegeix almenys un compte corrent o d\'estalvi.',
'bs_onb_sal_link' => 'Sous',
'bs_onb_sal_desc' => 'Introdueix els ingressos de l\'any en curs.',
'bs_onb_items_link' => 'Línies de Pressupost',
'bs_onb_items_desc' => 'Ves a la pestanya Resum per llistar les teves despeses fixes (Lloguer, Assegurances...).',
'bs_rule_search_ph' => '🔍 Cercar una paraula clau (ex: AUCHAN)...',
'error_loading_settings' => 'Error en carregar la configuració',
'settings' => 'Configuració',
// --- MODULE BUDGET : IMPORT CSV --- // --- MODULE BUDGET : IMPORT CSV ---
'bud_import_csv' => "Importar CSV", 'bud_import_csv' => "Importar CSV",
@@ -652,6 +717,9 @@ return [
'bud_sav_modal_title_add' => 'Introduir un mes', 'bud_sav_modal_title_add' => 'Introduir un mes',
'bud_sav_ph_name' => 'Nom (ex: Vacances)', 'bud_sav_ph_name' => 'Nom (ex: Vacances)',
'bud_prev_label_name' => 'Nom', 'bud_prev_label_name' => 'Nom',
'bud_prev_monthly_target' => 'Import objectiu / Mensualitat (€)',
'bud_prev_transfer_dest' => 'Destinació de la transferència',
'bud_prev_none' => 'Cap destinació (Despesa estàndard)',
// --- BUDGET : EPARGNE --- // --- BUDGET : EPARGNE ---
'bud_sav_add_month' => 'Introduir un mes', 'bud_sav_add_month' => 'Introduir un mes',
@@ -750,6 +818,8 @@ return [
'bud_prev_err_no_history' => 'Impossible: no hi ha historial disponible per copiar.', 'bud_prev_err_no_history' => 'Impossible: no hi ha historial disponible per copiar.',
'bud_prev_confirm_copy' => "Vols copiar les dades de %s a %t?\n\n⚠️ Això sobreescriurà tots els valors ja presents per a %t.", 'bud_prev_confirm_copy' => "Vols copiar les dades de %s a %t?\n\n⚠️ Això sobreescriurà tots els valors ja presents per a %t.",
'bud_prev_confirm_transfers' => "Confirmes que %p ha fet totes les transferències per a %m?\n\nAixò actualitzarà l'Estalvi automàticament.", 'bud_prev_confirm_transfers' => "Confirmes que %p ha fet totes les transferències per a %m?\n\nAixò actualitzarà l'Estalvi automàticament.",
'bud_prev_target_ph' => 'Ex: 150',
'bud_prev_transfer_to' => 'Cap a',
// --- MODULE BUDGET : AVANCES & TRICOUNT --- // --- MODULE BUDGET : AVANCES & TRICOUNT ---
'bud_adv_title' => 'Avançaments i Tricount Familiar', 'bud_adv_title' => 'Avançaments i Tricount Familiar',
@@ -820,6 +890,22 @@ return [
'btn_save' => 'Desar', 'btn_save' => 'Desar',
'btn_add' => 'Afegir', 'btn_add' => 'Afegir',
'loading' => 'Carregant...', // 'loading' => 'Carregant...', //
// --- MODULE CADEAUX : SETTINGS ---
'gift_settings' => 'Configuració',
'gift_tab_occasions' => 'Festes i Ocasions',
'gift_tab_proches' => 'Familiars (Fora de la llar)',
'gift_settings_icon' => 'Icona',
'gift_settings_date' => 'Data (MM-DD)',
'gift_settings_add_occ' => 'Afegeix una festa',
'gift_settings_add_proche'=> 'Afegeix un familiar',
'gift_role_proche_adulte'=> 'Adult (Oncle, amic...)',
'gift_role_proche_enfant'=> 'Nen (Nebot, neboda...)',
'gift_ph_proche_name' => 'Nom',
'gift_role_label' => 'Rol',
'gift_settings_saved' => 'Configuració desada!',
'gift_settings_edit_occ' => 'Modificar la festa',
'gift_settings_edit_proche' => 'Modificar el familiar',
// ========================================== // ==========================================
// GARAGE MANAGER // GARAGE MANAGER
+86
View File
@@ -281,6 +281,56 @@ return [
'btn_save_rights' => 'Save', 'btn_save_rights' => 'Save',
// ==========================================
// CALENDAR SETTINGS MODAL
// ==========================================
'fc_modal_settings_title' => 'Calendar Settings',
'fc_tab_foyer' => 'Household',
'fc_tab_members' => 'Members & Rights',
// Household: School Zone
'fc_zone_label' => 'School zone',
'fc_zone_a' => 'Zone A',
'fc_zone_b' => 'Zone B',
'fc_zone_c' => 'Zone C',
'fc_zone_other' => 'Other / Outside France',
'fc_zone_desc' => 'Used to accurately display school holidays.',
// Household: Care Modes
'fc_care_modes_label' => 'Childcare options',
'fc_add_mode_placeholder' => 'Ex: Nanny, Daycare...',
'fc_care_modes_desc' => 'Add the different childcare options for your children.',
'fc_empty_kids_msg' => 'No children are configured in your household yet.',
'fc_link_settings' => 'Go to general settings',
// Household: Leave Catalog
'fc_leave_catalog_title' => 'Leave Catalog',
'fc_leave_catalog_desc' => 'Define the possible leave types for the whole family.',
'fc_add_leave_type' => 'Add a leave type',
'fc_col_code' => 'Code',
'fc_code_irreversible' => 'Irreversible',
'fc_col_label' => 'Label',
'fc_leave_label_ph' => 'Ex: PTO, Sick leave...',
'fc_quota_default' => 'Default quota',
'fc_reset_month' => 'Reset month',
// Members
'fc_select_member' => 'Select a member:',
// Months
'month_jan' => 'January',
'month_feb' => 'February',
'month_mar' => 'March',
'month_apr' => 'April',
'month_may' => 'May',
'month_jun' => 'June',
'month_jul' => 'July',
'month_aug' => 'August',
'month_sep' => 'September',
'month_oct' => 'October',
'month_nov' => 'November',
'month_dec' => 'December',
'leg_presence' => 'Pep presence', 'leg_presence' => 'Pep presence',
'leg_school_holidays' => 'School holidays', 'leg_school_holidays' => 'School holidays',
'leg_public_holiday' => 'Public holiday', 'leg_public_holiday' => 'Public holiday',
@@ -532,6 +582,21 @@ return [
'bs_csv_empty_col' => 'Empty', 'bs_csv_empty_col' => 'Empty',
'bs_csv_no_data' => 'No transaction row found in this file.', 'bs_csv_no_data' => 'No transaction row found in this file.',
// ONBOARDING BUDGET
'bs_onb_title' => 'Welcome! Let\'s set up your budget:',
'bs_onb_desc' => 'Complete these steps so the app can calculate your remaining budget:',
'bs_onb_cat_link' => 'Categories',
'bs_onb_cat_desc' => 'Create at least one expense category.',
'bs_onb_acc_link' => 'Bank Accounts',
'bs_onb_acc_desc' => 'Add at least one checking or savings account.',
'bs_onb_sal_link' => 'Salaries',
'bs_onb_sal_desc' => 'Enter your income for the current year.',
'bs_onb_items_link' => 'Budget Items',
'bs_onb_items_desc' => 'Go to the Summary tab to list your fixed expenses (Rent, Insurance...).',
'bs_rule_search_ph' => '🔍 Search for a keyword (e.g. WALMART)...',
'error_loading_settings' => 'Error loading settings',
'settings' => 'Settings',
// --- BUDGET: TRACKING --- // --- BUDGET: TRACKING ---
'bud_rem_school' => 'School (estimated remaining)', 'bud_rem_school' => 'School (estimated remaining)',
'bud_rem_fmcg' => 'Groceries (estimated remaining)', 'bud_rem_fmcg' => 'Groceries (estimated remaining)',
@@ -630,6 +695,9 @@ return [
'bud_reg_none' => 'None', 'bud_reg_none' => 'None',
'bud_recap_confirm_delete' => 'Are you sure you want to delete this item?', 'bud_recap_confirm_delete' => 'Are you sure you want to delete this item?',
'bud_err_delete' => 'Error while deleting.', 'bud_err_delete' => 'Error while deleting.',
'bud_prev_monthly_target' => 'Target amount / Monthly payment (€)',
'bud_prev_transfer_dest' => 'Transfer destination',
'bud_prev_none' => 'No destination (Standard expense)',
// --- BUDGET: SAVINGS --- // --- BUDGET: SAVINGS ---
'bud_sav_add_month' => 'Enter a month', 'bud_sav_add_month' => 'Enter a month',
@@ -753,6 +821,8 @@ return [
'delete' => 'Delete', 'delete' => 'Delete',
'btn_cancel' => 'Cancel', 'btn_cancel' => 'Cancel',
'btn_save' => 'Save', 'btn_save' => 'Save',
'bud_prev_target_ph' => 'E.g., 150',
'bud_prev_transfer_to' => 'To',
// ========================================== // ==========================================
// MODULE: GIFTS (gift-list) // MODULE: GIFTS (gift-list)
@@ -806,6 +876,22 @@ return [
'btn_add' => 'Add', 'btn_add' => 'Add',
'loading' => 'Loading...', 'loading' => 'Loading...',
// --- MODULE CADEAUX : SETTINGS ---
'gift_settings' => 'Settings',
'gift_tab_occasions' => 'Occasions & Events',
'gift_tab_proches' => 'Relatives (Outside household)',
'gift_settings_icon' => 'Icon',
'gift_settings_date' => 'Date (MM-DD)',
'gift_settings_add_occ' => 'Add an occasion',
'gift_settings_add_proche'=> 'Add a relative',
'gift_role_proche_adulte'=> 'Adult (Uncle, friend...)',
'gift_role_proche_enfant'=> 'Child (Nephew, niece...)',
'gift_ph_proche_name' => 'First name',
'gift_role_label' => 'Role',
'gift_settings_saved' => 'Settings saved!',
'gift_settings_edit_occ' => 'Edit occasion',
'gift_settings_edit_proche' => 'Edit relative',
// ========================================== // ==========================================
// GARAGE MANAGER // GARAGE MANAGER
// ========================================== // ==========================================
+88 -1
View File
@@ -282,7 +282,56 @@ return [
'fc_table_renewal' => 'Renouv.', 'fc_table_renewal' => 'Renouv.',
'btn_save_rights' => 'Enregistrer', 'btn_save_rights' => 'Enregistrer',
// ==========================================
// MODALE PARAMÈTRES CALENDRIER
// ==========================================
'fc_modal_settings_title' => 'Paramètres du calendrier',
'fc_tab_foyer' => 'Foyer',
'fc_tab_members' => 'Membres & Droits',
// Foyer : Zone scolaire
'fc_zone_label' => 'Zone scolaire',
'fc_zone_a' => 'Zone A',
'fc_zone_b' => 'Zone B',
'fc_zone_c' => 'Zone C',
'fc_zone_other' => 'Autre / Hors France',
'fc_zone_desc' => 'Permet d\'afficher correctement les vacances scolaires.',
// Foyer : Modes de garde
'fc_care_modes_label' => 'Modes de garde',
'fc_add_mode_placeholder' => 'Ex: Nounou, Crèche...',
'fc_care_modes_desc' => 'Ajoutez les différents modes de garde pour vos enfants.',
'fc_empty_kids_msg' => 'Aucun enfant n\'est configuré dans votre famille pour le moment.',
'fc_link_settings' => 'Aller aux paramètres généraux',
// Foyer : Catalogue des congés
'fc_leave_catalog_title' => 'Catalogue des congés',
'fc_leave_catalog_desc' => 'Définissez les types de congés possibles pour toute la famille.',
'fc_add_leave_type' => 'Ajouter un type de congé',
'fc_col_code' => 'Code',
'fc_code_irreversible' => 'Irréversible',
'fc_col_label' => 'Label',
'fc_leave_label_ph' => 'Ex: RTT, Récup...',
'fc_quota_default' => 'Quota par défaut',
'fc_reset_month' => 'Mois de renouv.',
// Membres
'fc_select_member' => 'Sélectionner un membre :',
// Mois (S'ils n'existent pas déjà dans ton système)
'month_jan' => 'Janvier',
'month_feb' => 'Février',
'month_mar' => 'Mars',
'month_apr' => 'Avril',
'month_may' => 'Mai',
'month_jun' => 'Juin',
'month_jul' => 'Juillet',
'month_aug' => 'Août',
'month_sep' => 'Septembre',
'month_oct' => 'Octobre',
'month_nov' => 'Novembre',
'month_dec' => 'Décembre',
'leg_presence' => 'Présence Pep', 'leg_presence' => 'Présence Pep',
@@ -607,6 +656,21 @@ return [
'bud_to_define_js' => 'à définir', 'bud_to_define_js' => 'à définir',
'bud_select_beneficiary' => 'Choisir un revenu attendu', 'bud_select_beneficiary' => 'Choisir un revenu attendu',
'bud_bar_actual' => 'Actuel', 'bud_bar_actual' => 'Actuel',
// ONBOARDING BUDGET
'bs_onb_title' => 'Bienvenue ! Configurons votre budget :',
'bs_onb_desc' => 'Complétez ces étapes pour que l\'application puisse calculer votre Reste à Vivre :',
'bs_onb_cat_link' => 'Catégories',
'bs_onb_cat_desc' => 'Créez au moins une catégorie de dépenses.',
'bs_onb_acc_link' => 'Comptes Bancaires',
'bs_onb_acc_desc' => 'Ajoutez au moins un compte courant ou épargne.',
'bs_onb_sal_link' => 'Salaires',
'bs_onb_sal_desc' => 'Renseignez les rentrées d\'argent de l\'année en cours.',
'bs_onb_items_link' => 'Lignes de Budget',
'bs_onb_items_desc' => 'Rendez-vous dans l\'onglet Récapitulatif pour lister vos charges fixes (Loyer, Assurances...).',
'bs_rule_search_ph' => '🔍 Rechercher un mot-clé (ex: AUCHAN)...',
'error_loading_settings' => 'Erreur lors du chargement des paramètres',
'settings' => 'Paramètres',
// --- BUDGET : RECAP --- // --- BUDGET : RECAP ---
'bud_recap_monthly_title' => 'Récapitulatif Mensuel', 'bud_recap_monthly_title' => 'Récapitulatif Mensuel',
@@ -649,6 +713,10 @@ return [
'bud_reg_none' => 'Aucune', 'bud_reg_none' => 'Aucune',
'bud_recap_confirm_delete' => 'Voulez-vous vraiment supprimer cet élément ?', 'bud_recap_confirm_delete' => 'Voulez-vous vraiment supprimer cet élément ?',
'bud_err_delete' => 'Erreur lors de la suppression.', 'bud_err_delete' => 'Erreur lors de la suppression.',
'bud_prev_monthly_target' => 'Montant cible / Mensualité (€)',
'bud_prev_transfer_dest' => 'Destination du virement',
'bud_prev_none' => 'Aucune destination (Charge standard)',
// --- BUDGET : EPARGNE --- // --- BUDGET : EPARGNE ---
'bud_sav_add_month' => 'Saisir un mois', 'bud_sav_add_month' => 'Saisir un mois',
@@ -704,6 +772,7 @@ return [
'bud_sav_selection' => 'Sélection :', 'bud_sav_selection' => 'Sélection :',
'bud_sav_ph_name' => 'Nom du compte', 'bud_sav_ph_name' => 'Nom du compte',
// JS & Erreurs // JS & Erreurs
'bud_sav_confirm_delete_month' => 'Voulez-vous vraiment supprimer toutes les données de %m pour %o ?', 'bud_sav_confirm_delete_month' => 'Voulez-vous vraiment supprimer toutes les données de %m pour %o ?',
'bud_sav_prompt_duplicate' => "Voulez-vous dupliquer les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :", 'bud_sav_prompt_duplicate' => "Voulez-vous dupliquer les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
@@ -766,6 +835,8 @@ return [
'bud_adv_ph_desc' => 'Ex: Courses, Facture Électricité...', 'bud_adv_ph_desc' => 'Ex: Courses, Facture Électricité...',
'bud_adv_confirm_resolve' => 'Confirmer le remboursement complet de cette avance ?', 'bud_adv_confirm_resolve' => 'Confirmer le remboursement complet de cette avance ?',
'bud_adv_confirm_delete' => 'Supprimer définitivement cette avance ?', 'bud_adv_confirm_delete' => 'Supprimer définitivement cette avance ?',
'bud_prev_target_ph' => 'Ex: 150',
'bud_prev_transfer_to' => 'Vers',
// ========================================== // ==========================================
// MODULE : CADEAUX (gift-list) // MODULE : CADEAUX (gift-list)
@@ -818,6 +889,22 @@ return [
'btn_save' => 'Enregistrer', 'btn_save' => 'Enregistrer',
'btn_add' => 'Ajouter', 'btn_add' => 'Ajouter',
'loading' => 'Chargement...', 'loading' => 'Chargement...',
// --- MODULE CADEAUX : SETTINGS ---
'gift_settings' => 'Configuration',
'gift_tab_occasions' => 'Fêtes & Occasions',
'gift_tab_proches' => 'Proches (Hors foyer)',
'gift_settings_icon' => 'Icône',
'gift_settings_date' => 'Date (MM-JJ)',
'gift_settings_add_occ' => 'Ajouter une fête',
'gift_settings_add_proche'=> 'Ajouter un proche',
'gift_role_proche_adulte'=> 'Adulte (Oncle, ami...)',
'gift_role_proche_enfant'=> 'Enfant (Neveu, nièce...)',
'gift_ph_proche_name' => 'Prénom',
'gift_role_label' => 'Rôle',
'gift_settings_saved' => 'Paramètres enregistrés !',
'gift_settings_edit_occ' => 'Modifier la fête',
'gift_settings_edit_proche' => 'Modifier le proche',
// ========================================== // ==========================================
// GARAGE MANAGER // GARAGE MANAGER
+93 -557
View File
@@ -1,644 +1,180 @@
<?php <?php
/** /**
* Script de migration global HouseHub OS (Multi-tenant) * Script de migration global HouseHub OS (Multi-tenant)
* Auto-Sync dynamique basé sur schema_family.sql + Migration des données
* À exécuter via le navigateur : http://localhost:8083/migrate.php * À exécuter via le navigateur : http://localhost:8083/migrate.php
*/ */
require_once __DIR__ . '/includes/meta_db.php'; require_once __DIR__ . '/includes/meta_db.php';
$db_host = getenv('DB_HOST') ?: '127.0.0.1'; $db_host = getenv('DB_HOST') ?: '127.0.0.1';
$db_user = getenv('DB_USER') ?: 'househub'; $db_user = getenv('DB_USER') ?: 'househub';
$db_pass = getenv('DB_PASS') ?: 'househub_dev'; $db_pass = getenv('DB_PASS') ?: 'househub_dev';
echo "<h1>🚀 Début de la migration Multi-Tenant (Auto-Sync)</h1>";
// ---------------------------------------------------------
// 1. LECTURE ET PARSING DU FICHIER SCHEMA_FAMILY.SQL
// ---------------------------------------------------------
$schemaPath = __DIR__ . '/schema_family.sql'; // Modifie si rangé dans /docker/
if (!file_exists($schemaPath)) {
$schemaPath = __DIR__ . '/docker/schema_family.sql';
}
if (!file_exists($schemaPath)) {
die("❌ Impossible de trouver le fichier schema_family.sql");
}
echo "<h1>🚀 Début de la migration Multi-Tenant</h1>"; $sqlContent = file_get_contents($schemaPath);
/**
* Analyse le code SQL pour en extraire la structure [Table => [Colonnes]]
*/
function parseExpectedSchema($sql) {
$schema = [];
// Récupère tout ce qui se trouve entre CREATE TABLE (...) ENGINE
preg_match_all('/CREATE TABLE (?:IF NOT EXISTS )?`?([a-zA-Z0-9_]+)`?\s*\((.*?)\)\s*ENGINE/si', $sql, $tableMatches, PREG_SET_ORDER);
foreach($tableMatches as $match) {
$tableName = $match[1];
$body = $match[2];
// Sépare les lignes par virgule, en ignorant les virgules entre parenthèses (ex: DECIMAL(10,2) ou ENUM('a','b'))
$lines = preg_split('/,(?![^\(]*\))/', $body);
$columns = [];
foreach($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
// On ignore la déclaration des clés, contraintes et index
if (preg_match('/^(PRIMARY KEY|UNIQUE KEY|FOREIGN KEY|KEY|INDEX|FULLTEXT KEY|CONSTRAINT)\b/i', $line)) {
continue;
}
// Extrait le nom de la colonne et sa définition SQL
if (preg_match('/^`?([a-zA-Z0-9_]+)`?\s+(.*)$/i', $line, $colMatch)) {
$colName = $colMatch[1];
$colDef = rtrim($colMatch[2], ',');
$columns[$colName] = $colDef;
}
}
$schema[$tableName] = $columns;
}
return $schema;
}
$expectedSchema = parseExpectedSchema($sqlContent);
echo "️ Modèle SQL chargé et analysé : <b>" . count($expectedSchema) . " tables</b> détectées.<hr>";
try { try {
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1"); $stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
$families = $stmt->fetchAll(PDO::FETCH_ASSOC); $families = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($families as $family) { foreach ($families as $family) {
$dbName = $family['db_name']; $dbName = $family['db_name'];
echo "<h2>🏡 Famille : {$family['name']} ($dbName)</h2>";
echo "<h2>Famille : {$family['name']} ($dbName)</h2>";
try { try {
$pdo = new PDO( $pdo = new PDO(
"mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", "mysql:host=$db_host;dbname=$dbName;charset=utf8mb4",
$db_user, $db_pass, $db_user, $db_pass,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
); );
// --------------------------------------------------------- // ---------------------------------------------------------
// 2. EXÉCUTION DU SQL (CRÉATION TABLES MANQUANTES + DONNÉES PAR DÉFAUT)
// 1. UPDATE DE pf_people (Juste la date de naissance et l'état actif)
// --------------------------------------------------------- // ---------------------------------------------------------
$colCheck = $pdo->prepare("SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'pf_people' AND COLUMN_NAME = 'birthdate'");
$colCheck->execute([$dbName]);
if ($colCheck->rowCount() === 0) {
$pdo->exec("ALTER TABLE pf_people
ADD COLUMN birthdate DATE DEFAULT NULL,
ADD COLUMN is_active TINYINT(1) DEFAULT 1
");
echo "✅ pf_people mis à jour (birthdate ajouté).<br>";
} else {
echo " pf_people déjà à jour.<br>";
}
// ---------------------------------------------------------
// 2. CRÉATION DES NOUVELLES TABLES (IF NOT EXISTS)
// ---------------------------------------------------------
// Comptes bancaires
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_bank_accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
owner_person_id INT DEFAULT NULL,
account_type VARCHAR(50) DEFAULT 'savings',
is_default TINYINT(1) DEFAULT 0,
FOREIGN KEY (owner_person_id) REFERENCES pf_people(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Catégories de budget
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_budget_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(50) NOT NULL,
label VARCHAR(100) NOT NULL,
type VARCHAR(50) NOT NULL,
color VARCHAR(20) DEFAULT '#ccc',
icon VARCHAR(20) DEFAULT '💰',
UNIQUE KEY (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Types de congés
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_leave_types (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) NOT NULL,
label VARCHAR(100) NOT NULL,
default_allowance DECIMAL(5,2) DEFAULT 0,
reset_month INT DEFAULT 1,
allow_carry_over TINYINT(1) DEFAULT 0,
UNIQUE KEY (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Fêtes (Cadeaux)
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_occasions (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) NOT NULL,
name VARCHAR(100) NOT NULL,
month_date VARCHAR(5) DEFAULT NULL,
is_active TINYINT(1) DEFAULT 1,
UNIQUE KEY (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Règles Cadeaux
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_rules (
id INT AUTO_INCREMENT PRIMARY KEY,
adult_person_id INT NOT NULL,
child_person_id INT NOT NULL,
occasion_id INT NOT NULL,
FOREIGN KEY (adult_person_id) REFERENCES pf_people(id) ON DELETE CASCADE,
FOREIGN KEY (child_person_id) REFERENCES pf_people(id) ON DELETE CASCADE,
FOREIGN KEY (occasion_id) REFERENCES pf_gift_occasions(id) ON DELETE CASCADE,
UNIQUE KEY adult_child_occ (adult_person_id, child_person_id, occasion_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
echo "✅ Nouvelles tables de configuration créées ou vérifiées.<br>";
// ---------------------------------------------------------
// 3. INJECTION DE DONNÉES PAR DÉFAUT (Pour ne rien casser)
// ---------------------------------------------------------
if ($pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn() == 0) {
$pdo->exec("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES
('Compte Commun', 'checking', 1),
('Livret A Alex', 'savings', 0),
('Livret A Laia', 'savings', 0),
('Livret A Pol', 'savings', 0),
('Livret A Pep', 'savings', 0)
");
}
// Utilisation de INSERT IGNORE pour éviter les doublons tout en injectant les catégories manquantes
$pdo->exec("INSERT IGNORE INTO pf_budget_categories (code, label, type, color, icon) VALUES
('INCOME', 'Revenus', 'Income', '#22c55e', '💵'),
('FMCG', 'Alimentation', 'Expense', '#3b82f6', '🛒'),
('FUEL', 'Carburant', 'Expense', '#f59e0b', '⛽'),
('SCHOOL', 'École / Garde', 'Expense', '#a855f7', '🎒'),
('HEALTH', 'Santé', 'Expense', '#ef4444', '⚕️'),
('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢'),
('SAVINGS', 'Épargne & Projets', 'Expense', '#8b5cf6', '🐷'),
('AUTRES', 'Autres / Divers', 'Expense', '#64748b', '📁')
");
if ($pdo->query("SELECT COUNT(*) FROM pf_leave_types")->fetchColumn() == 0) {
$pdo->exec("INSERT INTO pf_leave_types (code, label, default_allowance, reset_month, allow_carry_over) VALUES
('CA', 'Congés Annuels', 25, 6, 1),
('JRA', 'Jours de Repos', 10, 1, 0),
('JA', 'Jour Anniversaire', 1, 1, 0)
");
}
if ($pdo->query("SELECT COUNT(*) FROM pf_gift_occasions")->fetchColumn() == 0) {
$pdo->exec("INSERT INTO pf_gift_occasions (code, name, month_date) VALUES
('NOEL', 'Noël', '12-25'),
('ROIS', 'Les Rois', '01-06'),
('ANNIV', 'Anniversaire', NULL)
");
}
echo "✅ Données de base injectées (si nécessaire).<br>";
// ---------------------------------------------------------
// 4. MIGRATION SPECIFIQUE : MODULE CALENDRIER (CONGÉS GRANULAIRES)
// ---------------------------------------------------------
$tableExists = $pdo->query("SHOW TABLES LIKE 'pf_person_leave_meta'")->rowCount() > 0;
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) {}
$pdo->exec("ALTER TABLE pf_person_leave_meta ADD UNIQUE KEY uq_person_leave (person_id, leave_type)");
echo "✅ Table pf_person_leave_meta mise à jour avec le type granulaire.<br>";
} else {
echo " Table pf_person_leave_meta déjà à jour.<br>";
}
}
// ---------------------------------------------------------
// 5. MIGRATION DES PARAMÈTRES DYNAMIQUES (pf_settings)
// ---------------------------------------------------------
$pdo->exec("
CREATE TABLE IF NOT EXISTS `pf_settings` (
`setting_key` varchar(50) NOT NULL,
`setting_value` text DEFAULT NULL,
`module` varchar(50) NOT NULL,
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`setting_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
$pdo->exec("
INSERT IGNORE INTO `pf_settings` (`setting_key`, `setting_value`, `module`) VALUES
('calendar_default_view', 'month', 'calendar'),
('calendar_first_day', '1', 'calendar'),
('calendar_working_hours', '08:00-19:00', 'calendar'),
('budget_start_day', '1', 'budget'),
('budget_default_tab', 'dépenses', 'budget'),
('travel_default_transport', 'car', 'voyage'),
('travel_default_fuel_price', '1.85', 'voyage'),
('gifts_hide_purchased', '0', 'cadeaux'),
('gifts_default_sort', 'person', 'cadeaux'),
('gifts_budget_alert', '500', 'cadeaux');
");
echo "✅ Table pf_settings créée et paramètres de base injectés.<br>";
try { try {
$pdo->exec($sqlContent);
$pdo->exec("ALTER TABLE `pf_vehicles` ADD COLUMN `consumption` decimal(4,2) DEFAULT NULL COMMENT 'Consommation L/100km' AFTER `fuel_type`"); echo "✅ Structure de base validée (les tables manquantes ont été créées).<br>";
echo "✅ Colonne consumption ajoutée au Garage.<br>";
} catch (PDOException $e) { } catch (PDOException $e) {
echo "⚠️ Avertissement SQL brut : " . $e->getMessage() . "<br>";
echo " pf_vehicles déjà à jour.<br>";
} }
// --------------------------------------------------------- // ---------------------------------------------------------
// 3. DIFFING (AJOUT DYNAMIQUE DES COLONNES MANQUANTES)
// 6. MIGRATION DE L'HISTORIQUE CALENDRIER (FINI LE CODÉ EN DUR)
// --------------------------------------------------------- // ---------------------------------------------------------
$colsAdded = 0;
foreach ($expectedSchema as $table => $expectedCols) {
$stmtCol = $pdo->query("SHOW COLUMNS FROM `$table`");
$existingCols = $stmtCol->fetchAll(PDO::FETCH_COLUMN);
$stmtHelper = $pdo->query("SELECT id FROM pf_people WHERE role = 'helper' LIMIT 1"); foreach ($expectedCols as $colName => $colDef) {
if (!in_array($colName, $existingCols)) {
$helperId = $stmtHelper->fetchColumn(); $pdo->exec("ALTER TABLE `$table` ADD COLUMN `$colName` $colDef");
echo " Nouvelle colonne ajoutée : <b>$table.$colName</b><br>";
$colsAdded++;
}
if ($helperId) { }
}
$pdo->exec("UPDATE pf_events SET event_type = 'HELPER_OFF', person_id = $helperId WHERE event_type = 'OFF_CAROLE'"); if ($colsAdded === 0) {
echo "✅ Toutes les colonnes de toutes les tables sont déjà à jour.<br>";
$pdo->exec("UPDATE pf_events SET event_type = 'HELPER_EXTRA', person_id = $helperId WHERE event_type = 'EXTRA_OFF_CAROLE'");
echo "✅ Historique des absences Nounou migré dynamiquement.<br>";
} }
$stmtKid = $pdo->query("SELECT id FROM pf_people WHERE role IN ('child', 'enfant') ORDER BY id ASC LIMIT 1");
$kidId = $stmtKid->fetchColumn();
if ($kidId) {
$pdo->exec("UPDATE pf_events SET event_type = 'CHILD_SICK', person_id = $kidId WHERE event_type = 'PEP_SICK'");
echo "✅ Historique des maladies Enfant migré dynamiquement.<br>";
}
// --------------------------------------------------------- // ---------------------------------------------------------
// 4. MIGRATION DES DONNÉES (TRANSFORMATIONS HISTORIQUES)
// 7. MIGRATION DÉFINITIVE : STRUCTURE DU BUDGET
// --------------------------------------------------------- // ---------------------------------------------------------
echo "<i>🔄 Exécution des transformations de données héritées...</i><br>";
// A. Changement de types spécifiques (Car le parseur n'ajoute que les colonnes manquantes)
// A. Changement du type de la colonne (ENUM -> VARCHAR)
$pdo->exec("ALTER TABLE pf_budget_items MODIFY category VARCHAR(100) DEFAULT NULL"); $pdo->exec("ALTER TABLE pf_budget_items MODIFY category VARCHAR(100) DEFAULT NULL");
// B. Migration de l'historique calendrier (Enfants & Helpers)
$helperId = $pdo->query("SELECT id FROM pf_people WHERE role = 'helper' LIMIT 1")->fetchColumn();
if ($helperId) {
$pdo->exec("UPDATE pf_events SET event_type = 'HELPER_OFF', person_id = $helperId WHERE event_type = 'OFF_CAROLE'");
$pdo->exec("UPDATE pf_events SET event_type = 'HELPER_EXTRA', person_id = $helperId WHERE event_type = 'EXTRA_OFF_CAROLE'");
}
// B. MAPPING INTELLIGENT DES DONNEES EXISTANTES (Nettoyage de l'ancienne logique) $kidId = $pdo->query("SELECT id FROM pf_people WHERE role IN ('child', 'enfant') ORDER BY id ASC LIMIT 1")->fetchColumn();
if ($kidId) {
$pdo->exec("UPDATE pf_events SET event_type = 'CHILD_SICK', person_id = $kidId WHERE event_type = 'PEP_SICK'");
}
// C. Mapping Intelligent du Budget (is_estimate)
$itemsMigres = 0; $itemsMigres = 0;
// 1. Les charges fixes (is_estimate = 0) deviennent FIXED
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'FIXED' WHERE category = 'expense' AND is_estimate = 0"); $itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'FIXED' WHERE category = 'expense' AND is_estimate = 0");
// 2. Les estimations (is_estimate = 1) sont mappées selon leur nom ou mots clés (hack historique)
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'FUEL' WHERE category = 'expense' AND is_estimate = 1 AND (mapping_keywords LIKE '%ESSENCE%' OR name LIKE '%gasolina%')"); $itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'FUEL' WHERE category = 'expense' AND is_estimate = 1 AND (mapping_keywords LIKE '%ESSENCE%' OR name LIKE '%gasolina%')");
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'SCHOOL' WHERE category = 'expense' AND is_estimate = 1 AND (mapping_keywords LIKE '%ESCOLA%' OR mapping_keywords LIKE '%PARASCOL%' OR name LIKE '%escola%')"); $itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'SCHOOL' WHERE category = 'expense' AND is_estimate = 1 AND (mapping_keywords LIKE '%ESCOLA%' OR mapping_keywords LIKE '%PARASCOL%' OR name LIKE '%escola%')");
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'FMCG' WHERE category = 'expense' AND is_estimate = 1 AND (mapping_keywords LIKE '%FMCG%' OR name LIKE '%F&B%')"); $itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'FMCG' WHERE category = 'expense' AND is_estimate = 1 AND (mapping_keywords LIKE '%FMCG%' OR name LIKE '%F&B%')");
// 3. On sécurise les éventuelles estimations restantes dans AUTRES
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'AUTRES' WHERE category = 'expense'"); $itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'AUTRES' WHERE category = 'expense'");
// 4. On sécurise les anciens revenus
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'INCOME' WHERE category = 'income'"); $itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'INCOME' WHERE category = 'income'");
// D. Nettoyage des virgules orphelines dans mapping_keywords
// C. Nettoyage des virgules orphelines dans mapping_keywords (suppression des vieux hacks)
$budgetCodes = ['INCOME', 'FMCG', 'FUEL', 'SCHOOL', 'HEALTH', 'FIXED', 'SAVINGS', 'AUTRES']; $budgetCodes = ['INCOME', 'FMCG', 'FUEL', 'SCHOOL', 'HEALTH', 'FIXED', 'SAVINGS', 'AUTRES'];
foreach ($budgetCodes as $code) { foreach ($budgetCodes as $code) {
$pdo->exec("UPDATE pf_budget_items SET mapping_keywords = REPLACE(mapping_keywords, '$code', '') WHERE mapping_keywords LIKE '%$code%'"); $pdo->exec("UPDATE pf_budget_items SET mapping_keywords = REPLACE(mapping_keywords, '$code', '') WHERE mapping_keywords LIKE '%$code%'");
} }
$pdo->exec("UPDATE pf_budget_items SET mapping_keywords = TRIM(BOTH ',' FROM REPLACE(REPLACE(mapping_keywords, ' ', ''), ',,', ','))"); $pdo->exec("UPDATE pf_budget_items SET mapping_keywords = TRIM(BOTH ',' FROM REPLACE(REPLACE(mapping_keywords, ' ', ''), ',,', ','))");
// E. Migration de l'historique des dépenses et des règles d'import
// D. Migration de l'historique des dépenses et des règles (Anciens libellés FR/EN -> Nouveaux Codes Dynamiques)
$budgetMapping = [ $budgetMapping = [
'Income' => 'INCOME', 'Income' => 'INCOME',
'FMCG' => 'FMCG', 'FMCG' => 'FMCG',
'Essence' => 'FUEL', 'Essence' => 'FUEL',
'School' => 'SCHOOL', 'School' => 'SCHOOL',
'Frais' => 'FIXED', 'Frais' => 'FIXED',
'LivretA' => 'SAVINGS', 'LivretA' => 'SAVINGS',
'Autres' => 'AUTRES' 'Autres' => 'AUTRES'
]; ];
$stmtExp = $pdo->prepare("UPDATE pf_expenses SET category = ? WHERE category = ?"); $stmtExp = $pdo->prepare("UPDATE pf_expenses SET category = ? WHERE category = ?");
$stmtRules = $pdo->prepare("UPDATE pf_import_rules SET category = ? WHERE category = ?"); $stmtRules = $pdo->prepare("UPDATE pf_import_rules SET category = ? WHERE category = ?");
$expCount = 0;
$rulesCount = 0;
foreach ($budgetMapping as $old => $newCode) { foreach ($budgetMapping as $old => $newCode) {
$stmtExp->execute([$newCode, $old]); $stmtExp->execute([$newCode, $old]);
$expCount += $stmtExp->rowCount();
$stmtRules->execute([$newCode, $old]); $stmtRules->execute([$newCode, $old]);
$rulesCount += $stmtRules->rowCount();
} }
echo "✅ Transformations Data terminées ($itemsMigres prévisions remappées).<hr>";
echo "✅ Budget Structure : Table pf_budget_items passée en VARCHAR. $itemsMigres prévisions recatégorisées. Historique migré ($expCount dépenses et $rulesCount règles mises à jour).<br>";
} catch (PDOException $e) { } catch (PDOException $e) {
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<hr>";
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>";
} }
} // FIN DE LA BOUCLE FOREACH FAMILLES
} // FIN DE LA BOUCLE FOREACH echo "<h1>🎉 Synchronisation et Migration terminées avec succès !</h1>";
try {
// Vérification si la colonne csv_mapping existe déjà dans pf_foyer_settings
$checkCol = $pdo->query("SHOW COLUMNS FROM pf_foyer_settings LIKE 'csv_mapping'")->fetch();
if (!$checkCol) {
$pdo->exec("ALTER TABLE pf_foyer_settings ADD COLUMN csv_mapping TEXT NULL");
echo "✅ Migration : Colonne 'csv_mapping' ajoutée à la table 'pf_foyer_settings'.\n";
} else {
echo "️ Migration : La colonne 'csv_mapping' existe déjà.\n";
}
} catch (PDOException $e) {
echo "❌ Erreur lors de la migration : " . $e->getMessage() . "\n";
}
echo "<h1>🎉 Migration terminée avec succès !</h1>";
} catch (Exception $e) { } catch (Exception $e) {
die("❌ Erreur fatale Meta DB : " . $e->getMessage()); die("❌ Erreur fatale Meta DB : " . $e->getMessage());
} }
?>
?>
+542 -232
View File
@@ -37,6 +37,7 @@
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
/* Thèmes des tableaux */
.nens-title.theme-pol { .nens-title.theme-pol {
color: #1e3a8a !important; color: #1e3a8a !important;
background: #eaf2ff !important; background: #eaf2ff !important;
@@ -147,6 +148,7 @@
z-index: 10 !important; z-index: 10 !important;
pointer-events: auto !important; pointer-events: auto !important;
} }
.month-actions { .month-actions {
display: flex; display: flex;
gap: 4px; gap: 4px;
@@ -261,13 +263,13 @@ td:hover .btn-cell-delete {
padding: 18px 16px; padding: 18px 16px;
border-top: 1px solid #e2e8f0; border-top: 1px solid #e2e8f0;
} }
.form-row { .form-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 16px; gap: 16px;
margin-bottom: 16px; margin-bottom: 16px;
} }
input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
@@ -373,7 +375,7 @@ input[type="number"] {
gap: 10px; gap: 10px;
} }
/* Configuration Chromatique Parent 1 */ /* Configuration Chromatique Parent 1 & 2 */
.txt-p1, .txt-p1,
.txt-alex { .txt-alex {
color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important; color: color-mix(in srgb, var(--p1-main, #0891b2) 85%, #000000) !important;
@@ -404,7 +406,6 @@ input.txt-p1:focus {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--p1-main, #0891b2) 10%, #ffffff) !important; box-shadow: 0 0 0 3px color-mix(in srgb, var(--p1-main, #0891b2) 10%, #ffffff) !important;
} }
/* Configuration Chromatique Parent 2 */
.txt-p2 { .txt-p2 {
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important; color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
font-weight: bold; font-weight: bold;
@@ -503,8 +504,6 @@ input.txt-p2:focus {
color: var(--text-main) !important; color: var(--text-main) !important;
border-left: 4px solid var(--p1-main, #0891b2) !important; border-left: 4px solid var(--p1-main, #0891b2) !important;
} }
/* Parent 2 (Ligne 2, ex: Laia) */
.prev-salary-table tbody tr:nth-child(2) td:first-child { .prev-salary-table tbody tr:nth-child(2) td:first-child {
text-align: left !important; text-align: left !important;
font-weight: bold !important; font-weight: bold !important;
@@ -762,7 +761,6 @@ tr:hover .row-actions {
gap: 4px; gap: 4px;
} }
/* --- Style de base du bouton de validation (Générique) --- */
.recap-table .pf-btn.btn-small { .recap-table .pf-btn.btn-small {
background: #ffffff !important; background: #ffffff !important;
font-size: 0.7rem !important; font-size: 0.7rem !important;
@@ -775,8 +773,6 @@ tr:hover .row-actions {
transform: none !important; transform: none !important;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
/* Couleurs dynamiques adaptées au Parent 1 (ex: Féfé) */
.col-p1 .pf-btn.btn-small { .col-p1 .pf-btn.btn-small {
color: var(--p1-main, #0891b2) !important; color: var(--p1-main, #0891b2) !important;
border: 1px solid var(--p1-main, #0891b2) !important; border: 1px solid var(--p1-main, #0891b2) !important;
@@ -788,8 +784,6 @@ tr:hover .row-actions {
#ffffff #ffffff
) !important; ) !important;
} }
/* Couleurs dynamiques adaptées au Parent 2 (ex: Laia) */
.col-p2 .pf-btn.btn-small { .col-p2 .pf-btn.btn-small {
color: var(--p2-main, #f59e0b) !important; color: var(--p2-main, #f59e0b) !important;
border: 1px solid var(--p2-main, #f59e0b) !important; border: 1px solid var(--p2-main, #f59e0b) !important;
@@ -802,7 +796,6 @@ tr:hover .row-actions {
) !important; ) !important;
} }
/* Grand Totaux Structuraux */
.row-grand-total td { .row-grand-total td {
background-color: #1e293b; background-color: #1e293b;
color: white; color: white;
@@ -1094,7 +1087,544 @@ body.sum-mode-active .sum-target {
transition: background-color 0.2s; transition: background-color 0.2s;
} }
/* --- 13. RESPONSIVE MOBILE BUDGET --- */ /* --- 13. 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;
}
/* Utilitaires et Formulaires 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;
background: var(--bg-panel);
}
.bs-list-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
.bs-section-title {
font-size: 1.1rem;
color: var(--text-main);
margin-top: 0;
margin-bottom: 1rem;
}
.bs-section-subtitle {
font-size: 1rem;
margin-top: 0;
margin-bottom: 1rem;
}
.bs-section-subtitle.bordered {
margin: 0 0 15px 0;
border-bottom: 1px solid var(--border-light);
padding-bottom: 5px;
font-size: 0.95rem;
}
.bs-form-inline {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.bs-form-inline.align-center {
align-items: center;
}
.bs-input-flex {
flex: 1;
min-width: 150px;
padding: 0.4rem 0.75rem;
}
.bs-input-fixed {
width: 130px;
padding: 0.4rem 0.75rem;
}
.bs-input-icon {
width: 65px;
padding: 0.4rem 0.2rem;
font-size: 1.1rem;
cursor: pointer;
text-align: center;
}
.bs-input-color {
width: 38px;
height: 38px;
padding: 0;
border: 1px solid var(--border-light);
border-radius: 6px;
cursor: pointer;
}
.bs-pane-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.2rem;
border-bottom: 1px solid var(--border-light);
padding-bottom: 0.5rem;
}
.bs-currency-selector {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.8rem;
color: var(--text-muted);
font-weight: 500;
}
.bs-currency-select {
width: 75px;
padding: 3px 6px;
font-size: 0.8rem;
height: auto;
cursor: pointer;
background: var(--bg-page);
}
.bs-divider {
margin: 1.5rem 0;
border: 0;
border-bottom: 1px solid var(--border-light);
}
.bs-badge-default {
font-size: 0.7em;
background: var(--success);
color: white;
padding: 2px 6px;
border-radius: 4px;
margin-left: 8px;
}
/* Onboarding Checklist */
.bs-onboarding-card {
background: #fffbeb;
border-left: 4px solid #f59e0b;
padding: 15px;
margin-bottom: 25px;
border-radius: 8px;
}
.bs-onboarding-title {
margin: 0 0 10px 0;
color: #b45309;
display: flex;
align-items: center;
gap: 8px;
font-size: 1.1rem;
}
.bs-onboarding-desc {
margin: 0 0 10px 0;
font-size: 0.85rem;
color: #d97706;
}
.bs-onboarding-list {
margin: 0;
padding-left: 20px;
color: #b45309;
font-size: 0.9rem;
line-height: 1.8;
}
.bs-onboarding-list li {
margin-bottom: 4px;
}
.bs-onboarding-list strong {
font-size: 1.1rem;
margin-right: 5px;
}
.bs-onboarding-link {
color: #b45309;
text-decoration: underline;
font-weight: bold;
cursor: pointer;
}
/* Affichage Tag Cloud Règles */
.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-accordion-summary {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.bs-accordion-count {
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;
}
.bs-rule-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
padding: 0.5rem 0;
}
.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);
}
/* CSV & Utils */
.bs-desc {
margin-top: 0;
margin-bottom: 15px;
color: var(--text-muted);
font-size: 0.85rem;
}
.bs-csv-dropzone {
border: 2px dashed var(--border-light);
border-radius: var(--radius);
padding: 30px;
text-align: center;
cursor: pointer;
margin-bottom: 25px;
transition: all 0.2s;
background: var(--bg-page);
}
.bs-csv-dropzone:hover {
border-color: var(--primary);
background: rgba(37, 99, 235, 0.05);
}
.bs-csv-icon {
font-size: 2rem;
margin-bottom: 10px;
}
.bs-csv-title {
margin: 0 0 5px 0;
font-size: 1.1rem;
}
.bs-csv-subtitle {
margin: 0;
color: var(--text-muted);
font-size: 0.85rem;
}
.bs-csv-preview {
display: none;
background: var(--bg-page);
padding: 15px;
border-radius: var(--radius);
border: 1px dashed var(--primary);
margin-bottom: 25px;
}
.bs-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.bs-text-muted-normal {
color: var(--text-muted);
font-weight: normal;
}
.bs-card-gray {
background: var(--bg-panel);
padding: 10px;
border-radius: var(--radius);
border: 1px solid var(--border-light);
margin-bottom: 15px;
}
.bs-input-mb {
margin-bottom: 10px;
}
.bs-text-right {
text-align: right;
}
.bs-csv-table-wrapper {
overflow-x: auto;
max-width: 100%;
border: 1px solid var(--border-light);
border-radius: 8px;
background: white;
}
.bs-csv-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
text-align: left;
white-space: nowrap;
}
.bs-csv-thead {
background: var(--bg-page);
border-bottom: 2px solid var(--border-light);
}
.bs-csv-th {
padding: 10px 15px;
font-weight: 600;
color: var(--text-main);
border-right: 1px solid var(--border-light);
}
.bs-csv-th-index {
font-size: 0.75rem;
color: var(--primary);
margin-bottom: 2px;
}
.bs-csv-td-empty {
padding: 15px;
text-align: center;
color: var(--text-muted);
font-style: italic;
}
.bs-csv-tr {
border-bottom: 1px solid #e2e8f0;
}
.bs-csv-td {
padding: 10px 15px;
color: var(--text-muted);
border-right: 1px dashed #e2e8f0;
}
/* Ajouts Structure Global Budget */
.budget-hero {
text-align: center;
margin-bottom: 30px;
}
.budget-title-group {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.budget-title-group h1 {
margin: 0;
}
.btn-settings-gear {
position: relative;
}
.budget-error-msg {
text-align: center;
padding: 50px;
color: var(--danger);
}
@keyframes pulse-warning {
0% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
}
70% {
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
}
}
/* Le bouton engrenage devient un simple conteneur fixe */
.btn-header-settings {
position: relative;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 5px;
text-decoration: none;
color: inherit;
}
/* C'est uniquement l'icône à l'intérieur qui tourne ! */
.gear-icon-anim {
display: inline-block;
transition: transform 0.4s ease-in-out;
}
.btn-header-settings:hover .gear-icon-anim {
transform: rotate(90deg);
}
.alert-dot {
display: flex;
align-items: center;
justify-content: center;
background: var(--danger, #ef4444);
color: white;
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 0.85rem;
font-weight: bold;
position: absolute;
top: -4px;
right: -4px;
animation: pulse-warning 2s infinite;
border: 2px solid white;
line-height: 1;
}
.alert-dot-inline {
position: relative;
top: auto;
right: auto;
display: inline-flex;
margin-left: 6px;
width: 16px;
height: 16px;
font-size: 0.75rem;
border-width: 1px;
animation: none; /* Pas de clignotement dans le menu pour ne pas agresser l'œil */
}
/* Responsive Modale Paramètres */
@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;
}
}
/* Responsive Mobile Budget */
@media (max-width: 768px) { @media (max-width: 768px) {
.budget-view { .budget-view {
padding: 10px !important; padding: 10px !important;
@@ -1268,223 +1798,3 @@ 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);
}
+10 -1
View File
@@ -29,7 +29,16 @@ try {
// 4. Salaires (Année en cours) // 4. Salaires (Année en cours)
$currentYear = (int)date('Y'); $currentYear = (int)date('Y');
$salaries = $pdo->query("SELECT * FROM pf_salary_config WHERE year = $currentYear ORDER BY person ASC")->fetchAll(PDO::FETCH_ASSOC); $salaries = $pdo->query("
SELECT p.name as person,
COALESCE(s.salary, 0) as salary,
COALESCE(s.mensualite, 0) as mensualite,
s.id as id
FROM pf_people p
LEFT JOIN pf_salary_config s ON p.name = s.person AND s.year = $currentYear
WHERE p.role NOT IN ('enfant', 'nounou') AND p.is_active = 1
ORDER BY p.name ASC
")->fetchAll(PDO::FETCH_ASSOC);
// 5. Paramètres Foyer (Devise + Mapping CSV) // 5. Paramètres Foyer (Devise + Mapping CSV)
$foyerData = $pdo->query("SELECT currency, csv_mapping FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC); $foyerData = $pdo->query("SELECT currency, csv_mapping FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC);
+151 -133
View File
@@ -1,15 +1,18 @@
<?php <?php
// modules/budget/views/budget_prev.php // modules/budget/views/budget_prev.php
// 1. Chargement dynamique des parents // 1. Chargement dynamique des ADULTES de la famille
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color FROM pf_people WHERE role = 'parent' ORDER BY id ASC"); $stmtPeople = $pdo->query("SELECT id, name, user_id, role, color FROM pf_people WHERE role NOT IN ('enfant', 'nounou') AND is_active = 1 ORDER BY id ASC");
$budgetParents = $stmtPeople->fetchAll(PDO::FETCH_ASSOC); $budgetParents = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
$p1_name = $budgetParents[0]['name'] ?? 'Parent 1'; // Sécurité : au cas où aucun adulte n'est trouvé, on évite un crash
$p2_name = $budgetParents[1]['name'] ?? 'Parent 2'; if (empty($budgetParents)) {
$budgetParents[] = ['id' => 0, 'name' => 'Utilisateur', 'color' => '#0891b2'];
}
$currentYear = date('Y'); $currentYear = date('Y');
// Cartographie dynamique // Cartographie dynamique (Supporte 1, 2 ou N adultes)
$parentMapping = []; $parentMapping = [];
foreach ($budgetParents as $index => $parent) { foreach ($budgetParents as $index => $parent) {
$num = $index + 1; $num = $index + 1;
@@ -21,6 +24,10 @@ foreach ($budgetParents as $index => $parent) {
]; ];
} }
// 1.5 Récupération des VRAIS comptes bancaires créés dans les paramètres
$stmtAccounts = $pdo->query("SELECT name FROM pf_bank_accounts ORDER BY is_default DESC, name ASC");
$bankAccounts = $stmtAccounts->fetchAll(PDO::FETCH_COLUMN);
// 2. Récupération Config Salaires // 2. Récupération Config Salaires
$salaryConfig = []; $salaryConfig = [];
$stmt = $pdo->prepare("SELECT * FROM pf_salary_config WHERE year = ?"); $stmt = $pdo->prepare("SELECT * FROM pf_salary_config WHERE year = ?");
@@ -29,7 +36,7 @@ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$salaryConfig[$row['person']] = $row; $salaryConfig[$row['person']] = $row;
} }
// Sécurité profils de salaire vides // Sécurité profils de salaire vides (Garantit l'affichage à 0 même si non configuré)
foreach ($parentMapping as $map) { foreach ($parentMapping as $map) {
if (!isset($salaryConfig[$map['name']])) { if (!isset($salaryConfig[$map['name']])) {
$salaryConfig[$map['name']] = ['salary'=>0, 'mensualite'=>0, 'frais_func'=>0, 'eco_perso'=>0, 'eco_family'=>0]; $salaryConfig[$map['name']] = ['salary'=>0, 'mensualite'=>0, 'frais_func'=>0, 'eco_perso'=>0, 'eco_family'=>0];
@@ -103,21 +110,28 @@ function getTranslatedMonthName($dateString) {
return tr('month_' . $m) . ' ' . $y; return tr('month_' . $m) . ' ' . $y;
} }
// --- 🧠 PREPARATION DATA TRICOUNT / AVANCES --- // --- 🧠 PREPARATION DATA TRICOUNT / AVANCES (Dynamique) ---
$stmtAdvancesList = $pdo->query("SELECT * FROM pf_advances WHERE is_resolved = 0 ORDER BY advance_date DESC"); $stmtAdvancesList = $pdo->query("SELECT * FROM pf_advances WHERE is_resolved = 0 ORDER BY advance_date DESC");
$activeAdvances = $stmtAdvancesList->fetchAll(PDO::FETCH_ASSOC); $activeAdvances = $stmtAdvancesList->fetchAll(PDO::FETCH_ASSOC);
$advTotal = [$p1_name => 0, $p2_name => 0]; $advTotal = [];
$livretTotal = [$p1_name => 0, $p2_name => 0]; $livretTotal = [];
$labelsCC = [];
$labelsLivret = [];
$labelsCC = [$p1_name => [], $p2_name => []]; foreach ($parentMapping as $map) {
$labelsLivret = [$p1_name => [], $p2_name => []]; $advTotal[$map['name']] = 0;
$livretTotal[$map['name']] = 0;
$labelsCC[$map['name']] = [];
$labelsLivret[$map['name']] = [];
}
foreach ($activeAdvances as $adv) { foreach ($activeAdvances as $adv) {
$p = $adv['payer']; $p = $adv['payer'];
$amt = (float)$adv['amount']; $amt = (float)$adv['amount'];
$labelStr = htmlspecialchars($adv['description']) . ' (' . number_format($amt, 0, ',', ' ') . '€)'; $labelStr = htmlspecialchars($adv['description']) . ' (' . number_format($amt, 0, ',', ' ') . '€)';
// Si un ancien nom traîne en base, on l'initialise
if (!isset($advTotal[$p])) { if (!isset($advTotal[$p])) {
$advTotal[$p] = 0; $livretTotal[$p] = 0; $advTotal[$p] = 0; $livretTotal[$p] = 0;
$labelsCC[$p] = []; $labelsLivret[$p] = []; $labelsCC[$p] = []; $labelsLivret[$p] = [];
@@ -132,10 +146,16 @@ foreach ($activeAdvances as $adv) {
} }
} }
$balanceDiff = abs(($advTotal[$p1_name] ?? 0) - ($advTotal[$p2_name] ?? 0)); // Calcul de la dette croisée (Valable surtout si 2 parents)
$balanceDiff = 0;
$owedTo = ''; $owedTo = '';
if (($advTotal[$p1_name] ?? 0) > ($advTotal[$p2_name] ?? 0)) $owedTo = $p1_name; if (count($parentMapping) >= 2) {
elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_name; $p1 = $parentMapping[0]['name'];
$p2 = $parentMapping[1]['name'];
$balanceDiff = abs(($advTotal[$p1] ?? 0) - ($advTotal[$p2] ?? 0));
if (($advTotal[$p1] ?? 0) > ($advTotal[$p2] ?? 0)) $owedTo = $p1;
elseif (($advTotal[$p2] ?? 0) > ($advTotal[$p1] ?? 0)) $owedTo = $p2;
}
?> ?>
@@ -260,10 +280,11 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
$isIndicative = (strpos($cat['name'], 'Eco P') === 0); $isIndicative = (strpos($cat['name'], 'Eco P') === 0);
$rowClass = $isIndicative ? 'row-indicative' : ''; $rowClass = $isIndicative ? 'row-indicative' : '';
$inputClass = $isIndicative ? 'ignore-calc' : ''; $inputClass = $isIndicative ? 'ignore-calc' : '';
// Retrocompatibilité pour les anciens noms d'économies hardcodés
$catDisplayName = $cat['name']; $catDisplayName = $cat['name'];
if ($catDisplayName === 'Eco P1') $catDisplayName = 'Eco ' . $p1_name; if (isset($parentMapping[0]) && $catDisplayName === 'Eco P1') $catDisplayName = 'Eco ' . $parentMapping[0]['name'];
if ($catDisplayName === 'Eco P2') $catDisplayName = 'Eco ' . $p2_name; if (isset($parentMapping[1]) && $catDisplayName === 'Eco P2') $catDisplayName = 'Eco ' . $parentMapping[1]['name'];
?> ?>
<tr class="<?= $rowClass ?>"> <tr class="<?= $rowClass ?>">
<td class="col-sticky"> <td class="col-sticky">
@@ -323,6 +344,7 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</div> </div>
</div> </div>
<?php if (count($parentMapping) > 1): ?>
<div style="margin: 30px 0; background: var(--bg-panel); padding: 24px; border-radius: var(--radius); border: 1px solid var(--border-light); box-shadow: var(--shadow);"> <div style="margin: 30px 0; background: var(--bg-panel); padding: 24px; border-radius: var(--radius); border: 1px solid var(--border-light); box-shadow: var(--shadow);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px;">
<h3 style="margin: 0; font-size: 1.3rem; font-weight: 800;"><?= tr('bud_adv_title') ?></h3> <h3 style="margin: 0; font-size: 1.3rem; font-weight: 800;"><?= tr('bud_adv_title') ?></h3>
@@ -332,57 +354,39 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</div> </div>
<div style="display: flex; gap: 20px; margin-bottom: 24px; flex-wrap: wrap;"> <div style="display: flex; gap: 20px; margin-bottom: 24px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 240px; background: rgba(8, 145, 178, 0.06); border: 1px solid rgba(8, 145, 178, 0.2); padding: 18px; border-radius: 12px;"> <?php foreach ($parentMapping as $idx => $map):
<div style="font-size: 0.85rem; color: #0891b2; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($p1_name)) ?></div> $pName = $map['name'];
$bgClass = ($idx === 0) ? 'rgba(8, 145, 178, 0.06)' : 'rgba(217, 119, 6, 0.06)';
$bdClass = ($idx === 0) ? 'rgba(8, 145, 178, 0.2)' : 'rgba(217, 119, 6, 0.2)';
$colorMain = ($idx === 0) ? '#0891b2' : '#d97706';
$colorDark = ($idx === 0) ? '#164e63' : '#78350f';
?>
<div style="flex: 1; min-width: 240px; background: <?= $bgClass ?>; border: 1px solid <?= $bdClass ?>; padding: 18px; border-radius: 12px;">
<div style="font-size: 0.85rem; color: <?= $colorMain ?>; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($pName)) ?></div>
<div style="margin-top: 10px;"> <div style="margin-top: 10px;">
<div style="display: flex; align-items: baseline; gap: 6px;"> <div style="display: flex; align-items: baseline; gap: 6px;">
<span style="font-size: 1.5rem; font-weight: 800; color: #164e63; font-family: monospace;"><?= number_format($advTotal[$p1_name] ?? 0, 2, ',', ' ') ?> €</span> <span style="font-size: 1.5rem; font-weight: 800; color: <?= $colorDark ?>; font-family: monospace;"><?= number_format($advTotal[$pName] ?? 0, 2, ',', ' ') ?> €</span>
</div> </div>
<small style="color: var(--text-muted); font-size: 0.75rem; font-weight: 500;"><?= tr('bud_adv_cc_label') ?></small> <small style="color: var(--text-muted); font-size: 0.75rem; font-weight: 500;"><?= tr('bud_adv_cc_label') ?></small>
<?php if (!empty($labelsCC[$p1_name])): ?> <?php if (!empty($labelsCC[$pName])): ?>
<div style="font-size: 0.75rem; color: #0e7490; margin-top: 6px; line-height: 1.4; font-style: italic;"> <div style="font-size: 0.75rem; color: <?= $colorMain ?>; margin-top: 6px; line-height: 1.4; font-style: italic;">
<?= implode(', ', $labelsCC[$p1_name]) ?> <?= implode(', ', $labelsCC[$pName]) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php if(($livretTotal[$p1_name] ?? 0) > 0): ?> <?php if(($livretTotal[$pName] ?? 0) > 0): ?>
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed rgba(8, 145, 178, 0.3);"> <div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed <?= $bdClass ?>;">
<div style="font-size: 1.2rem; font-weight: 800; color: #4338ca; font-family: monospace;">+ <?= number_format($livretTotal[$p1_name], 2, ',', ' ') ?> €</div> <div style="font-size: 1.2rem; font-weight: 800; color: <?= $colorDark ?>; font-family: monospace;">+ <?= number_format($livretTotal[$pName], 2, ',', ' ') ?> €</div>
<small style="color: #4338ca; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small> <small style="color: <?= $colorDark ?>; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
<?php if (!empty($labelsLivret[$p1_name])): ?> <?php if (!empty($labelsLivret[$pName])): ?>
<div style="font-size: 0.75rem; color: #3730a3; margin-top: 4px; line-height: 1.4;"> <div style="font-size: 0.75rem; color: <?= $colorDark ?>; margin-top: 4px; line-height: 1.4;">
<?= implode(', ', $labelsLivret[$p1_name]) ?> <?= implode(', ', $labelsLivret[$pName]) ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div style="flex: 1; min-width: 240px; background: rgba(217, 119, 6, 0.06); border: 1px solid rgba(217, 119, 6, 0.2); padding: 18px; border-radius: 12px;">
<div style="font-size: 0.85rem; color: #d97706; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($p2_name)) ?></div>
<div style="margin-top: 10px;">
<div style="display: flex; align-items: baseline; gap: 6px;">
<span style="font-size: 1.5rem; font-weight: 800; color: #78350f; font-family: monospace;"><?= number_format($advTotal[$p2_name] ?? 0, 2, ',', ' ') ?> €</span>
</div>
<small style="color: var(--text-muted); font-size: 0.75rem; font-weight: 500;"><?= tr('bud_adv_cc_label') ?></small>
<?php if (!empty($labelsCC[$p2_name])): ?>
<div style="font-size: 0.75rem; color: #b45309; margin-top: 6px; line-height: 1.4; font-style: italic;">
<?= implode(', ', $labelsCC[$p2_name]) ?>
</div>
<?php endif; ?>
</div>
<?php if(($livretTotal[$p2_name] ?? 0) > 0): ?>
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed rgba(217, 119, 6, 0.3);">
<div style="font-size: 1.2rem; font-weight: 800; color: #b45309; font-family: monospace;">+ <?= number_format($livretTotal[$p2_name], 2, ',', ' ') ?> €</div>
<small style="color: #b45309; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
<?php if (!empty($labelsLivret[$p2_name])): ?>
<div style="font-size: 0.75rem; color: #92400e; margin-top: 4px; line-height: 1.4;">
<?= implode(', ', $labelsLivret[$p2_name]) ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endforeach; ?>
<div style="flex: 1; min-width: 240px; background: var(--bg-page); border: 1px solid var(--border-light); padding: 18px; border-radius: 12px; display: flex; flex-direction: column; justify-content: center;"> <div style="flex: 1; min-width: 240px; background: var(--bg-page); border: 1px solid var(--border-light); padding: 18px; border-radius: 12px; display: flex; flex-direction: column; justify-content: center;">
<div style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px;"> <div style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px;">
@@ -411,10 +415,19 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($activeAdvances as $adv): ?> <?php foreach ($activeAdvances as $adv):
// Attribution de la couleur selon l'utilisateur
$colorP = '#64748b'; // Defaut
foreach($parentMapping as $idx => $m) {
if($m['name'] === $adv['payer']) {
$colorP = ($idx === 0) ? '#0891b2' : '#d97706';
break;
}
}
?>
<tr> <tr>
<td style="color: var(--text-muted);"><?= date('d/m/Y', strtotime($adv['advance_date'])) ?></td> <td style="color: var(--text-muted);"><?= date('d/m/Y', strtotime($adv['advance_date'])) ?></td>
<td style="font-weight: 700; color: <?= $adv['payer'] === $p1_name ? '#0891b2' : '#d97706' ?>;"><?= htmlspecialchars($adv['payer']) ?></td> <td style="font-weight: 700; color: <?= $colorP ?>;"><?= htmlspecialchars($adv['payer']) ?></td>
<td> <td>
<?= htmlspecialchars($adv['description']) ?> <?= htmlspecialchars($adv['description']) ?>
<?php if ($adv['from_savings']): ?> <?php if ($adv['from_savings']): ?>
@@ -441,11 +454,19 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; // Fin du Tricount conditionnel ?>
<?php <?php
$focusMonth = $months[0]; $focusMonth = $months[0];
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
// Génération dynamique des cibles basées UNIQUEMENT sur les comptes bancaires
$targetsOrder = [];
foreach ($bankAccounts as $accName) {
$targetsOrder[] = 'vers ' . $accName;
}
// On ajoute quand même les cibles historiques enregistrées dans les catégories
// (pour ne pas casser l'affichage si on supprime un compte plus tard)
$allTargets = $targetsOrder; $allTargets = $targetsOrder;
foreach($cats as $c) { foreach($cats as $c) {
$t = trim($c['transfer_dest'] ?? ''); $t = trim($c['transfer_dest'] ?? '');
@@ -506,33 +527,38 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</div> </div>
</div> </div>
<div id="addCatModal" class="pf-modal"> <div id="addCatModal" class="pf-modal" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');">
<div class="pf-modal-content"> <div class="pf-modal-content" onclick="event.stopPropagation()">
<div class="prev-header-left">
<h3 class="pf-modal-title"><?= tr('bud_prev_new_line_title') ?></h3> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<button onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');">&times;</button> <h3 class="pf-modal-title" style="margin: 0; font-size: 1.25rem;"><?= tr('bud_prev_new_line_title') ?></h3>
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer; color: var(--text-muted, #64748b);">×</button>
</div> </div>
<form action="/modules/budget/includes/api/save-budget.php" method="POST"> <form action="/modules/budget/includes/api/save-budget.php" method="POST">
<input type="hidden" name="action" value="add_category"> <input type="hidden" name="action" value="add_category">
<div class="form-group">
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label"><?= tr('bud_prev_label_name') ?></label> <label class="pf-label"><?= tr('bud_prev_label_name') ?></label>
<input type="text" name="name" class="pf-input" required> <input type="text" name="name" class="pf-input" required>
</div> </div>
<div class="form-group">
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?? 'Objectif Mensuel (€)' ?></label> <div class="form-group" style="margin-bottom: 1rem;">
<input type="number" step="1" name="target" class="pf-input" placeholder="Ex: 150"> <label class="pf-label"><?= tr('bud_prev_monthly_target') ?></label>
<input type="number" step="1" name="target" class="pf-input" placeholder="<?= tr('bud_prev_target_ph') ?>">
</div> </div>
<div class="form-group">
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?? 'Destination Virement (Optionnel)' ?></label> <div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?></label>
<select name="transfer_dest" class="pf-input"> <select name="transfer_dest" class="pf-input">
<option value="" selected>-- <?= tr('bud_prev_none') ?? 'Aucune' ?> --</option> <option value="">-- <?= tr('bud_prev_none') ?> --</option>
<option value="vers L.Pol">vers L.Pol</option> <?php foreach ($bankAccounts as $accName): ?>
<option value="vers L.Pep">vers L.Pep</option> <option value="vers <?= htmlspecialchars($accName) ?>"><?= tr('bud_prev_transfer_to') ?> <?= htmlspecialchars($accName) ?></option>
<option value="vers L.Perso">vers L.Perso</option> <?php endforeach; ?>
<option value="vers commune">vers commune</option>
</select> </select>
</div> </div>
<div class="form-group">
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label> <label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label>
<select name="holiday_id" class="pf-input"> <select name="holiday_id" class="pf-input">
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option> <option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
@@ -541,7 +567,8 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div class="modal-footer">
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 1.5rem;">
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button> <button type="button" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('bud_add_title') ?></button> <button type="submit" class="pf-btn"><?= tr('bud_add_title') ?></button>
</div> </div>
@@ -549,34 +576,39 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
</div> </div>
</div> </div>
<div id="editCatModal" class="pf-modal"> <div id="editCatModal" class="pf-modal" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');">
<div class="pf-modal-content"> <div class="pf-modal-content" onclick="event.stopPropagation()">
<div class="prev-header-left">
<h3 class="pf-modal-title"><?= tr('bud_prev_edit_line_title') ?></h3> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<button onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');">&times;</button> <h3 class="pf-modal-title" style="margin: 0; font-size: 1.25rem;"><?= tr('bud_prev_edit_line_title') ?></h3>
<button type="button" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer; color: var(--text-muted, #64748b);">×</button>
</div> </div>
<form action="/modules/budget/includes/api/save-budget.php" method="POST"> <form action="/modules/budget/includes/api/save-budget.php" method="POST">
<input type="hidden" name="action" value="update_category"> <input type="hidden" name="action" value="update_category">
<input type="hidden" name="cat_id" id="edit_cat_id"> <input type="hidden" name="cat_id" id="edit_cat_id">
<div class="form-group">
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label"><?= tr('bud_label_name') ?></label> <label class="pf-label"><?= tr('bud_label_name') ?></label>
<input type="text" name="name" id="edit_cat_name" class="pf-input" required> <input type="text" name="name" id="edit_cat_name" class="pf-input" required>
</div> </div>
<div class="form-group">
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?? 'Objectif Mensuel (€)' ?></label> <div class="form-group" style="margin-bottom: 1rem;">
<input type="number" step="1" name="target" id="edit_cat_target" class="pf-input" placeholder="Ex: 150"> <label class="pf-label"><?= tr('bud_prev_monthly_target') ?></label>
<input type="number" step="1" name="target" id="edit_cat_target" class="pf-input" placeholder="<?= tr('bud_prev_target_ph') ?>">
</div> </div>
<div class="form-group">
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?? 'Destination Virement (Optionnel)' ?></label> <div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?></label>
<select name="transfer_dest" id="edit_cat_transfer_dest" class="pf-input"> <select name="transfer_dest" id="edit_cat_transfer_dest" class="pf-input">
<option value="">-- <?= tr('bud_prev_none') ?? 'Aucune' ?> --</option> <option value="">-- <?= tr('bud_prev_none') ?> --</option>
<option value="vers L.Pol">vers L.Pol</option> <?php foreach ($bankAccounts as $accName): ?>
<option value="vers L.Pep">vers L.Pep</option> <option value="vers <?= htmlspecialchars($accName) ?>"><?= tr('bud_prev_transfer_to') ?> <?= htmlspecialchars($accName) ?></option>
<option value="vers L.Perso">vers L.Perso</option> <?php endforeach; ?>
<option value="vers commune">vers commune</option>
</select> </select>
</div> </div>
<div class="form-group">
<div class="form-group" style="margin-bottom: 1rem;">
<label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label> <label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label>
<select name="holiday_id" id="edit_cat_holiday" class="pf-input"> <select name="holiday_id" id="edit_cat_holiday" class="pf-input">
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option> <option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
@@ -585,7 +617,8 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<div class="modal-footer">
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 1.5rem;">
<button type="button" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button> <button type="button" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button> <button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div> </div>
@@ -604,8 +637,9 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label"><?= tr('bud_adv_who_paid') ?></label> <label class="pf-label"><?= tr('bud_adv_who_paid') ?></label>
<select name="payer" class="pf-input" required> <select name="payer" class="pf-input" required>
<option value="<?= htmlspecialchars($p1_name) ?>"><?= htmlspecialchars($p1_name) ?></option> <?php foreach ($parentMapping as $map): ?>
<option value="<?= htmlspecialchars($p2_name) ?>"><?= htmlspecialchars($p2_name) ?></option> <option value="<?= htmlspecialchars($map['name']) ?>"><?= htmlspecialchars($map['name']) ?></option>
<?php endforeach; ?>
</select> </select>
</div> </div>
<div class="pf-form-group"> <div class="pf-form-group">
@@ -644,8 +678,9 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label"><?= tr('bud_adv_who_paid') ?></label> <label class="pf-label"><?= tr('bud_adv_who_paid') ?></label>
<select name="payer" id="edit_adv_payer" class="pf-input" required> <select name="payer" id="edit_adv_payer" class="pf-input" required>
<option value="<?= htmlspecialchars($p1_name) ?>"><?= htmlspecialchars($p1_name) ?></option> <?php foreach ($parentMapping as $map): ?>
<option value="<?= htmlspecialchars($p2_name) ?>"><?= htmlspecialchars($p2_name) ?></option> <option value="<?= htmlspecialchars($map['name']) ?>"><?= htmlspecialchars($map['name']) ?></option>
<?php endforeach; ?>
</select> </select>
</div> </div>
<div class="pf-form-group"> <div class="pf-form-group">
@@ -684,20 +719,6 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
<script> <script>
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR"; window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
// Dictionnaire I18N injecté proprement depuis PHP
window.I18N = {
...(window.I18N || {}),
'bud_prev_label_name': <?= json_encode(tr('bud_prev_label_name')) ?>,
'bud_prev_err_no_history': <?= json_encode(tr('bud_prev_err_no_history')) ?>,
'bud_prev_confirm_copy': <?= json_encode(tr('bud_prev_confirm_copy')) ?>,
'bud_prev_confirm_transfers': <?= json_encode(tr('bud_prev_confirm_transfers')) ?>,
'bud_prev_confirm_del_line': <?= json_encode(tr('bud_prev_confirm_del_line')) ?>,
'bud_adv_confirm_resolve': <?= json_encode(tr('bud_adv_confirm_resolve') ?? 'Confirmer le remboursement ?') ?>,
'bud_adv_confirm_delete': <?= json_encode(tr('bud_adv_confirm_delete') ?? 'Supprimer définitivement ?') ?>,
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>
};
window.CONFIG = window.CONFIG || {}; window.CONFIG = window.CONFIG || {};
window.CONFIG.parentMapping = <?= json_encode($parentMapping) ?>; window.CONFIG.parentMapping = <?= json_encode($parentMapping) ?>;
window.CONFIG.CURRENCY = '<?= defined('CURRENCY') ? CURRENCY : "€" ?>'; window.CONFIG.CURRENCY = '<?= defined('CURRENCY') ? CURRENCY : "€" ?>';
@@ -746,7 +767,7 @@ function duplicateMonth() {
const parentMap = window.CONFIG.parentMapping; const parentMap = window.CONFIG.parentMapping;
if (!sourceDateStr) { if (!sourceDateStr) {
alert(window.I18N['bud_prev_err_no_history']); alert(tr('bud_prev_err_no_history'));
return; return;
} }
@@ -757,7 +778,7 @@ function duplicateMonth() {
const sourceName = formatMonth(sourceDateStr); const sourceName = formatMonth(sourceDateStr);
const targetName = formatMonth(targetDateStr); const targetName = formatMonth(targetDateStr);
const message = window.I18N['bud_prev_confirm_copy'].replace('%s', sourceName).replace('%t', targetName); const message = tr('bud_prev_confirm_copy').replace('%s', sourceName).replace('%t', targetName);
if(!confirm(message)) return; if(!confirm(message)) return;
@@ -883,10 +904,9 @@ function updateSummaryTable() {
function saveData(action, data) { function saveData(action, data) {
const formData = new FormData(); const formData = new FormData();
formData.append('action', action); formData.append('action', action);
formData.append('ajax', '1'); // <-- Sécurisation pour pachaFetch / backend formData.append('ajax', '1');
for (const key in data) formData.append(key, data[key]); for (const key in data) formData.append(key, data[key]);
// On utilise fetch ici en mode aveugle pour ne pas bloquer l'UI lors de la frappe rapide
fetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData }); fetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
} }
@@ -894,7 +914,7 @@ async function validateTransfers(personCss, month) {
const parentMap = window.CONFIG.parentMapping.find(m => m.css === personCss); const parentMap = window.CONFIG.parentMapping.find(m => m.css === personCss);
if (!parentMap) return; if (!parentMap) return;
const msg = window.I18N['bud_prev_confirm_transfers'].replace('%p', parentMap.name).replace('%m', month); const msg = tr('bud_prev_confirm_transfers').replace('%p', parentMap.name).replace('%m', month);
if (!confirm(msg)) return; if (!confirm(msg)) return;
const formData = new FormData(); const formData = new FormData();
@@ -908,10 +928,10 @@ async function validateTransfers(personCss, month) {
if(result.success) { if(result.success) {
window.location.reload(); window.location.reload();
} else { } else {
alert(window.I18N['bud_err_tech'] + " : " + result.error); alert(tr('bud_err_tech') + " : " + result.error);
} }
} catch(e) { } catch(e) {
alert(window.I18N['bud_err_tech']); alert(tr('bud_err_tech'));
} }
} }
@@ -932,15 +952,15 @@ async function saveGenericNote(noteType, refId, content) {
setTimeout(() => indicator.style.opacity = '0', 2000); setTimeout(() => indicator.style.opacity = '0', 2000);
} }
} else { } else {
alert(window.I18N['bud_err_tech'] + " : " + data.error); alert(tr('bud_err_tech') + " : " + data.error);
} }
} catch(e) { } catch(e) {
alert(window.I18N['bud_err_tech']); alert(tr('bud_err_tech'));
} }
} }
async function deleteCategory(id) { async function deleteCategory(id) {
if (!confirm(window.I18N['bud_prev_confirm_del_line'])) return; if (!confirm(tr('bud_prev_confirm_del_line'))) return;
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'delete_category'); formData.append('action', 'delete_category');
formData.append('id', id); formData.append('id', id);
@@ -980,29 +1000,28 @@ async function handleAdvanceSubmit(event, form) {
const endpoint = form.getAttribute('action'); const endpoint = form.getAttribute('action');
const formData = new FormData(form); const formData = new FormData(form);
formData.append('ajax', '1'); // <-- Correction de l'erreur d'empty string formData.append('ajax', '1');
try { try {
// Utilisation robuste de pachaFetch
const result = await pachaFetch(endpoint, { method: 'POST', body: formData }); const result = await pachaFetch(endpoint, { method: 'POST', body: formData });
if (result.success) { if (result.success) {
window.location.reload(); window.location.reload();
} else { } else {
alert((window.I18N['bud_err_tech'] || "Erreur") + " : " + (result.error || "Opération échouée")); alert((tr('bud_err_tech') || "Erreur") + " : " + (result.error || "Opération échouée"));
btnSubmit.disabled = false; btnSubmit.disabled = false;
btnSubmit.innerHTML = oldText; btnSubmit.innerHTML = oldText;
} }
} catch (err) { } catch (err) {
console.error("AJAX Error:", err); console.error("AJAX Error:", err);
alert(window.I18N['bud_err_tech'] || "Une erreur technique est survenue."); alert(tr('bud_err_tech') || "Une erreur technique est survenue.");
btnSubmit.disabled = false; btnSubmit.disabled = false;
btnSubmit.innerHTML = oldText; btnSubmit.innerHTML = oldText;
} }
} }
async function executeResolveAdvance(id) { async function executeResolveAdvance(id) {
if (!confirm(window.I18N['bud_adv_confirm_resolve'])) return; if (!confirm(tr('bud_adv_confirm_resolve'))) return;
const fd = new FormData(); const fd = new FormData();
fd.append('action', 'resolve_advance'); fd.append('action', 'resolve_advance');
fd.append('id', id); fd.append('id', id);
@@ -1017,7 +1036,7 @@ async function executeResolveAdvance(id) {
} }
async function executeDeleteAdvance(id) { async function executeDeleteAdvance(id) {
if (!confirm(window.I18N['bud_adv_confirm_delete'])) return; if (!confirm(tr('bud_adv_confirm_delete'))) return;
const fd = new FormData(); const fd = new FormData();
fd.append('action', 'delete_advance'); fd.append('action', 'delete_advance');
fd.append('id', id); fd.append('id', id);
@@ -1087,7 +1106,6 @@ document.addEventListener('click', function(e) {
document.addEventListener('DOMContentLoaded', recalcAllAllocations); document.addEventListener('DOMContentLoaded', recalcAllAllocations);
// Soumission standard des modales de catégories avec pachaFetch
document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => { document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => {
form.addEventListener('submit', async (e) => { form.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
@@ -1097,7 +1115,7 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
submitBtn.disabled = true; submitBtn.disabled = true;
submitBtn.innerText = '⏳ ...'; submitBtn.innerText = '⏳ ...';
const formData = new FormData(form); const formData = new FormData(form);
formData.append('ajax', '1'); // Toujours sécuriser formData.append('ajax', '1');
const actionUrl = form.getAttribute('action'); const actionUrl = form.getAttribute('action');
const result = await pachaFetch(actionUrl, { method: 'POST', body: formData }); const result = await pachaFetch(actionUrl, { method: 'POST', body: formData });
@@ -1107,10 +1125,10 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
document.body.classList.remove('no-scroll'); document.body.classList.remove('no-scroll');
window.location.reload(); window.location.reload();
} else { } else {
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue")); alert((tr('bud_err_tech') || 'Erreur') + " : " + (result.error || "Inconnue"));
} }
} catch (error) { } catch (error) {
alert(window.I18N['bud_err_tech'] || "Une erreur technique est survenue."); alert(tr('bud_err_tech') || "Une erreur technique est survenue.");
} finally { } finally {
submitBtn.disabled = false; submitBtn.disabled = false;
submitBtn.innerText = originalText; submitBtn.innerText = originalText;
+484
View File
@@ -0,0 +1,484 @@
<?php
// Sécurité : Ce fichier ne doit pas être appelé directement
if (!defined('CURRENCY')) {
define('CURRENCY', '€');
}
?>
<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') ?>
<?php if (!$hasAccounts): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
</button>
<button class="bs-tab-btn" onclick="switchBsTab('categories', this)">
<?= tr('bs_tab_categories') ?>
<?php if (!$hasCategories): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
</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') ?>
<?php if (!$hasSalaries): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
</button>
<button class="bs-tab-btn" onclick="switchBsTab('csv', this)"><?= tr('bs_tab_csv') ?></button>
</div>
<div class="bs-content">
<?php if (!isset($isBudgetSetupOk) || !$isBudgetSetupOk || !$hasBudgetItems): ?>
<div class="bs-onboarding-card">
<h4 class="bs-onboarding-title">
<span>🚀</span> <?= tr('bs_onb_title') ?>
</h4>
<p class="bs-onboarding-desc"><?= tr('bs_onb_desc') ?></p>
<ul class="bs-onboarding-list">
<li>
<strong class="<?= $hasCategories ? 'text-success' : 'text-danger' ?>"><?= $hasCategories ? '✅' : '❌' ?></strong>
<a class="bs-onboarding-link" onclick="switchBsTab('categories', document.querySelectorAll('.bs-tab-btn')[1]); return false;"><?= tr('bs_onb_cat_link') ?></a> : <?= tr('bs_onb_cat_desc') ?>
</li>
<li>
<strong class="<?= $hasAccounts ? 'text-success' : 'text-danger' ?>"><?= $hasAccounts ? '✅' : '❌' ?></strong>
<a class="bs-onboarding-link" onclick="switchBsTab('accounts', document.querySelectorAll('.bs-tab-btn')[0]); return false;"><?= tr('bs_onb_acc_link') ?></a> : <?= tr('bs_onb_acc_desc') ?>
</li>
<li>
<strong class="<?= $hasSalaries ? 'text-success' : 'text-danger' ?>"><?= $hasSalaries ? '✅' : '❌' ?></strong>
<a class="bs-onboarding-link" onclick="switchBsTab('salaries', document.querySelectorAll('.bs-tab-btn')[3]); return false;"><?= tr('bs_onb_sal_link') ?></a> : <?= tr('bs_onb_sal_desc') ?>
</li>
<li style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed rgba(180, 83, 9, 0.3);">
<strong class="<?= $hasBudgetItems ? 'text-success' : 'text-warning' ?>" style="color: <?= $hasBudgetItems ? '' : '#f59e0b' ?>"><?= $hasBudgetItems ? '✅' : '⏳' ?></strong>
<a href="?tab=recap" onclick="closeBudgetSettings();" class="bs-onboarding-link"><?= tr('bs_onb_items_link') ?></a> : <?= tr('bs_onb_items_desc') ?>
</li>
</ul>
</div>
<?php endif; ?>
<div id="pane-accounts" class="bs-pane active">
<div class="bs-pane-header">
<h4 class="bs-section-title"><?= tr('bs_tab_accounts') ?></h4>
<div class="bs-currency-selector">
<span><?= tr('bs_currency_label') ?> :</span>
<select id="bs-currency-select" class="pf-input bs-currency-select" 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" class="bs-list-container">⏳ <?= tr('loading') ?></div>
<hr class="bs-divider">
<h5 class="bs-section-subtitle"><?= tr('bs_add_account_title') ?></h5>
<form id="form-add-account" class="bs-form-inline">
<input type="text" name="name" class="pf-input bs-input-flex" placeholder="<?= tr('bs_account_name_ph') ?>" required>
<select name="type" class="pf-input bs-input-fixed">
<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" class="bs-list-container">⏳ <?= tr('loading') ?></div>
<hr class="bs-divider">
<h5 class="bs-section-subtitle"><?= tr('bs_add_category_title') ?></h5>
<form id="form-add-category" class="bs-form-inline align-center">
<input type="text" name="code" class="pf-input bs-input-fixed" placeholder="<?= tr('bs_cat_code_ph') ?>" required>
<input type="text" name="label" class="pf-input bs-input-flex" placeholder="<?= tr('bs_cat_label_ph') ?>" required>
<select name="type" class="pf-input bs-input-fixed">
<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 bs-input-icon">
<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" class="bs-input-color">
<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="<?= tr('bs_rule_search_ph') ?>" onkeyup="filterRules()">
<div id="bs-list-rules" class="bs-list-container">⏳ <?= tr('loading') ?></div>
<hr class="bs-divider">
<h5 class="bs-section-subtitle"><?= tr('bs_add_rule_title') ?></h5>
<form id="form-add-rule" class="bs-form-inline align-center">
<input type="text" name="keyword" class="pf-input bs-input-flex" placeholder="<?= tr('bs_rule_keyword_ph') ?>" required>
<select name="category" id="select-rule-category" class="pf-input bs-input-fixed" required></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" class="bs-list-container">⏳ <?= tr('loading') ?></div>
</div>
<div id="pane-csv" class="bs-pane">
<h4 class="bs-section-title"><?= tr('bs_csv_title') ?></h4>
<p class="pf-muted-tiny bs-desc"><?= tr('bs_csv_desc') ?></p>
<div id="csv-drop-zone" class="bs-csv-dropzone">
<div class="bs-csv-icon">📥</div>
<h5 class="bs-csv-title"><?= tr('bs_csv_drop_title') ?></h5>
<p class="pf-muted-tiny bs-csv-subtitle"><?= tr('bs_csv_drop_desc') ?></p>
<input type="file" id="csv_file_input" accept=".csv" style="display: none;">
</div>
<div id="csv-preview-container" class="bs-csv-preview"></div>
<h5 class="bs-section-subtitle bordered"><?= tr('bs_csv_settings_title') ?></h5>
<form id="form-csv-mapping" onsubmit="saveCsvMapping(event)">
<div class="bs-grid-2">
<div>
<label class="pf-label"><?= tr('bs_csv_delimiter') ?></label>
<select name="csv_delimiter" id="csv_delimiter" class="pf-input">
<option value=";"><?= tr('bs_csv_delim_semi') ?></option>
<option value=","><?= tr('bs_csv_delim_comma') ?></option>
<option value="\t"><?= tr('bs_csv_delim_tab') ?></option>
</select>
</div>
<div>
<label class="pf-label"><?= tr('bs_csv_date_format') ?></label>
<select name="csv_date_format" id="csv_date_format" class="pf-input">
<option value="d/m/Y">JJ/MM/AAAA</option>
<option value="Y-m-d">AAAA-MM-JJ</option>
</select>
</div>
</div>
<h5 class="bs-section-subtitle">
<?= tr('bs_csv_col_index') ?>
<small class="bs-text-muted-normal"><?= tr('bs_csv_col_index_help') ?></small>
</h5>
<div class="bs-grid-2">
<div>
<label class="pf-label"><?= tr('bs_csv_col_date') ?></label>
<input type="number" min="0" name="csv_col_date" id="csv_col_date" class="pf-input" value="0" required>
</div>
<div>
<label class="pf-label"><?= tr('bs_csv_col_label') ?></label>
<input type="number" min="0" name="csv_col_label" id="csv_col_label" class="pf-input" value="1" required>
</div>
</div>
<div class="bs-card-gray">
<label class="pf-label"><?= tr('bs_csv_amount_mgmt') ?></label>
<select name="csv_amount_type" id="csv_amount_type" class="pf-input bs-input-mb" onchange="toggleCsvAmountCols(this.value)">
<option value="single"><?= tr('bs_csv_amount_single') ?></option>
<option value="split"><?= tr('bs_csv_amount_split') ?></option>
</select>
<div class="bs-grid-2">
<div>
<label class="pf-label" id="lbl_col_debit"><?= tr('bs_csv_col_amount') ?></label>
<input type="number" min="0" name="csv_col_debit" id="csv_col_debit" class="pf-input" value="8" required>
</div>
<div id="wrapper_col_credit" style="display:none;">
<label class="pf-label"><?= tr('bs_csv_col_credit') ?></label>
<input type="number" min="0" name="csv_col_credit" id="csv_col_credit" class="pf-input" value="9">
</div>
</div>
</div>
<div class="bs-input-mb">
<label class="pf-label"><?= tr('bs_csv_col_ref') ?></label>
<input type="number" min="0" name="csv_col_ref" id="csv_col_ref" class="pf-input" value="3">
</div>
<div class="bs-text-right">
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save_format') ?></button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tabsContainer = document.querySelector('.budget-tabs-container');
const activeTab = document.querySelector('.tab-item.active');
if (tabsContainer && activeTab) {
if (tabsContainer.scrollWidth > tabsContainer.clientWidth) {
const scrollPos = activeTab.offsetLeft - (tabsContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2);
tabsContainer.scrollTo({ left: scrollPos, behavior: 'smooth' });
}
}
});
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 || 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 bs-input-mb">❌ ${tr('error_loading_settings')} : ${err.message}</div>`;
['accounts', 'categories', 'rules', 'salaries'].forEach(id => {
const el = document.getElementById('bs-list-' + id);
if (el) el.innerHTML = errorMsg;
});
}
}
// 1. ONGLET : COMPTES
function renderAccounts(accounts) {
const container = document.getElementById('bs-list-accounts');
if (!container) return;
if (!accounts || accounts.length === 0) {
container.innerHTML = `<em class="text-muted">${tr('bs_empty_accounts')}</em>`;
return;
}
let html = '';
accounts.forEach(acc => {
const typeBadge = acc.account_type === 'checking' ? tr('bs_type_checking') : tr('bs_type_savings');
const defaultBadge = acc.is_default == 1 ? `<span class="bs-badge-default">${tr('bs_badge_default')}</span>` : '';
const ownerText = acc.owner_name ? `${tr('bs_owner')} : ${acc.owner_name}` : tr('bs_shared_account');
html += `
<div class="bs-list-item">
<div>
<strong>${acc.name}</strong> ${defaultBadge}<br>
<small class="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="${tr('btn_delete')}">🗑️</button>
</div>
</div>`;
});
container.innerHTML = html;
}
// 2. ONGLET : CATÉGORIES
function renderCategories(categories) {
const container = document.getElementById('bs-list-categories');
if (!container) return;
if (!categories || categories.length === 0) {
container.innerHTML = `<em class="text-muted">${tr('bs_empty_categories')}</em>`;
return;
}
let html = '';
categories.forEach(cat => {
let typeLabel = cat.type === 'Income' ? tr('bs_type_income') : cat.type === 'Expense' ? tr('bs_type_expense') : tr('bs_type_savings_cat');
html += `
<div class="bs-list-item" style="border-left: 4px solid ${cat.color || '#ccc'};">
<div>
<span style="font-size:1.2rem; margin-right:8px;">${cat.icon || '📌'}</span>
<strong>${cat.label}</strong> <small class="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="${tr('btn_delete')}">🗑️</button>
</div>
</div>`;
});
container.innerHTML = html;
}
// 3. ONGLET : RÈGLES D'IMPORT
function renderRules(rules) {
const container = document.getElementById('bs-list-rules');
if (!container) return;
if (!rules || rules.length === 0) {
container.innerHTML = `<em class="text-muted">${tr('bs_empty_rules')}</em>`;
return;
}
const groupedRules = {};
rules.forEach(r => {
const catName = r.cat_label || r.category;
if (!groupedRules[catName]) groupedRules[catName] = [];
groupedRules[catName].push(r);
});
let html = '';
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 bs-accordion-summary">
<span>📁 ${catName}</span>
<span class="bs-accordion-count">${catRules.length}</span>
</summary>
<div class="pf-accordion-content" style="padding: 0.5rem;">
<div class="bs-rule-tags">`;
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="${tr('btn_delete')}">×</button>
</div>`;
});
html += ` </div>
</div>
</details>`;
}
container.innerHTML = html;
}
function filterRules() {
const searchVal = document.getElementById('input-search-rules').value.toLowerCase();
document.querySelectorAll('.js-rule-group').forEach(group => {
let hasVisibleTag = false;
const catName = group.getAttribute('data-catname');
group.querySelectorAll('.js-rule-tag').forEach(tag => {
const keyword = tag.getAttribute('data-keyword');
if (keyword.includes(searchVal)) { tag.style.display = 'inline-flex'; hasVisibleTag = true; }
else { tag.style.display = 'none'; }
});
if (catName.includes(searchVal) && searchVal.length > 0) {
group.querySelectorAll('.js-rule-tag').forEach(tag => tag.style.display = 'inline-flex');
hasVisibleTag = true;
}
if (hasVisibleTag) {
group.style.display = 'block';
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 class="text-muted">${tr('bs_empty_salaries')} ${year}.</em>`;
return;
}
const currency = '<?= CURRENCY ?>';
let html = `<p class="bs-desc">${tr('bs_salaries_year_desc')} <strong>${year}</strong>.</p>`;
salaries.forEach(s => {
html += `
<div class="bs-list-item" id="salary-item-${s.id}">
<div>
<strong>${s.person}</strong><br>
<small class="text-muted">
${tr('bs_salary_net')} : ${s.salary} ${currency}<br>
${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="${tr('btn_edit')}">✏️</button>
</div>
</div>`;
});
container.innerHTML = html;
}
// --- CRUD ACTIONS ---
async function deleteAccount(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete'))) return; try { const fd = new FormData(); fd.append('action', 'delete_account'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); 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'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddAccount); fd.append('action', 'add_account'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddAccount.reset(); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
async function deleteCategory(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete_cat'))) return; try { const fd = new FormData(); fd.append('action', 'delete_category'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); 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'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddCategory); fd.append('action', 'add_category'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddCategory.reset(); formAddCategory.querySelector('input[type="color"]').value = "#3b82f6"; await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
function populateRuleCategories(categories) { const select = document.getElementById('select-rule-category'); if (!select) return; select.innerHTML = ''; if (!categories) return; categories.forEach(cat => { const opt = document.createElement('option'); opt.value = cat.code; opt.textContent = `${cat.icon || ''} ${cat.label}`; select.appendChild(opt); }); }
async function deleteRule(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete_rule'))) return; try { const fd = new FormData(); fd.append('action', 'delete_rule'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
const formAddRule = document.getElementById('form-add-rule');
if (formAddRule) formAddRule.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddRule.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddRule); fd.append('action', 'add_rule'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddRule.reset(); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
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 ?>';
document.getElementById(`salary-item-${id}`).innerHTML = `
<form onsubmit="submitInlineSalary(event, ${id})" class="bs-form-inline align-center">
<div class="bs-input-flex"><strong>${person}</strong></div>
<div style="display:flex; align-items:center; gap:4px;"><small class="text-muted">${tr('bs_salary_net')}:</small><input type="number" step="0.01" name="salary" class="pf-input bs-input-fixed" value="${salary}" required> ${currency}</div>
<div style="display:flex; align-items:center; gap:4px;"><small class="text-muted">${tr('bs_salary_fees')}:</small><input type="number" step="0.01" name="mensualite" class="pf-input bs-input-fixed" value="${mensualite}" required> ${currency}</div>
<button type="submit" class="btn btn-secondary">💾</button>
<button type="button" class="btn btn-ghost" onclick="loadBudgetSettingsData()">❌</button>
</form>`;
}
async function submitInlineSalary(event, id) { event.preventDefault(); const form = event.target; const btn = form.querySelector('button'); btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(form); fd.append('action', 'save_salary'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); btn.innerText = '💾'; btn.disabled = false; } }
async function updateBudgetCurrency(selectEl) { try { const fd = new FormData(); fd.append('action', 'save_currency'); fd.append('currency', selectEl.value); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); if (typeof showToast === 'function') { showToast(`${tr('bs_currency_updated')} ✅`); } await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
// CSV
function toggleCsvAmountCols(type) { if (type === 'split') { document.getElementById('wrapper_col_credit').style.display = 'block'; document.getElementById('lbl_col_debit').innerText = tr('bs_csv_col_debit'); } else { document.getElementById('wrapper_col_credit').style.display = 'none'; document.getElementById('lbl_col_debit').innerText = tr('bs_csv_col_amount'); } }
function renderCsvMapping(mapping) { if (!mapping) return; document.getElementById('csv_delimiter').value = mapping.delimiter || ';'; document.getElementById('csv_date_format').value = mapping.date_format || 'd/m/Y'; document.getElementById('csv_col_date').value = mapping.col_date ?? 0; document.getElementById('csv_col_label').value = mapping.col_label ?? 1; document.getElementById('csv_col_ref').value = mapping.col_ref ?? 3; const amountType = mapping.amount_type || 'split'; document.getElementById('csv_amount_type').value = amountType; document.getElementById('csv_col_debit').value = mapping.col_debit ?? 8; document.getElementById('csv_col_credit').value = mapping.col_credit ?? 9; toggleCsvAmountCols(amountType); }
async function saveCsvMapping(e) { e.preventDefault(); const form = e.target; const btn = form.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳...'; btn.disabled = true; try { const fd = new FormData(form); fd.append('action', 'save_csv_mapping'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); if (typeof showToast === 'function') showToast(tr('bs_csv_saved')); else alert(tr('bs_csv_saved')); } catch (err) { alert(tr('error_occured') + " : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } }
const dropZone = document.getElementById('csv-drop-zone'); const fileInput = document.getElementById('csv_file_input');
if (dropZone && fileInput) {
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.backgroundColor = 'rgba(0, 123, 255, 0.05)'; dropZone.style.borderColor = 'var(--primary-color, #007bff)'; });
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.style.backgroundColor = ''; dropZone.style.borderColor = 'var(--border-light, #ccc)'; });
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.style.backgroundColor = ''; dropZone.style.borderColor = 'var(--border-light, #ccc)'; if (e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; handleCsvUpload(e.dataTransfer.files[0]); } });
fileInput.addEventListener('change', function() { if (this.files.length) { handleCsvUpload(this.files[0]); } });
}
function handleCsvUpload(file) {
if (!file.name.endsWith('.csv')) { alert(tr('bs_csv_err_type')); return; }
if (typeof showToast === 'function') { showToast(`${tr('bs_csv_file_ok')} : ${file.name}`); }
const reader = new FileReader();
reader.onload = function(e) {
const text = e.target.result; const delimiter = document.getElementById('csv_delimiter').value || ';';
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
if (lines.length > 0) {
const headers = lines[0].split(delimiter).map(h => h.replace(/^"|"$/g, '').trim());
const sampleRows = [];
for (let i = 1; i < Math.min(lines.length, 3); i++) { sampleRows.push(lines[i].split(delimiter).map(d => d.replace(/^"|"$/g, '').trim())); }
renderCsvTablePreview(headers, sampleRows);
}
};
reader.readAsText(file, 'ISO-8859-1');
}
function renderCsvTablePreview(headers, sampleRows) {
const container = document.getElementById('csv-preview-container');
let html = `<h5 class="bs-section-subtitle">👁️ ${tr('bs_csv_preview_title')}</h5>`;
html += `<div class="bs-csv-table-wrapper"><table class="bs-csv-table"><thead class="bs-csv-thead"><tr>`;
headers.forEach((header, index) => {
const cleanHeader = header || `(${tr('bs_csv_empty_col')})`;
html += `<th class="bs-csv-th"><div class="bs-csv-th-index">N° ${index}</div><div>${cleanHeader}</div></th>`;
});
html += `</tr></thead><tbody>`;
if (sampleRows.length === 0) { html += `<tr><td colspan="${headers.length}" class="bs-csv-td-empty">${tr('bs_csv_no_data')}</td></tr>`; }
else { sampleRows.forEach(row => { html += `<tr class="bs-csv-tr">`; for (let i = 0; i < headers.length; i++) { const cellData = row[i] !== undefined ? row[i] : ''; const displayData = cellData.length > 40 ? cellData.substring(0, 40) + '...' : cellData; html += `<td class="bs-csv-td">${displayData}</td>`; } html += `</tr>`; }); }
html += `</tbody></table></div>`;
container.innerHTML = html; container.style.display = 'block';
}
</script>
+316 -190
View File
@@ -13,7 +13,7 @@ let localCareModes = [];
async function openCalendarSettings() { async function openCalendarSettings() {
try { try {
const res = await pachaFetch( const res = await pachaFetch(
"/modules/family-calendar/includes/api/settings.php?action=get_all", "/modules/family-calendar/includes/api/calendar-settings.php?action=get_all",
); );
if (!res.success) throw new Error(res.error); if (!res.success) throw new Error(res.error);
@@ -63,29 +63,34 @@ function closeCalendarSettings() {
document.body.classList.remove("no-scroll"); document.body.classList.remove("no-scroll");
} }
function switchCalendarTab(tabName) { function switchCalendarTab(tabId) {
document // 1. On réinitialise l'affichage
.querySelectorAll(".cal-settings-pane")
.forEach((p) => (p.style.display = "none"));
document document
.querySelectorAll(".bs-tab-btn") .querySelectorAll(".bs-tab-btn")
.forEach((b) => b.classList.remove("active")); .forEach((btn) => btn.classList.remove("active"));
document
.querySelectorAll(".cal-settings-pane")
.forEach((pane) => (pane.style.display = "none"));
if (tabName === "foyer") { // 2. On active l'onglet cliqué
document.getElementById("cal-pane-foyer").style.display = "block"; document.getElementById(`tab-btn-${tabId}`).classList.add("active");
document.getElementById("tab-btn-foyer").classList.add("active"); document.getElementById(`cal-pane-${tabId}`).style.display = "block";
} else if (tabName === "affichage") {
document.getElementById("cal-pane-affichage").style.display = "block"; // 3. Actions spécifiques
document.getElementById("tab-btn-affichage").classList.add("active"); if (tabId === "foyer") {
} else { loadLeaveCatalog();
document.getElementById("cal-pane-membres").style.display = "block"; } else if (tabId === "membres") {
document.getElementById("tab-btn-membres").classList.add("active"); // On charge la vue du membre sélectionné par défaut dans la liste
loadMemberConfigView(); if (document.getElementById("selectCalMember").value) {
loadMemberConfigView();
}
} }
} }
function renderCareModeTags() { function renderCareModeTags() {
const container = document.getElementById("careModesContainer"); const container = document.getElementById("careModesContainer");
if (!container) return; // 🛑 LA BARRIÈRE DE SÉCURITÉ EST ICI
container.innerHTML = localCareModes container.innerHTML = localCareModes
.map( .map(
(mode, i) => ` (mode, i) => `
@@ -97,8 +102,11 @@ function renderCareModeTags() {
.join(""); .join("");
} }
// Ajoutons la même sécurité pour l'ajout au cas où :
function addCareModeTag() { function addCareModeTag() {
const input = document.getElementById("inputNewCareMode"); const input = document.getElementById("inputNewCareMode");
if (!input) return; // 🛑 SÉCURITÉ
const val = input.value.trim(); const val = input.value.trim();
if (val && !localCareModes.includes(val)) { if (val && !localCareModes.includes(val)) {
localCareModes.push(val); localCareModes.push(val);
@@ -124,7 +132,7 @@ async function submitCalFoyer(e) {
formData.append("care_modes", JSON.stringify(localCareModes)); formData.append("care_modes", JSON.stringify(localCareModes));
const res = await pachaFetch( const res = await pachaFetch(
"/modules/family-calendar/includes/api/settings.php", "/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: formData }, { method: "POST", body: formData },
); );
if (!res.success) throw new Error(res.error); if (!res.success) throw new Error(res.error);
@@ -142,139 +150,195 @@ async function submitCalFoyer(e) {
} }
} }
window.addLeaveRowToMatrix = function () { window.submitChildCareModes = async function () {
const tbody = document.getElementById("tbodyLeaveMatrix"); if (!window.currentSelectedMemberId) return;
const empty = document.getElementById("emptyMatrixRow"); const checkboxes = document.querySelectorAll(".js-care-mode-cb:checked");
if (empty) empty.remove(); const selectedModes = Array.from(checkboxes).map((cb) => cb.value);
if (!tbody) return; try {
const formData = new FormData();
formData.append("action", "save_child_care_modes");
formData.append("person_id", window.currentSelectedMemberId);
formData.append("care_modes", JSON.stringify(selectedModes));
const rowTypeOptions = (calGlobalData.leave_types || []) const res = await pachaFetch(
.map((lt) => `<option value="${lt.code}">${lt.code} - ${lt.label}</option>`) "/modules/family-calendar/includes/api/calendar-settings.php",
.join(""); { method: "POST", body: formData },
);
if (!res.success) throw new Error(res.error);
if (window.showToast)
showToast(tr("fc_child_saved") || "Sauvegardé !", "success");
else alert(tr("fc_child_saved") || "Sauvegardé !");
const trRow = document.createElement("tr"); const currentPerson = calGlobalData.people.find(
trRow.className = "js-leave-row"; (k) => parseInt(k.id) === window.currentSelectedMemberId,
trRow.style.borderBottom = "1px solid var(--border-light)"; );
trRow.innerHTML = ` if (currentPerson) {
<td style="padding: 6px 2px;"> currentPerson.care_modes = JSON.stringify(selectedModes);
<select class="pf-input js-leave-type" required style="width:100%; padding:6px 2px; font-size:0.8rem; box-sizing: border-box;"> }
<option value="">${tr("fc_leave_code_placeholder") || "Code..."}</option> } catch (err) {
${rowTypeOptions} alert("Erreur: " + err.message);
</select> }
</td>
<td style="padding: 6px 2px;">
<select class="pf-input js-leave-method" style="width:100%; padding:6px 2px; font-size:0.8rem; box-sizing: border-box;">
<option value="FIXED">${tr("fc_leave_method_fixed") || "Fixe"}</option>
<option value="ACCUMULATED">${tr("fc_leave_method_accumulated") || "Cumul"}</option>
</select>
</td>
<td style="padding: 6px 2px;"><input type="number" step="0.5" class="pf-input js-leave-allowance" placeholder="0" required style="width:100%; padding:6px 4px; font-size:0.85rem; text-align:center; box-sizing: border-box;"></td>
<td style="padding: 6px 2px;"><input type="text" class="pf-input js-leave-date" placeholder="${tr("fc_date_format_placeholder") || "JJ/MM"}" pattern="(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[012])" maxlength="5" required style="width:100%; padding:6px 4px; font-size:0.85rem; text-align:center; box-sizing: border-box;" title="${tr("fc_date_format_title") || "Format JJ/MM"}"></td>
<td style="padding: 6px 2px; text-align:right;"><button type="button" class="btn-icon-action delete" style="padding:4px; margin:0;" onclick="this.closest('tr').remove()" title="${tr("btn_delete") || "Supprimer"}">🗑️</button></td>
`;
tbody.appendChild(trRow);
}; };
window.populateCalendarSettings = function (settings) { // ==========================================
if (!settings) return; // NOUVEAU CATALOGUE GLOBAL (FOYER)
const viewSelect = document.getElementById("setCalView"); // ==========================================
if (viewSelect && settings.calendar_default_view) let globalLeaveCatalog = [];
viewSelect.value = settings.calendar_default_view;
const firstDaySelect = document.getElementById("setCalFirstDay");
if (firstDaySelect && settings.calendar_first_day !== undefined)
firstDaySelect.value = settings.calendar_first_day;
const hoursInput = document.getElementById("setCalHours");
if (hoursInput && settings.calendar_working_hours)
hoursInput.value = settings.calendar_working_hours;
};
window.loadAdultConfigView = function (memberId, zoneElement) { async function loadLeaveCatalog() {
let userLeaves = calGlobalData.leaves[memberId] || []; try {
const res = await pachaFetch(
"/modules/family-calendar/includes/api/calendar-settings.php?action=get_leave_types",
);
globalLeaveCatalog = res.data || [];
renderLeaveCatalog(globalLeaveCatalog);
} catch (err) {
console.error("Erreur chargement catalogue", err);
}
}
const tableRows = userLeaves function renderLeaveCatalog(types) {
.map((l) => { const container = document.getElementById("leaveTypesContainer");
let dateDisp = ""; if (!container) return;
if (l.date) { container.innerHTML = "";
const parts = l.date.split("-");
if (parts.length >= 3) dateDisp = `${parts[2]}/${parts[1]}`;
}
let rowTypeOptions = (calGlobalData.leave_types || []) if (types.length === 0) {
.map( container.innerHTML = `<p class="pf-muted-note">Aucun congé configuré.</p>`;
(lt) => return;
`<option value="${lt.code}" ${lt.code === l.type ? "selected" : ""}>${lt.code} - ${lt.label}</option>`, }
)
.join("");
if (!calGlobalData.leave_types.some((lt) => lt.code === l.type)) { types.forEach((lt) => {
rowTypeOptions += `<option value="${l.type}" selected>${l.type} (${tr("fc_leave_obsolete") || "Obsolète"})</option>`; container.innerHTML += `
} <div style="display: flex; justify-content: space-between; align-items: center; background: var(--bg-page); padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border-light); margin-bottom: 6px;">
<div>
return ` <span style="font-weight: bold; color: var(--text-main);">${lt.label}</span>
<tr class="js-leave-row" style="border-bottom: 1px solid var(--border-light);"> <span style="background: var(--bg-subtle); border: 1px solid var(--border-main); border-radius: 4px; padding: 2px 6px; font-size: 0.8rem; margin-left: 8px; color: var(--text-muted);">${lt.code}</span>
<td style="padding: 6px 2px;"> </div>
<select class="pf-input js-leave-type" required style="width:100%; padding:6px 2px; font-size:0.8rem; box-sizing: border-box;"> <div style="display: flex; gap: 4px;">
<option value="">${tr("fc_leave_code_placeholder") || "Code..."}</option> <button type="button" class="pf-btn btn-secondary" style="padding: 4px 8px;" onclick="editLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')">✏️</button>
${rowTypeOptions} <button type="button" class="pf-btn btn-secondary" style="padding: 4px 8px; color: var(--danger);" onclick="deleteLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')">🗑️</button>
</select> </div>
</td>
<td style="padding: 6px 2px;">
<select class="pf-input js-leave-method" style="width:100%; padding:6px 2px; font-size:0.8rem; box-sizing: border-box;">
<option value="FIXED" ${l.method === "FIXED" ? "selected" : ""}>${tr("fc_leave_method_fixed") || "Fixe"}</option>
<option value="ACCUMULATED" ${l.method === "ACCUMULATED" ? "selected" : ""}>${tr("fc_leave_method_accumulated") || "Cumul"}</option>
</select>
</td>
<td style="padding: 6px 2px;"><input type="number" step="0.5" class="pf-input js-leave-allowance" value="${l.allowance || 0}" placeholder="0" required style="width:100%; padding:6px 4px; font-size:0.85rem; text-align:center; box-sizing: border-box;"></td>
<td style="padding: 6px 2px;"><input type="text" class="pf-input js-leave-date" value="${dateDisp}" placeholder="${tr("fc_date_format_placeholder") || "JJ/MM"}" pattern="(0[1-9]|[12][0-9]|3[01])/(0[1-9]|1[012])" maxlength="5" required style="width:100%; padding:6px 4px; font-size:0.85rem; text-align:center; box-sizing: border-box;" title="${tr("fc_date_format_title") || "Format JJ/MM"}"></td>
<td style="padding: 6px 2px; text-align:right;"><button type="button" class="btn-icon-action delete" style="padding:4px; margin:0;" onclick="this.closest('tr').remove()" title="${tr("btn_delete") || "Supprimer"}">🗑️</button></td>
</tr>
`;
})
.join("");
zoneElement.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:12px;">
<div>
<h5 style="margin:0 0 4px 0; color:var(--text-main); font-size:1rem;">🗓️ ${tr("fc_matrix_title") || "Matrice des Congés"}</h5>
<p style="font-size:0.8rem; color:var(--text-muted); margin:0; line-height:1.3;">${tr("fc_matrix_desc") || "Définissez les quotas et la date de renouvellement."}</p>
</div> </div>
<button type="button" class="pf-btn pf-btn-secondary" style="padding:6px 10px; font-size:0.8rem; white-space:nowrap;" onclick="addLeaveRowToMatrix()"> ${tr("btn_add") || "Ajouter"}</button> `;
</div> });
<div style="overflow-x:hidden; margin: 0 -5px;"> }
<table style="width:100%; border-collapse:collapse; margin-bottom:15px; table-layout: fixed;">
<thead>
<tr style="font-size:0.75rem; text-align:left; color:var(--text-muted); text-transform:uppercase;">
<th style="padding:0 2px 8px; width: 33%;">${tr("fc_table_code") || "Code"}</th>
<th style="padding:0 2px 8px; width: 22%;">${tr("fc_table_method") || "Méthode"}</th>
<th style="padding:0 2px 8px; width: 15%;">${tr("fc_table_quota") || "Quota"}</th>
<th style="padding:0 2px 8px; width: 20%;">${tr("fc_table_renewal") || "Renouv."}</th>
<th style="padding:0 2px 8px; width: 10%;"></th>
</tr>
</thead>
<tbody id="tbodyLeaveMatrix">
${tableRows || `<tr><td colspan="5" id="emptyMatrixRow" style="text-align:center; padding:20px; color:var(--text-muted); font-style:italic; font-size:0.9rem;">${tr("fc_matrix_empty") || "Aucun compteur défini."}</td></tr>`}
</tbody>
</table>
</div>
<div style="display:flex; justify-content:flex-end; border-top:1px solid var(--border-light); padding-top:12px;">
<button type="button" class="pf-btn pf-btn-primary" style="padding:8px 16px;" onclick="submitMemberLeaves()">${tr("btn_save") || "Enregistrer"}</button>
</div>
`;
};
window.loadMemberConfigView = function () { function editLeaveType(code, label) {
document.getElementById("leaveTypeFormTitle").innerText =
"✏️ Modifier : " + label;
document.getElementById("lt-mode").value = "edit";
const codeInput = document.getElementById("lt-code");
codeInput.value = code;
codeInput.readOnly = true;
codeInput.style.backgroundColor = "var(--bg-subtle)";
codeInput.style.opacity = "0.6";
codeInput.style.cursor = "not-allowed";
document.getElementById("lt-code-note").innerText = "Non modifiable";
document.getElementById("lt-label").value = label;
}
function resetLeaveTypeForm() {
document.getElementById("leaveTypeFormTitle").innerText = "+ Ajouter";
document.getElementById("lt-mode").value = "add";
const codeInput = document.getElementById("lt-code");
codeInput.value = "";
codeInput.readOnly = false;
codeInput.style.backgroundColor = "";
codeInput.style.opacity = "1";
codeInput.style.cursor = "text";
document.getElementById("lt-code-note").innerText = "Irréversible";
document.getElementById("lt-label").value = "";
}
// NOUVELLE FONCTION DE SUPPRESSION
async function deleteLeaveType(code, label) {
if (
!confirm(
`Supprimer définitivement le congé "${label}" du catalogue ? Cela le retirera également des membres qui l'utilisent.`,
)
)
return;
const fd = new FormData();
fd.append("action", "delete_leave_type");
fd.append("code", code);
try {
const res = await pachaFetch(
"/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: fd },
);
if (!res.success) throw new Error(res.error);
if (window.showToast) showToast("Congé supprimé avec succès !", "success");
resetLeaveTypeForm();
loadLeaveCatalog();
// On recharge l'onglet membre au cas où le membre affiché utilisait ce congé
if (document.getElementById("selectCalMember").value) {
loadMemberConfigView();
}
} catch (err) {
alert("Erreur de suppression : " + err.message);
}
}
async function saveLeaveType() {
const code = document.getElementById("lt-code").value.toUpperCase();
const label = document.getElementById("lt-label").value;
if (!code || !label)
return window.showToast
? showToast("Code et Label requis", "error")
: alert("Code et Label requis");
const fd = new FormData();
fd.append("action", "save_leave_type");
fd.append("mode", document.getElementById("lt-mode").value);
fd.append("code", code);
fd.append("label", label);
try {
const res = await pachaFetch(
"/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: fd },
);
if (!res.success) throw new Error(res.error);
if (window.showToast) showToast("Catalogue mis à jour !", "success");
resetLeaveTypeForm();
loadLeaveCatalog();
} catch (err) {
alert("Erreur d'enregistrement : " + err.message);
}
}
// ==========================================
// MODALE SETTINGS : AFFECTATION (MEMBRES)
// ==========================================
// Quand on choisit un membre dans la liste déroulante
async function loadMemberConfigView() {
const select = document.getElementById("selectCalMember"); const select = document.getElementById("selectCalMember");
const zone = document.getElementById("memberConfigZone"); const zone = document.getElementById("memberConfigZone");
if (!select || !select.value || !zone) return; if (!select || !select.value || !zone) return;
window.currentSelectedMemberId = parseInt(select.value); const personId = parseInt(select.value);
window.currentSelectedMemberId = personId;
const role = ( const role = (
select.options[select.selectedIndex].dataset.role || "" select.options[select.selectedIndex].dataset.role || ""
).toLowerCase(); ).toLowerCase();
zone.innerHTML = '<p class="pf-muted-note">Chargement...</p>';
// === CAS 1 : C'EST UN ENFANT (On gère les modes de garde) ===
if (role === "child" || role === "enfant") { if (role === "child" || role === "enfant") {
const currentPerson = calGlobalData.people.find( const currentPerson = calGlobalData.people.find(
(k) => parseInt(k.id) === window.currentSelectedMemberId, (k) => parseInt(k.id) === personId,
); );
let savedModes = []; let savedModes = [];
try { try {
@@ -296,83 +360,145 @@ window.loadMemberConfigView = function () {
.join(""); .join("");
zone.innerHTML = ` zone.innerHTML = `
<h5 style="margin:0 0 8px 0; color:var(--text-main);">${tr("fc_care_modes_title") || "Modes de Garde"}</h5> <h5 style="margin:0 0 8px 0; color:var(--text-main);">${tr("fc_care_modes_title") || "Modes de Garde"}</h5>
<p style="font-size:0.85rem; color:var(--text-muted); margin-bottom:12px;">Quels modes de garde s'appliquent à cet enfant ?</p> <p style="font-size:0.85rem; color:var(--text-muted); margin-bottom:12px;">Quels modes de garde s'appliquent à cet enfant ?</p>
<div style="display:flex; flex-direction:column; gap:6px;"> <div style="display:flex; flex-direction:column; gap:6px;">
${modesHtml || `<em>Aucun mode de garde configuré dans le foyer.</em>`} ${modesHtml || `<em>Aucun mode de garde configuré dans le foyer.</em>`}
</div> </div>
<button class="pf-btn pf-btn-primary" style="margin-top:15px; width:100%;" onclick="submitChildCareModes()">${tr("btn_save_rights") || "Enregistrer"}</button> <button class="pf-btn pf-btn-primary" style="margin-top:15px; width:100%;" onclick="submitChildCareModes()">${tr("btn_save_rights") || "Enregistrer"}</button>
`; `;
} else {
window.loadAdultConfigView(window.currentSelectedMemberId, zone);
} }
}; // === CAS 2 : C'EST UN ADULTE (On gère les congés) ===
else {
try {
const res = await pachaFetch(
`/modules/family-calendar/includes/api/calendar-settings.php?action=get_person_leaves&person_id=${personId}`,
);
const memberLeaves = res.data || [];
renderMemberLeavesView(personId, memberLeaves);
} catch (err) {
zone.innerHTML =
'<p class="pf-muted-note" style="color:var(--danger);">Erreur de chargement.</p>';
}
}
}
// Affichage des congés du membre et du formulaire d'ajout
function renderMemberLeavesView(personId, memberLeaves) {
const zone = document.getElementById("memberConfigZone");
let html = `<h5 style="margin: 0 0 10px 0; color: var(--text-main);">Congés attribués</h5>`;
if (memberLeaves.length === 0) {
html += `<p class="pf-muted-note" style="font-size: 0.85rem;">Aucun congé attribué à ce membre.</p>`;
} else {
html += `<div style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px;">`;
memberLeaves.forEach((ml) => {
// On extrait le mois de la date anniversaire (ex: "2000-06-01" -> 6)
const moisRenouv = ml.anniversary_date
? parseInt(ml.anniversary_date.split("-")[1])
: 1;
html += `
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--bg-subtle); padding: 8px; border-radius: 6px; border: 1px solid var(--border-light);">
<div>
<strong style="color: var(--text-main);">${ml.leave_type}</strong>
<span style="font-size: 0.8rem; color: var(--text-muted); margin-left: 5px;">(Quota: <b>${ml.allowance}j</b> - Renouv: <b>Mois ${moisRenouv}</b>)</span>
</div>
<button class="pf-btn btn-secondary" style="padding: 2px 6px; color: var(--danger);" onclick="deleteMemberLeave(${ml.id}, ${personId})">🗑️</button>
</div>
`;
});
html += `</div>`;
}
// Ajout du selecteur de mois dans l'attribution
html += `
<hr style="border: 0; border-top: 1px solid var(--border-light); margin: 15px 0;">
<h5 style="margin: 0 0 10px 0;">+ Attribuer un congé</h5>
<div style="display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap;">
<div style="flex: 1; min-width: 120px;">
<label class="pf-label" style="font-size: 0.8rem;">Type de congé</label>
<select id="new-member-leave-type" class="pf-input" style="width: 100%;">
<option value="" disabled selected>-- Sélectionner --</option>
${globalLeaveCatalog.map((lt) => `<option value="${lt.code}">${lt.label}</option>`).join("")}
</select>
</div>
<div style="flex: 0 0 70px;">
<label class="pf-label" style="font-size: 0.8rem;">Quota</label>
<input type="number" step="0.5" id="new-member-leave-allowance" class="pf-input" value="0" style="width: 100%;">
</div>
<div style="flex: 0 0 100px;">
<label class="pf-label" style="font-size: 0.8rem;">Renouvellement</label>
<select id="new-member-leave-reset" class="pf-input" style="width: 100%;">
<option value="1">${tr("month_jan") || "Janvier"}</option>
<option value="2">${tr("month_feb") || "Février"}</option>
<option value="3">${tr("month_mar") || "Mars"}</option>
<option value="4">${tr("month_apr") || "Avril"}</option>
<option value="5">${tr("month_may") || "Mai"}</option>
<option value="6">${tr("month_jun") || "Juin"}</option>
<option value="7">${tr("month_jul") || "Juillet"}</option>
<option value="8">${tr("month_aug") || "Août"}</option>
<option value="9">${tr("month_sep") || "Septembre"}</option>
<option value="10">${tr("month_oct") || "Octobre"}</option>
<option value="11">${tr("month_nov") || "Novembre"}</option>
<option value="12">${tr("month_dec") || "Décembre"}</option>
</select>
</div>
<button class="pf-btn pf-btn-primary" onclick="addMemberLeave(${personId})">Ajouter</button>
</div>
`;
zone.innerHTML = html;
}
async function addMemberLeave(personId) {
const leaveCode = document.getElementById("new-member-leave-type").value;
const allowance = document.getElementById("new-member-leave-allowance").value;
const resetMonth = document.getElementById("new-member-leave-reset").value;
if (!leaveCode) return showToast("Veuillez sélectionner un type", "error");
const fd = new FormData();
fd.append("action", "add_person_leave");
fd.append("person_id", personId);
fd.append("leave_type", leaveCode);
fd.append("allowance", allowance);
fd.append("reset_month", resetMonth);
window.submitChildCareModes = async function () {
if (!window.currentSelectedMemberId) return;
const checkboxes = document.querySelectorAll(".js-care-mode-cb:checked");
const selectedModes = Array.from(checkboxes).map((cb) => cb.value);
try { try {
const formData = new FormData();
formData.append("action", "save_child_care_modes");
formData.append("person_id", window.currentSelectedMemberId);
formData.append("care_modes", JSON.stringify(selectedModes));
const res = await pachaFetch( const res = await pachaFetch(
"/modules/family-calendar/includes/api/settings.php", "/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: formData }, { method: "POST", body: fd },
); );
if (!res.success) throw new Error(res.error); if (!res.success) throw new Error(res.error);
if (window.showToast)
showToast(tr("fc_child_saved") || "Sauvegardé !", "success");
else alert(tr("fc_child_saved") || "Sauvegardé !");
const currentPerson = calGlobalData.people.find( if (window.showToast) showToast("Congé attribué avec succès !", "success");
(k) => parseInt(k.id) === window.currentSelectedMemberId, loadMemberConfigView();
);
if (currentPerson) {
currentPerson.care_modes = JSON.stringify(selectedModes);
}
} catch (err) { } catch (err) {
alert("Erreur: " + err.message); alert("Erreur: " + err.message);
} }
}; }
async function submitMemberLeaves() { // Supprimer un congé d'un membre
if (!currentSelectedMemberId || isNaN(currentSelectedMemberId)) { async function deleteMemberLeave(leaveId, personId) {
alert("Erreur technique : Aucun membre valide sélectionné."); if (!confirm(tr("confirm_delete") || "Retirer ce congé pour ce membre ?"))
return; return;
}
const fd = new FormData();
fd.append("action", "delete_person_leave");
fd.append("id", leaveId);
try { try {
const rows = document.querySelectorAll(".js-leave-row");
const leavesPayload = Array.from(rows)
.map((row) => ({
type: row.querySelector(".js-leave-type").value.trim().toUpperCase(),
method: row.querySelector(".js-leave-method").value,
allowance: row.querySelector(".js-leave-allowance").value,
date: row.querySelector(".js-leave-date").value,
}))
.filter((l) => l.type && l.date);
const formData = new FormData();
formData.append("action", "save_member_leaves");
formData.append("person_id", currentSelectedMemberId);
formData.append("leaves", JSON.stringify(leavesPayload));
const res = await pachaFetch( const res = await pachaFetch(
"/modules/family-calendar/includes/api/settings.php", "/modules/family-calendar/includes/api/calendar-settings.php",
{ method: "POST", body: formData }, { method: "POST", body: fd },
); );
if (!res.success) throw new Error(res.error); if (!res.success) throw new Error(res.error);
if (window.showToast) if (window.showToast) showToast("Congé retiré !", "success");
showToast(tr("fc_matrix_saved") || "Matrice enregistrée", "success"); loadMemberConfigView();
else alert(tr("fc_matrix_saved") || "Matrice enregistrée");
closeCalendarSettings();
setTimeout(openCalendarSettings, 300);
} catch (err) { } catch (err) {
alert("Erreur : " + err.message); alert("Erreur: " + err.message);
} }
} }
@@ -450,7 +576,7 @@ document.addEventListener("DOMContentLoaded", () => {
try { try {
const res = await pachaFetch( const res = await pachaFetch(
"/modules/family-calendar/includes/api/settings.php?action=get_all", "/modules/family-calendar/includes/api/calendar-settings.php?action=get_all",
); );
if (res && res.success && res.data) { if (res && res.success && res.data) {
window.calGlobalData = res.data; window.calGlobalData = res.data;
@@ -7,19 +7,14 @@ header('Content-Type: application/json');
$action = $_REQUEST['action'] ?? ''; $action = $_REQUEST['action'] ?? '';
try { try {
// ─── 1. RECUPÉRATION COMPLÈTE DES PARAMÈTRES ─── // ─── 1. RECUPÉRATION COMPLÈTE DES PARAMÈTRES (INITIALISATION) ───
if ($action === 'get_all') { if ($action === 'get_all') {
// A. Options globales du foyer
$stmtFoyer = $pdo->query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1"); $stmtFoyer = $pdo->query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1");
// On retire le fallback "Nounou" codé en dur, on part sur vide si rien n'est configuré
$foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '[]']; $foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '[]'];
// B. Liste des membres de la famille (Triés par rôle pour grouper Parents, Enfants, Helpers)
// 🟢 CORRECTION : Ajout de la colonne `care_modes` et `color` pour le Javascript !
$stmtPeople = $pdo->query("SELECT id, name, role, care_modes, color FROM pf_people WHERE is_active = 1 ORDER BY role ASC, name ASC"); $stmtPeople = $pdo->query("SELECT id, name, role, care_modes, color FROM pf_people WHERE is_active = 1 ORDER BY role ASC, name ASC");
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC); $people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
// C. Matrice des congés par personne
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance FROM pf_person_leave_meta"); $stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance FROM pf_person_leave_meta");
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC); $leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
@@ -33,18 +28,16 @@ try {
]; ];
} }
// D. Paramètres dynamiques du calendrier (pf_settings)
$stmtSettings = $pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'calendar'"); $stmtSettings = $pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'calendar'");
$calendarSettingsRaw = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR); // Crée un tableau [key => value] $calendarSettingsRaw = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR);
// Paramètres par défaut si la table est vide
$calendarSettings = [ $calendarSettings = [
'calendar_default_view' => $calendarSettingsRaw['calendar_default_view'] ?? 'month', 'calendar_default_view' => $calendarSettingsRaw['calendar_default_view'] ?? 'month',
'calendar_first_day' => $calendarSettingsRaw['calendar_first_day'] ?? '1', 'calendar_first_day' => $calendarSettingsRaw['calendar_first_day'] ?? '1',
'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00' 'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00'
]; ];
// E. NOUVEAU : Récupération du catalogue des types de congés de la famille // Catalogue des congés
$stmtLeaveTypes = $pdo->query("SELECT code, label, default_allowance, reset_month, allow_carry_over FROM pf_leave_types ORDER BY label ASC"); $stmtLeaveTypes = $pdo->query("SELECT code, label, default_allowance, reset_month, allow_carry_over FROM pf_leave_types ORDER BY label ASC");
$leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC); $leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC);
@@ -58,7 +51,7 @@ try {
'people' => $people, 'people' => $people,
'leaves' => $leavesMap, 'leaves' => $leavesMap,
'calendar_settings' => $calendarSettings, 'calendar_settings' => $calendarSettings,
'leave_types' => $leaveTypes // On envoie le catalogue au JS ! 'leave_types' => $leaveTypes
] ]
]); ]);
exit; exit;
@@ -70,7 +63,6 @@ try {
$zone = trim($_POST['zone_scolaire'] ?? 'C'); $zone = trim($_POST['zone_scolaire'] ?? 'C');
$modesRaw = $_POST['care_modes'] ?? '[]'; $modesRaw = $_POST['care_modes'] ?? '[]';
$modesArray = json_encode(array_values(array_unique(array_filter(json_decode($modesRaw, true))))); $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 = $pdo->prepare("UPDATE pf_foyer_settings SET zone_scolaire = ?, care_modes = ? WHERE id = 1");
@@ -80,74 +72,7 @@ try {
exit; exit;
} }
// ─── 3. SAUVEGARDE DES CONGÉS D'UN MEMBRE (ONGLET 2) ─── // ─── 3. SAUVEGARDE DES MODES DE GARDE D'UN ENFANT ───
if ($action === 'save_member_leaves') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$personId = (int)($_POST['person_id'] ?? 0);
$leavesData = json_decode($_POST['leaves'] ?? '[]', true);
if ($personId <= 0) throw new Exception("Identifiant de membre invalide.");
$pdo->beginTransaction();
$stmtDelete = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE person_id = ?");
$stmtDelete->execute([$personId]);
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']);
// Si le JS n'envoie que "MM-DD" (Renouvellement perpétuel), on ajoute l'année bissextile 2000 pour satisfaire le format DATE de MySQL
if (preg_match('/^\d{2}-\d{2}$/', $date)) {
$date = "2000-" . $date;
}
if (!empty($type) && !empty($date)) {
$stmtInsert->execute([$personId, $type, $method, $allowance, $date]);
}
}
}
$pdo->commit();
echo json_encode(['success' => true]);
exit;
}
// ─── 4. SAUVEGARDE DES PARAMÈTRES D'AFFICHAGE DU CALENDRIER (ONGLET 3) ───
if ($action === 'save_calendar_settings') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$allowed_keys = ['calendar_default_view', 'calendar_first_day', 'calendar_working_hours'];
$stmt = $pdo->prepare("
INSERT INTO pf_settings (setting_key, setting_value, module)
VALUES (:key, :val, 'calendar')
ON DUPLICATE KEY UPDATE setting_value = :val2
");
$pdo->beginTransaction();
foreach ($allowed_keys as $key) {
if (isset($_POST[$key])) {
$value = trim($_POST[$key]);
$stmt->execute([
'key' => $key,
'val' => $value,
'val2' => $value
]);
}
}
$pdo->commit();
echo json_encode(['success' => true]);
exit;
}
// ─── 5. SAUVEGARDE DES MODES DE GARDE D'UN ENFANT ───
if ($action === 'save_child_care_modes') { if ($action === 'save_child_care_modes') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée"); if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
@@ -164,7 +89,100 @@ try {
exit; exit;
} }
throw new Exception("Action inconnue"); // ─── 4. GESTION DU CATALOGUE DE CONGÉS (FOYER) ───
if ($action === 'get_leave_types') {
$stmt = $pdo->query("SELECT * FROM pf_leave_types ORDER BY label ASC");
echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
exit;
}
if ($action === 'save_leave_type') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$mode = $_POST['mode'] ?? 'add';
$code = strtoupper(trim($_POST['code'] ?? ''));
$label = trim($_POST['label'] ?? '');
if (empty($code) || empty($label)) throw new Exception("Le code et le label sont obligatoires.");
if ($mode === 'add') {
$stmt = $pdo->prepare("INSERT INTO pf_leave_types (code, label) VALUES (?, ?)");
$stmt->execute([$code, $label]);
} else {
$stmt = $pdo->prepare("UPDATE pf_leave_types SET label = ? WHERE code = ?");
$stmt->execute([$label, $code]);
}
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_leave_type') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$code = strtoupper(trim($_POST['code'] ?? ''));
if (empty($code)) throw new Exception("Code du congé manquant.");
$pdo->beginTransaction();
// 1. On supprime d'abord ce congé chez tous les membres à qui on l'avait attribué
$stmtMeta = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE leave_type = ?");
$stmtMeta->execute([$code]);
// 2. Ensuite, on supprime le congé du catalogue global
$stmtCat = $pdo->prepare("DELETE FROM pf_leave_types WHERE code = ?");
$stmtCat->execute([$code]);
$pdo->commit();
echo json_encode(['success' => true]);
exit;
}
// ─── 5. GESTION DES CONGÉS INDIVIDUELS (MEMBRES) ───
if ($action === 'get_person_leaves') {
$personId = (int)($_GET['person_id'] ?? 0);
$stmt = $pdo->prepare("SELECT id, leave_type, allowance, anniversary_date FROM pf_person_leave_meta WHERE person_id = ? ORDER BY leave_type ASC");
$stmt->execute([$personId]);
echo json_encode(['success' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
exit;
}
if ($action === 'add_person_leave') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$personId = (int)($_POST['person_id'] ?? 0);
$type = strtoupper(trim($_POST['leave_type'] ?? ''));
$allowance = (float)($_POST['allowance'] ?? 0);
$resetMonth = (int)($_POST['reset_month'] ?? 1);
if ($personId <= 0 || empty($type)) throw new Exception("Données invalides.");
$check = $pdo->prepare("SELECT id FROM pf_person_leave_meta WHERE person_id = ? AND leave_type = ?");
$check->execute([$personId, $type]);
if ($check->rowCount() > 0) throw new Exception("Ce congé est déjà attribué à cette personne.");
// On construit la date anniversaire avec le mois choisi par l'utilisateur
$anniversaryDate = "2000-" . str_pad((string)$resetMonth, 2, "0", STR_PAD_LEFT) . "-01";
$stmt = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, allowance, method, anniversary_date) VALUES (?, ?, ?, 'FIXED', ?)");
$stmt->execute([$personId, $type, $allowance, $anniversaryDate]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_person_leave') {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
$id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
throw new Exception("Action inconnue : " . htmlspecialchars($action));
} catch (\Throwable $e) { } catch (\Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack(); if ($pdo->inTransaction()) $pdo->rollBack();
+67 -34
View File
@@ -1,51 +1,84 @@
<?php <?php
require_once __DIR__ . '/../../includes/db.php'; require __DIR__ . '/../../includes/auth.php';
require __DIR__ . '/../../includes/db.php';
require_login();
header('Content-Type: application/json'); header('Content-Type: application/json');
$action = $_POST['action'] ?? $_GET['action'] ?? ''; $action = $_POST['action'] ?? $_GET['action'] ?? '';
try { try {
// 1. Lire toutes les fêtes if ($action === 'get_all') {
if ($action === 'get_occasions') { $occ = $pdo->query("SELECT * FROM pf_gift_occasions ORDER BY sort_order ASC, id ASC")->fetchAll();
$stmt = $pdo->query("SELECT * FROM pf_gift_occasions ORDER BY month_date ASC, id ASC"); $proches = $pdo->query("SELECT * FROM pf_people WHERE role IN ('proche_adulte', 'proche_enfant') ORDER BY name ASC")->fetchAll();
echo json_encode(['ok' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]); echo json_encode(['success' => true, 'data' => ['occasions' => $occ, 'proches' => $proches]]);
exit; exit;
} }
// 2. Ajouter une nouvelle fête (Renvoie l'ID pour rafraîchir la modale) if ($action === 'save_occasion') {
if ($action === 'add_occasion') { $id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
$name = trim($_POST['name'] ?? ''); $name = trim($_POST['name']);
$month_date = trim($_POST['month_date'] ?? ''); $icon = trim($_POST['icon']) ?: '🎁';
$month_date = trim($_POST['month_date']) ?: null;
if (empty($name)) throw new Exception("Name required"); if (empty($name)) throw new Exception("Le nom est obligatoire.");
$code = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $name));
$code = substr($code, 0, 20);
$stmt = $pdo->prepare("INSERT INTO pf_gift_occasions (code, name, month_date, is_active) VALUES (?, ?, ?, 1)"); if ($id) {
$stmt->execute([$code, $name, $month_date ?: null]); $stmt = $pdo->prepare("UPDATE pf_gift_occasions SET name=?, icon=?, month_date=? WHERE id=?");
$stmt->execute([$name, $icon, $month_date, $id]);
echo json_encode(['ok' => true]); } else {
exit; $code = 'CUST_' . strtoupper(substr(preg_replace('/[^A-Za-z0-9]/', '', $name), 0, 4)) . '_' . rand(100, 999);
} $stmt = $pdo->prepare("INSERT INTO pf_gift_occasions (code, name, icon, month_date) VALUES (?, ?, ?, ?)");
$stmt->execute([$code, $name, $icon, $month_date]);
// 3. Sauvegarde globale des cases à cocher
if ($action === 'save_toggles') {
$states = json_decode($_POST['states'] ?? '[]', true);
if (is_array($states)) {
$stmt = $pdo->prepare("UPDATE pf_gift_occasions SET is_active = ? WHERE id = ?");
foreach ($states as $item) {
$stmt->execute([(int)$item['state'], (int)$item['id']]);
}
} }
echo json_encode(['ok' => true]); echo json_encode(['success' => true]);
exit; exit;
} }
throw new Exception("Action inconnue."); if ($action === 'delete_occasion') {
$id = (int)$_POST['id'];
$pdo->prepare("DELETE FROM pf_gift_occasions WHERE id=?")->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'toggle_occasion') {
$id = (int)$_POST['id'];
$state = (int)$_POST['state'];
$pdo->prepare("UPDATE pf_gift_occasions SET is_active=? WHERE id=?")->execute([$state, $id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'save_proche') {
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
$name = trim($_POST['name']);
$role = $_POST['role'];
if (empty($name)) throw new Exception("Le nom est obligatoire.");
if (!in_array($role, ['proche_adulte', 'proche_enfant'])) throw new Exception("Rôle invalide.");
if ($id) {
// Sécurité : on s'assure qu'on modifie bien un proche et pas un parent
$stmt = $pdo->prepare("UPDATE pf_people SET name=?, role=? WHERE id=? AND role IN ('proche_adulte', 'proche_enfant')");
$stmt->execute([$name, $role, $id]);
} else {
$stmt = $pdo->prepare("INSERT INTO pf_people (name, role, color) VALUES (?, ?, '#94a3b8')");
$stmt->execute([$name, $role]);
}
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_proche') {
$id = (int)$_POST['id'];
$stmt = $pdo->prepare("DELETE FROM pf_people WHERE id=? AND role IN ('proche_adulte', 'proche_enfant')");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
} catch (Exception $e) { } catch (Exception $e) {
http_response_code(400); echo json_encode(['success' => false, 'error' => $e->getMessage()]);
echo json_encode(['ok' => false, 'error' => $e->getMessage()]); exit;
} }
?>
+161 -339
View File
@@ -6,7 +6,6 @@
margin: 0; margin: 0;
color: var(--text-main); color: var(--text-main);
} }
.cl-titlebar { .cl-titlebar {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -17,7 +16,6 @@
border-bottom: 1px solid var(--border-light); border-bottom: 1px solid var(--border-light);
padding-bottom: 15px; padding-bottom: 15px;
} }
.cl-view-switch { .cl-view-switch {
display: flex; display: flex;
padding: 4px; padding: 4px;
@@ -25,7 +23,6 @@
border: 1px solid var(--border-light); border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
.cl-view-btn { .cl-view-btn {
padding: 6px 16px; padding: 6px 16px;
border-radius: 6px; border-radius: 6px;
@@ -35,13 +32,12 @@
color: var(--text-muted); color: var(--text-muted);
transition: 0.2s; transition: 0.2s;
} }
.cl-view-btn.is-active { .cl-view-btn.is-active {
background: var(--text-main); background: var(--text-main);
color: var(--bg-panel); color: var(--bg-panel);
} }
/* === FILTRES STICKY === */ /* === FILTRES === */
.pf-filter-bar { .pf-filter-bar {
display: flex; display: flex;
gap: 10px; gap: 10px;
@@ -52,7 +48,6 @@
z-index: 50; z-index: 50;
flex-wrap: wrap; flex-wrap: wrap;
} }
.pf-filter-label { .pf-filter-label {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
@@ -62,35 +57,6 @@
gap: 6px; gap: 6px;
} }
.pf-filter-select {
padding: 6px 12px;
border-radius: 20px;
border: 1px solid var(--border-light);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
font-size: 0.85rem;
font-weight: 600;
color: var(--text-main);
outline: none;
font-family: inherit;
cursor: pointer;
box-shadow: var(--shadow-sm);
width: auto;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px;
padding-right: 28px;
}
.pf-filter-select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
}
/* === SECTIONS ENFANTS === */ /* === SECTIONS ENFANTS === */
.pf-child-section { .pf-child-section {
margin-bottom: 30px; margin-bottom: 30px;
@@ -226,18 +192,69 @@
z-index: 2; z-index: 2;
border-right: 2px solid var(--border-light); border-right: 2px solid var(--border-light);
} }
.gift-tricount-section {
.cl-occasion-icon { border: 1px solid var(--border-light);
width: 20px; margin-top: 40px;
height: 20px; }
object-fit: contain; .gift-tricount-title {
vertical-align: middle; margin-top: 0;
color: var(--text-main);
font-size: 1.3rem;
}
.gift-tricount-success {
color: var(--success);
font-weight: 700;
margin-bottom: 0;
}
.gift-tricount-debt-amount {
color: var(--danger);
}
.gift-matrix-details {
margin-top: 20px;
padding: 12px;
border-radius: 8px;
background: var(--bg-panel);
border: 1px solid var(--border-light);
}
.gift-matrix-summary {
cursor: pointer;
font-weight: 600;
color: var(--text-muted);
outline: none;
}
.gift-matrix-wrapper {
overflow-x: auto;
margin-top: 15px;
}
.cl-debt-matrix th.sticky-col.bg-header {
background: var(--bg-page);
}
.cl-debt-matrix th.sticky-col.bg-body {
background: var(--bg-panel);
}
.gift-cell-danger {
color: var(--danger);
font-weight: 700;
background: var(--bg-danger-soft);
}
.gift-cell-muted {
color: var(--text-muted);
} }
/* === TYPOGRAPHIE & ICONES === */
.cl-occasion-icon {
font-size: 1.4rem;
margin-right: 8px;
}
.cl-occasion-title { .cl-occasion-title {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; }
.gift-empty-state {
color: var(--text-muted);
font-size: 0.9rem;
font-style: italic;
margin: 0;
} }
/* === COMPOSANT MULTI-SELECT VANILLA === */ /* === COMPOSANT MULTI-SELECT VANILLA === */
@@ -355,79 +372,7 @@
} }
} }
/* Header & Settings Button */ /* === MODALE EDITION CADEAU === */
.gift-settings-btn {
font-size: 1.5rem;
background: none;
border: none;
cursor: pointer;
}
/* Empty States */
.gift-empty-state {
color: var(--text-muted);
font-size: 0.9rem;
font-style: italic;
margin: 0;
}
/* Section Tricount (Bilan) */
.gift-tricount-section {
border: 1px solid var(--border-light);
margin-top: 40px;
}
.gift-tricount-title {
margin-top: 0;
color: var(--text-main);
font-size: 1.3rem;
}
.gift-tricount-success {
color: var(--success);
font-weight: 700;
margin-bottom: 0;
}
.gift-tricount-debt-amount {
color: var(--danger);
}
.gift-matrix-details {
margin-top: 20px;
padding: 12px;
border-radius: 8px;
}
.gift-matrix-summary {
cursor: pointer;
font-weight: 600;
color: var(--text-muted);
outline: none;
}
.gift-matrix-wrapper {
overflow-x: auto;
margin-top: 15px;
}
.cl-debt-matrix th.sticky-col,
.cl-debt-matrix td.sticky-col {
position: sticky;
left: 0;
z-index: 2;
border-right: 2px solid #e2e8f0;
}
.cl-debt-matrix th.sticky-col.bg-header {
background: #f8fafc;
}
.cl-debt-matrix th.sticky-col.bg-body {
background: white;
}
.gift-cell-danger {
color: var(--danger);
font-weight: 700;
background: #fef2f2;
}
.gift-cell-muted {
color: var(--text-muted);
}
/* Modale Edition Cadeau */
.gift-modal-custom-content { .gift-modal-custom-content {
max-width: 500px; max-width: 500px;
width: 95%; width: 95%;
@@ -459,249 +404,126 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 10px; gap: 10px;
border-top: 1px solid var(--border-light);
} }
/* Modale Paramètres Occasions (Bottom Sheet style) */ .gl-pane {
.gift-settings-backdrop {
display: none; display: none;
align-items: flex-end; /* Bottom sheet on mobile */ animation: fadeIn 0.3s ease;
} }
.gift-settings-modal { .gl-pane.active {
width: 100%; display: block;
border-radius: 12px 12px 0 0;
margin: 0;
} }
.gift-settings-header { .gl-pane-title {
display: flex; margin-top: 0;
justify-content: space-between; color: var(--text-main);
align-items: center; font-size: 1.1rem;
margin-bottom: 10px;
} }
.gift-settings-header h3 { .gl-pane-desc {
margin: 0; font-size: 0.85rem;
}
.gift-settings-body {
padding: 1rem;
}
.gift-occasions-list {
margin-bottom: 2rem;
}
.gift-occasions-grid {
display: grid;
gap: 0.5rem;
}
.gift-occasion-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 1rem;
border: 1px solid var(--border-light);
border-radius: 8px;
}
.gift-occasion-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.gift-date-badge {
font-size: 0.8em;
background: var(--bg-page);
padding: 2px 6px;
border-radius: 4px;
margin-left: 10px;
}
.gift-add-subtitle {
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.gift-add-form {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.gift-add-input-name {
flex: 1;
min-width: 150px;
}
.gift-add-input-date {
width: 120px;
}
.gift-help-text {
color: var(--text-muted); color: var(--text-muted);
font-size: 0.85em; margin-bottom: 15px;
} }
.gl-list-item {
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);
margin-bottom: 8px;
}
.gl-form-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: flex-end;
margin-top: 15px;
}
.gl-divider {
margin: 1.5rem 0;
border: 0;
border-bottom: 1px solid var(--border-light);
}
.gl-input-sm {
flex: 0 0 60px;
}
.gl-input-md {
flex: 1;
min-width: 120px;
}
.gift-loader { .gift-loader {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
}
.gift-error-msg {
color: var(--danger);
padding: 10px;
}
/* ==========================================================================
BOUTONS & MODALE (Design System Todo)
========================================================================== */
.btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.85rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.15s;
white-space: nowrap;
}
.btn-icon {
padding: 0.25rem;
min-width: 28px;
justify-content: center;
font-size: 1.2rem;
}
.btn-ghost {
background: transparent;
border: none;
color: var(--text-muted); color: var(--text-muted);
font-style: italic;
} }
.btn-ghost:hover { .align-center {
color: var(--text-main);
background: var(--bg-page);
border-radius: 6px;
}
/* Correction de la surimpression de la modale Paramètres */
.gift-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; align-items: center;
justify-content: center;
padding: 1rem;
} }
.gift-settings-modal { /* =========================================
background: var(--bg-panel); COMPOSANTS MODALE & EDITION IN-PLACE
border: 1px solid var(--border-light); ========================================= */
border-radius: 14px;
width: 100%; /* Sélecteur d'émojis horizontal */
max-width: 500px; .emoji-picker {
max-height: 90vh;
display: flex; display: flex;
flex-direction: column; gap: 8px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
animation: modalIn 0.18s ease;
}
@keyframes modalIn {
from {
opacity: 0;
transform: scale(0.96) translateY(-8px);
}
to {
opacity: 1;
transform: none;
}
}
/* ==========================================================================
MODALE PARAMÈTRES (Design System Todo)
========================================================================== */
.gift-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;
}
.gift-settings-backdrop.show {
display: flex;
}
.gift-settings-modal {
background: var(--bg-panel);
border: 1px solid var(--border-light);
border-radius: 14px;
width: 100%;
max-width: 500px;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
animation: giftModalIn 0.18s ease;
}
@keyframes giftModalIn {
from {
opacity: 0;
transform: scale(0.96) translateY(-8px);
}
to {
opacity: 1;
transform: none;
}
}
.gift-settings-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border-light);
}
.gift-settings-header h3 {
font-size: 1rem;
font-weight: 700;
margin: 0;
}
.gift-settings-close {
background: none;
border: none;
cursor: pointer;
color: var(--text-muted);
font-size: 1.2rem;
border-radius: 6px;
padding: 0.2rem 0.4rem;
}
.gift-settings-close:hover {
background: var(--bg-page);
}
.gift-settings-body {
padding: 1.25rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.gift-add-subtitle {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin: 0.5rem 0 0;
}
.gift-add-form {
display: flex;
gap: 0.5rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
/* Aligner le bouton sur celui de Todo */ .emoji-option {
.gift-add-form .btn-primary { cursor: pointer;
background: var(--primary); width: 38px;
color: #fff; height: 38px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border-light);
border-radius: 8px;
font-size: 1.4rem;
transition: all 0.2s;
background: transparent;
user-select: none;
}
.emoji-option:hover {
background: var(--bg-page);
border-color: #cbd5e1;
}
.emoji-option.selected {
border-color: var(--primary); border-color: var(--primary);
background: var(
--bg-subtle
); /* ou un bleu très clair rgba(59, 130, 246, 0.1) */
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
} }
.gift-add-form .btn-primary:hover {
background: var(--primary-dark); /* Alignement des boutons d'ajout */
.modal-actions-right {
display: flex;
gap: 10px;
justify-content: flex-end;
width: 100%;
margin-top: 10px;
padding-top: 15px;
border-top: 1px solid var(--border-light);
}
/* Édition In-Place sur les lignes de la liste */
.inline-edit-form {
display: flex;
gap: 8px;
align-items: center;
background: var(--bg-subtle);
padding: 8px 12px;
border-radius: 8px;
width: 100%;
flex-wrap: wrap; /* Évite que ça dépasse sur mobile */
}
.inline-edit-form .pf-input {
margin: 0;
} }
+16 -7
View File
@@ -89,14 +89,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$meta_pdo->prepare( $meta_pdo->prepare(
"INSERT INTO users (username, password_hash, display_name, family_id) VALUES (?, ?, ?, ?)" "INSERT INTO users (username, password_hash, display_name, family_id) VALUES (?, ?, ?, ?)"
)->execute([$username, $hash, $display_name, $family['id']]); )->execute([$username, $hash, $display_name, $family['id']]);
$user_id = (int)$meta_pdo->lastInsertId();
// 🔥 INJECTION ICI : Ajouter la personne à la base de la famille (Couleur orangée pour différencier)
$tenant_db = $family['db_name'];
$tenantPdo = new PDO("mysql:host=$db_host;dbname=$tenant_db;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$stmtPerson = $tenantPdo->prepare("INSERT INTO pf_people (name, user_id, role, color, is_active) VALUES (?, ?, 'parent', '#f59e0b', 1)");
$stmtPerson->execute([$display_name, $user_id]);
$_SESSION['user'] = [ $_SESSION['user'] = [
'id' => (int)$meta_pdo->lastInsertId(), 'id' => $user_id,
'username' => $username, 'username' => $username,
'display_name' => $display_name, 'display_name' => $display_name,
'family_id' => (int)$family['id'], 'family_id' => (int)$family['id'],
]; ];
$_SESSION['family_db'] = $family['db_name']; $_SESSION['family_db'] = $tenant_db;
session_regenerate_id(true); session_regenerate_id(true);
header('Location: /index.php'); header('Location: /index.php');
exit; exit;
@@ -129,6 +137,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
)->execute([$username, $hash, $display_name, $family_id]); )->execute([$username, $hash, $display_name, $family_id]);
$user_id = (int)$meta_pdo->lastInsertId(); $user_id = (int)$meta_pdo->lastInsertId();
// 🔥 INJECTION ICI : Ajouter le créateur comme premier parent de la famille (Couleur bleue)
$tenantPdo = new PDO("mysql:host=$db_host;dbname=$db_name;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$stmtPerson = $tenantPdo->prepare("INSERT INTO pf_people (name, user_id, role, color, is_active) VALUES (?, ?, 'parent', '#0891b2', 1)");
$stmtPerson->execute([$display_name, $user_id]);
$meta_pdo->commit(); $meta_pdo->commit();
$_SESSION['user'] = [ $_SESSION['user'] = [
@@ -169,7 +182,6 @@ require __DIR__ . '/header.php';
<div class="pf-login-error"><?= htmlspecialchars($error) ?></div> <div class="pf-login-error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?> <?php endif; ?>
<!-- Onglets -->
<div style="display:flex;gap:8px;margin-bottom:24px"> <div style="display:flex;gap:8px;margin-bottom:24px">
<button id="tab-create" onclick="switchTab('create')" <button id="tab-create" onclick="switchTab('create')"
class="pf-btn" style="flex:1">Nouvel espace</button> class="pf-btn" style="flex:1">Nouvel espace</button>
@@ -177,7 +189,6 @@ require __DIR__ . '/header.php';
class="pf-btn btn-secondary" style="flex:1">Rejoindre</button> class="pf-btn btn-secondary" style="flex:1">Rejoindre</button>
</div> </div>
<!-- Formulaire commun -->
<form method="post" action="/register.php" id="reg-form"> <form method="post" action="/register.php" id="reg-form">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
<input type="hidden" name="action" id="reg-action" value="create"> <input type="hidden" name="action" id="reg-action" value="create">
@@ -208,7 +219,6 @@ require __DIR__ . '/header.php';
placeholder="••••••" autocomplete="new-password"> placeholder="••••••" autocomplete="new-password">
</div> </div>
<!-- Champs spécifiques : Nouvel espace -->
<div id="section-create"> <div id="section-create">
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Nom de la famille / foyer</label> <label class="pf-label">Nom de la famille / foyer</label>
@@ -218,7 +228,6 @@ require __DIR__ . '/header.php';
</div> </div>
</div> </div>
<!-- Champs spécifiques : Rejoindre -->
<div id="section-join" style="display:none"> <div id="section-join" style="display:none">
<div class="pf-form-group"> <div class="pf-form-group">
<label class="pf-label">Code d'invitation</label> <label class="pf-label">Code d'invitation</label>
@@ -256,4 +265,4 @@ switchTab('join');
<?php endif; ?> <?php endif; ?>
</script> </script>
<?php require __DIR__ . '/footer.php'; ?> <?php require __DIR__ . '/footer.php'; ?>
+1 -1
View File
@@ -630,7 +630,7 @@ require __DIR__ . '/header.php';
</section> </section>
<?php endif; ?> <?php endif; ?>
<?php if ($_SESSION['user']['is_admin']): ?> <?php if (!empty($_SESSION['user']['is_admin'])): ?>
<div class="pf-settings-admin-link"> <div class="pf-settings-admin-link">
<a href="/admin/">→ Panneau d'administration</a> <a href="/admin/">→ Panneau d'administration</a>
</div> </div>