+26
-940
@@ -4,6 +4,18 @@ require __DIR__ . '/includes/db.php';
|
||||
require_once __DIR__ . '/includes/i18n.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')
|
||||
$tab = $_GET['tab'] ?? 'suivi';
|
||||
|
||||
@@ -15,11 +27,15 @@ require __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="pf-container">
|
||||
<div class="pf-hero" style="text-align: center; margin-bottom: 30px;">
|
||||
<div style="display: flex; justify-content: center; align-items: center; gap: 15px; margin-bottom: 20px;">
|
||||
<h1 style="margin: 0;"><?= tr('budget_main_header') ?></h1>
|
||||
<button class="btn-settings-gear" onclick="openBudgetSettings()" title="<?= tr('settings') ?>">
|
||||
⚙️
|
||||
<div class="pf-hero budget-hero">
|
||||
<div class="budget-title-group">
|
||||
<h1><?= tr('budget_main_header') ?></h1>
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +64,6 @@ require __DIR__ . '/header.php';
|
||||
|
||||
<section class="pf-section">
|
||||
<?php
|
||||
// ROUTAGE DYNAMIQUE
|
||||
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev'];
|
||||
|
||||
if (in_array($tab, $allowedTabs)) {
|
||||
@@ -57,7 +72,7 @@ require __DIR__ . '/header.php';
|
||||
if (file_exists($viewPath)) {
|
||||
include $viewPath;
|
||||
} 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 "<p>" . sprintf(tr('budget_err_file_detail'), htmlspecialchars($tab)) . "</p>";
|
||||
echo "</div>";
|
||||
@@ -68,937 +83,8 @@ require __DIR__ . '/header.php';
|
||||
?>
|
||||
</section>
|
||||
</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>
|
||||
|
||||
<hr style="margin: 1.5rem 0; border:0; border-bottom:1px solid var(--border-light);">
|
||||
|
||||
<h5 class="bs-section-title" style="font-size:1rem;"><?= tr('bs_add_account_title') ?></h5>
|
||||
<form id="form-add-account" style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<input type="text" name="name" class="pf-input" placeholder="<?= tr('bs_account_name_ph') ?>" required style="flex: 1; min-width: 150px; padding: 0.4rem 0.75rem;">
|
||||
<select name="type" class="pf-input" style="width: 130px; padding: 0.4rem 0.75rem;">
|
||||
<option value="checking"><?= tr('bs_type_checking') ?></option>
|
||||
<option value="savings"><?= tr('bs_type_savings') ?></option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="pane-categories" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_categories') ?></h4>
|
||||
<div id="bs-list-categories">⏳ <?= tr('loading') ?></div>
|
||||
|
||||
<hr style="margin: 1.5rem 0; border:0; border-bottom:1px solid var(--border-light);">
|
||||
|
||||
<h5 class="bs-section-title" style="font-size:1rem;"><?= tr('bs_add_category_title') ?></h5>
|
||||
<form id="form-add-category" style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
|
||||
<input type="text" name="code" class="pf-input" placeholder="<?= tr('bs_cat_code_ph') ?>" required style="width: 120px; padding: 0.4rem 0.75rem;">
|
||||
<input type="text" name="label" class="pf-input" placeholder="<?= tr('bs_cat_label_ph') ?>" required style="flex: 1; min-width: 150px; padding: 0.4rem 0.75rem;">
|
||||
|
||||
<select name="type" class="pf-input" style="width: 120px; padding: 0.4rem 0.75rem;">
|
||||
<option value="Expense"><?= tr('bs_type_expense') ?></option>
|
||||
<option value="Income"><?= tr('bs_type_income') ?></option>
|
||||
<option value="Savings"><?= tr('bs_type_savings_cat') ?></option>
|
||||
</select>
|
||||
|
||||
<select name="icon" class="pf-input" style="width: 65px; padding: 0.4rem 0.2rem; font-size: 1.1rem; cursor: pointer; text-align: center;">
|
||||
<option value="📌">📌</option>
|
||||
<option value="🛒">🛒</option>
|
||||
<option value="🥖">🥖</option>
|
||||
<option value="🍽️">🍽️</option>
|
||||
<option value="⛽">⛽</option>
|
||||
<option value="🚗">🚗</option>
|
||||
<option value="🚆">🚆</option>
|
||||
<option value="🏠">🏠</option>
|
||||
<option value="⚡">⚡</option>
|
||||
<option value="💧">💧</option>
|
||||
<option value="⚕️">⚕️</option>
|
||||
<option value="🎒">🎒</option>
|
||||
<option value="👕">👕</option>
|
||||
<option value="📱">📱</option>
|
||||
<option value="🎮">🎮</option>
|
||||
<option value="✈️">✈️</option>
|
||||
<option value="🎁">🎁</option>
|
||||
<option value="🐶">🐶</option>
|
||||
<option value="💵">💵</option>
|
||||
<option value="🐷">🐷</option>
|
||||
<option value="📈">📈</option>
|
||||
</select>
|
||||
|
||||
<input type="color" name="color" value="#3b82f6" style="width: 38px; height: 38px; padding: 0; border: 1px solid var(--border-light); border-radius: 6px; cursor: pointer;">
|
||||
|
||||
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="pane-rules" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_rules') ?></h4>
|
||||
|
||||
<input type="text" id="input-search-rules" class="bs-rule-search" placeholder="🔍 Rechercher un mot-clé (ex: AUCHAN)..." onkeyup="filterRules()">
|
||||
|
||||
<div id="bs-list-rules">⏳ <?= tr('loading') ?></div>
|
||||
|
||||
<hr style="margin: 1.5rem 0; border:0; border-bottom:1px solid var(--border-light);">
|
||||
|
||||
<h5 class="bs-section-title" style="font-size:1rem;"><?= tr('bs_add_rule_title') ?></h5>
|
||||
<form id="form-add-rule" style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
|
||||
<input type="text" name="keyword" class="pf-input" placeholder="<?= tr('bs_rule_keyword_ph') ?>" required style="flex: 1; min-width: 150px; padding: 0.4rem 0.75rem;">
|
||||
|
||||
<select name="category" id="select-rule-category" class="pf-input" required style="width: 180px; padding: 0.4rem 0.75rem;">
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="pane-salaries" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_salaries') ?> (<?= date('Y') ?>)</h4>
|
||||
<div id="bs-list-salaries">⏳ <?= tr('loading') ?></div>
|
||||
</div>
|
||||
|
||||
<div 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'; ?>
|
||||
<?php
|
||||
require __DIR__ . '/modules/budget/views/settings_modal.php';
|
||||
require __DIR__ . '/footer.php';
|
||||
?>
|
||||
+125
-66
@@ -7,6 +7,8 @@ CREATE TABLE IF NOT EXISTS pf_foyer_settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
currency VARCHAR(10) NOT NULL DEFAULT '€',
|
||||
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
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
@@ -44,12 +46,14 @@ CREATE TABLE IF NOT EXISTS pf_users (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ─── Personnes ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE `pf_people` (
|
||||
CREATE TABLE IF NOT EXISTS `pf_people` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
`role` varchar(50) DEFAULT NULL,
|
||||
`care_modes` varchar(255) DEFAULT NULL,
|
||||
`color` varchar(7) DEFAULT '#0891b2',
|
||||
`is_active` TINYINT(1) DEFAULT 1,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
@@ -88,10 +92,29 @@ CREATE TABLE IF NOT EXISTS pf_leave_snapshots (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_person_leave_meta (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
person_id INT NOT NULL UNIQUE,
|
||||
anniversary_date DATE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||
person_id INT(11) NOT NULL,
|
||||
leave_type VARCHAR(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
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 (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
@@ -108,87 +131,101 @@ CREATE TABLE IF NOT EXISTS pf_calendar_weeks (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ─── 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 (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
amount DECIMAL(10,2) DEFAULT 0,
|
||||
category VARCHAR(100),
|
||||
type VARCHAR(50),
|
||||
payment_day INT DEFAULT NULL,
|
||||
is_estimate TINYINT(1) DEFAULT 0,
|
||||
reg_month VARCHAR(7) DEFAULT NULL,
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
amount DECIMAL(10,2) DEFAULT 0,
|
||||
category VARCHAR(100),
|
||||
type VARCHAR(50),
|
||||
payment_day INT DEFAULT NULL,
|
||||
is_estimate TINYINT(1) DEFAULT 0,
|
||||
reg_month VARCHAR(7) DEFAULT NULL,
|
||||
mapping_keywords TEXT DEFAULT NULL,
|
||||
holiday_id INT DEFAULT NULL,
|
||||
is_checked TINYINT(1) DEFAULT 0,
|
||||
sort_order INT DEFAULT 0
|
||||
holiday_id INT DEFAULT NULL,
|
||||
is_checked TINYINT(1) DEFAULT 0,
|
||||
sort_order INT DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_expenses (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
date_exp DATE NOT NULL,
|
||||
gestion_month VARCHAR(10) NOT NULL,
|
||||
category VARCHAR(100),
|
||||
label VARCHAR(255),
|
||||
amount DECIMAL(10,2) DEFAULT 0,
|
||||
import_ref VARCHAR(255) DEFAULT NULL,
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
date_exp DATE NOT NULL,
|
||||
gestion_month VARCHAR(10) NOT NULL,
|
||||
category VARCHAR(100),
|
||||
label VARCHAR(255),
|
||||
amount DECIMAL(10,2) DEFAULT 0,
|
||||
import_ref VARCHAR(255) 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;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
target DECIMAL(10,2) DEFAULT 0.00,
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
target DECIMAL(10,2) DEFAULT 0.00,
|
||||
transfer_dest VARCHAR(50) DEFAULT NULL,
|
||||
holiday_id INT DEFAULT NULL,
|
||||
sort_order INT DEFAULT 0
|
||||
holiday_id INT DEFAULT NULL,
|
||||
sort_order INT DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_alloc_values (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
month_date VARCHAR(10) NOT NULL,
|
||||
cat_id INT NOT NULL,
|
||||
person_id INT NOT NULL,
|
||||
amount DECIMAL(10,2) DEFAULT 0.00,
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
month_date VARCHAR(10) NOT NULL,
|
||||
cat_id INT NOT NULL,
|
||||
person_id INT NOT NULL,
|
||||
amount DECIMAL(10,2) DEFAULT 0.00,
|
||||
UNIQUE KEY uq_alloc_person (month_date, cat_id, person_id),
|
||||
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_savings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
owner VARCHAR(50) NOT NULL,
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
owner VARCHAR(50) NOT NULL,
|
||||
month_date VARCHAR(10) NOT NULL,
|
||||
category VARCHAR(100) NOT NULL,
|
||||
amount DECIMAL(10,2) DEFAULT 0,
|
||||
holiday_id INT DEFAULT NULL
|
||||
category VARCHAR(100) NOT NULL,
|
||||
amount DECIMAL(10,2) DEFAULT 0,
|
||||
holiday_id INT DEFAULT NULL,
|
||||
UNIQUE KEY uq_savings (owner, month_date, category)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
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,
|
||||
amount DECIMAL(12,2) DEFAULT 0
|
||||
amount DECIMAL(12,2) DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_salary_config (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
year INT NOT NULL,
|
||||
person VARCHAR(50) NOT NULL,
|
||||
salary DECIMAL(10,2) DEFAULT 0,
|
||||
mensualite DECIMAL(10,2) DEFAULT 0,
|
||||
frais_func DECIMAL(10,2) DEFAULT 0,
|
||||
eco_perso DECIMAL(10,2) DEFAULT 0,
|
||||
eco_family DECIMAL(10,2) DEFAULT 0,
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
year INT NOT NULL,
|
||||
person VARCHAR(50) NOT NULL,
|
||||
salary DECIMAL(10,2) DEFAULT 0,
|
||||
mensualite DECIMAL(10,2) DEFAULT 0,
|
||||
frais_func DECIMAL(10,2) DEFAULT 0,
|
||||
eco_perso DECIMAL(10,2) DEFAULT 0,
|
||||
eco_family DECIMAL(10,2) DEFAULT 0,
|
||||
UNIQUE KEY uq_salary (year, person)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_import_rules (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
keyword VARCHAR(255) NOT NULL UNIQUE,
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
keyword VARCHAR(255) NOT NULL UNIQUE,
|
||||
category VARCHAR(100) NOT NULL,
|
||||
budget_item_id INT(11) NULL DEFAULT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pf_advances` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`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
|
||||
INSERT IGNORE INTO pf_budget_categories (code, label, type, color, icon) VALUES
|
||||
('INCOME', 'Revenus', 'Income', '#10b981', '💵'),
|
||||
('FMCG', 'Alimentation & Courses', 'Expense', '#3b82f6', '🛒'),
|
||||
('FUEL', 'Carburant', 'Expense', '#f59e0b', '⛽'),
|
||||
('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢'),
|
||||
('SCHOOL', 'École & Garde', 'Expense', '#a855f7', '🎒'),
|
||||
('SAVINGS', 'Épargne', 'Expense', '#8b5cf6', '🐷');
|
||||
('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢');
|
||||
|
||||
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_notes (
|
||||
@@ -308,16 +341,42 @@ CREATE TABLE IF NOT EXISTS pf_geocode_cache (
|
||||
|
||||
-- ─── Cadeaux ──────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_gifts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
year INT NOT NULL,
|
||||
adult_name VARCHAR(100) NOT NULL,
|
||||
payer_name VARCHAR(100),
|
||||
child_name VARCHAR(100) NOT NULL,
|
||||
occasion VARCHAR(50) NOT NULL,
|
||||
id INT(11) AUTO_INCREMENT PRIMARY KEY,
|
||||
year INT(11) NOT NULL,
|
||||
adult_name VARCHAR(100) NOT NULL,
|
||||
payer_name VARCHAR(100) DEFAULT NULL,
|
||||
child_name VARCHAR(100) NOT NULL,
|
||||
occasion VARCHAR(50) NOT NULL,
|
||||
gift_description TEXT NOT NULL,
|
||||
product_link VARCHAR(500) DEFAULT NULL,
|
||||
amount DECIMAL(8,2) DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
product_link VARCHAR(500) DEFAULT NULL,
|
||||
amount DECIMAL(8,2) DEFAULT 0.00
|
||||
) 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 ───────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS pf_vehicles (
|
||||
|
||||
+146
-41
@@ -29,7 +29,7 @@ foreach ($familyPeople as $p) {
|
||||
}
|
||||
|
||||
// 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);
|
||||
$activeCareModes = json_decode($foyerData['care_modes'] ?? '[]', true);
|
||||
if (!is_array($activeCareModes)) $activeCareModes = [];
|
||||
@@ -45,6 +45,38 @@ foreach ($dbLeaves as $l) {
|
||||
}
|
||||
$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');
|
||||
$activePage = "family-calendar";
|
||||
$mainClass = "pf-family-calendar";
|
||||
@@ -77,6 +109,20 @@ require __DIR__ . '/header.php';
|
||||
</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 class="pf-modal-content">
|
||||
<div class="pf-modal-header">
|
||||
@@ -155,61 +201,120 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
|
||||
<div id="modalCalendarSettings" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 600px; width: 90%;">
|
||||
<div class="pf-modal-header">
|
||||
<h3 class="pf-modal-title">⚙️ <?= tr('fc_modal_settings_title') ?></h3>
|
||||
<button type="button" class="pf-modal-close" onclick="closeCalendarSettings()">×</button>
|
||||
</div>
|
||||
<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>
|
||||
<button type="button" class="pf-modal-close" onclick="closeCalendarSettings()">×</button>
|
||||
</div>
|
||||
|
||||
<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-membres" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('membres')">👥 Membres & Droits</button>
|
||||
</div>
|
||||
<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')">
|
||||
🏡 <?= tr('fc_tab_foyer') ?>
|
||||
</button>
|
||||
<button type="button" id="tab-btn-membres" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('membres')">
|
||||
👥 <?= 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">
|
||||
<form id="formCalFoyer" onsubmit="submitCalFoyer(event)">
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('fc_zone_label') ?></label>
|
||||
<select id="setZoneScolaire" name="zone_scolaire" class="pf-input" required style="width: 100%;">
|
||||
<option value="A">Zone A</option>
|
||||
<option value="B">Zone B</option>
|
||||
<option value="C">Zone C</option>
|
||||
<option value="Autre">Autre / Hors France</option>
|
||||
</select>
|
||||
<small class="pf-muted-note"><?= tr('fc_zone_desc') ?></small>
|
||||
</div>
|
||||
<div id="cal-pane-foyer" class="cal-settings-pane">
|
||||
|
||||
<form id="formCalFoyer" onsubmit="submitCalFoyer(event)">
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('fc_zone_label') ?></label>
|
||||
<select id="setZoneScolaire" name="zone_scolaire" class="pf-input" required style="width: 100%;">
|
||||
<option value="A"><?= tr('fc_zone_a') ?></option>
|
||||
<option value="B"><?= tr('fc_zone_b') ?></option>
|
||||
<option value="C"><?= tr('fc_zone_c') ?></option>
|
||||
<option value="Autre"><?= tr('fc_zone_other') ?></option>
|
||||
</select>
|
||||
<small class="pf-muted-note"><?= tr('fc_zone_desc') ?></small>
|
||||
</div>
|
||||
|
||||
<div class="pf-form-group" style="margin-top: 1.5rem;">
|
||||
<label class="pf-label"><?= tr('fc_care_modes_label') ?></label>
|
||||
<div class="pf-form-group" style="margin-top: 1.5rem;">
|
||||
<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 style="display: flex; gap: 8px;">
|
||||
<input type="text" id="inputNewCareMode" class="pf-input" placeholder="<?= tr('fc_add_mode_placeholder') ?>" style="flex: 1;">
|
||||
<button type="button" class="btn btn-secondary" style="padding: 0 16px; border-radius: 6px;" onclick="addCareModeTag()">+</button>
|
||||
<button type="button" class="pf-btn btn-secondary" style="padding: 0 16px; border-radius: 6px;" onclick="addCareModeTag()">+</button>
|
||||
</div>
|
||||
<small class="pf-muted-note"><?= tr('fc_care_modes_desc') ?></small>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2rem; display: flex; justify-content: flex-end; gap: 8px;">
|
||||
<button type="button" class="pf-btn pf-btn-secondary" onclick="closeCalendarSettings()"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('save') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="cal-pane-membres" class="cal-settings-pane" style="display: none;">
|
||||
<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>
|
||||
<?php else: ?>
|
||||
<div style="background: var(--bg-subtle); padding: 12px; border-radius: 8px; border: 1px dashed var(--border-main); text-align: center;">
|
||||
<p style="margin: 0 0 10px 0; color: var(--text-muted); font-size: 0.9rem;">
|
||||
<?= tr('fc_empty_kids_msg') ?>
|
||||
</p>
|
||||
<a href="/settings.php" class="pf-btn btn-secondary" style="text-decoration: none; display: inline-block; font-size: 0.85rem;">
|
||||
⚙️ <?= tr('fc_link_settings') ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div id="memberConfigZone" style="background: var(--bg-page); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); min-height: 100px;">
|
||||
</div>
|
||||
<?php if (!empty($kids)): ?>
|
||||
<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 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 class="fc-month-calendar-wrapper">
|
||||
<div class="fc-month-header">
|
||||
|
||||
+493
-221
@@ -15,27 +15,24 @@ $activePage = "gift-list";
|
||||
$mainClass = "pf-gift-list";
|
||||
$pageCss = "/modules/gift-list/gift-list.css";
|
||||
|
||||
// --- 2. RÉCUPÉRATION DYNAMIQUE DES DONNÉES (Multi-tenant) ---
|
||||
$stmt = $pdo->query("SELECT name FROM pf_people WHERE role = 'enfant' AND is_active = 1 ORDER BY name ASC");
|
||||
// --- 2. RÉCUPÉRATION DYNAMIQUE DES DONNÉES ---
|
||||
// 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é'];
|
||||
|
||||
$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é'];
|
||||
|
||||
$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);
|
||||
|
||||
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');
|
||||
|
||||
$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 ---
|
||||
$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";
|
||||
@@ -43,8 +40,8 @@ $stmt = $pdo->prepare($sql);
|
||||
$stmt->execute(array_merge([$year], $allowedOccasionCodes));
|
||||
$gifts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$data = [];
|
||||
$adultsInView = [];
|
||||
$data = [];
|
||||
$adultsInView = [];
|
||||
|
||||
foreach ($gifts as $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++) {
|
||||
$a = $people[$i]; $b = $people[$j];
|
||||
$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]; }
|
||||
}
|
||||
}
|
||||
@@ -87,22 +84,26 @@ require __DIR__ . '/header.php';
|
||||
?>
|
||||
|
||||
<div class="pf-container cl-view-dynamic">
|
||||
|
||||
|
||||
<div class="cl-titlebar">
|
||||
<h1>🎁 <?= sprintf(tr('gift_main_title'), $year) ?></h1>
|
||||
<button class="btn btn-ghost btn-icon" id="btn-open-gift-settings" title="<?= tr('settings') ?>">⚙️</button>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<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 class="pf-filter-bar">
|
||||
<span class="pf-filter-icon">🔍</span>
|
||||
|
||||
|
||||
<div class="pf-multi-select" id="ms-child">
|
||||
<div class="pf-ms-trigger" onclick="toggleMS('ms-child-list', this)">
|
||||
👦 <span id="ms-child-label"><?= tr('gift_filter_all_children') ?></span>
|
||||
</div>
|
||||
<div class="pf-ms-dropdown" id="ms-child-list">
|
||||
<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') ?>
|
||||
</label>
|
||||
<?php foreach($children as $c): ?>
|
||||
@@ -117,7 +118,7 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
<div class="pf-ms-dropdown" id="ms-adult-list">
|
||||
<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') ?>
|
||||
</label>
|
||||
<?php foreach($allAdultsList as $a): ?>
|
||||
@@ -128,29 +129,27 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
|
||||
<section class="pf-section">
|
||||
<?php foreach ($activeOccasions as $occ):
|
||||
<?php foreach ($activeOccasions as $occ):
|
||||
$occCode = $occ['code'];
|
||||
// Affichage Brut de la BDD pour le nom de l'occasion
|
||||
$occName = $occ['name'];
|
||||
$occIcon = $occ['icon'] ?: '🎁';
|
||||
?>
|
||||
<div class="js-occ-section">
|
||||
<h2 class="cl-occasion-title">
|
||||
<?php if(!empty($occasionIcons[$occCode])): ?>
|
||||
<img src="<?= $occasionIcons[$occCode] ?>" class="cl-occasion-icon" alt="">
|
||||
<?php else: ?>
|
||||
<span>🎀</span>
|
||||
<?php endif; ?>
|
||||
<span class="cl-occasion-icon"><?= htmlspecialchars($occIcon) ?></span>
|
||||
<?= htmlspecialchars($occName) ?>
|
||||
</h2>
|
||||
|
||||
<?php foreach ($children as $child):
|
||||
<?php foreach ($children as $child):
|
||||
$childData = $data[$occCode][$child] ?? ['gifts' => [], 'totals' => []];
|
||||
?>
|
||||
<div class="pf-child-section js-child" data-name="<?= htmlspecialchars($child) ?>">
|
||||
<div class="pf-child-header">
|
||||
<h3>👦 <?= htmlspecialchars($child) ?></h3>
|
||||
<button class="pf-btn pf-btn-small btn-add-gift"
|
||||
data-child="<?= htmlspecialchars($child) ?>"
|
||||
data-occ="<?= htmlspecialchars($occCode) ?>"
|
||||
<button class="pf-btn pf-btn-small btn-add-gift"
|
||||
data-child="<?= htmlspecialchars($child) ?>"
|
||||
data-occ="<?= htmlspecialchars($occCode) ?>"
|
||||
data-adults="<?= htmlspecialchars(json_encode(array_values($allAdultsList))) ?>">
|
||||
+ <?= tr('gift_add_gift') ?>
|
||||
</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>
|
||||
<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; ?>
|
||||
</h4>
|
||||
<span class="pf-pill-adult">👤 <?= htmlspecialchars($g['adult_name']) ?></span>
|
||||
@@ -197,15 +196,15 @@ require __DIR__ . '/header.php';
|
||||
</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)): ?>
|
||||
<p class="gift-tricount-success">✅ <?= tr('gift_no_debt') ?></p>
|
||||
<?php else: ?>
|
||||
<ul class="pf-tricount-list">
|
||||
<?php foreach ($settlements as $s): ?>
|
||||
<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>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
@@ -226,7 +225,7 @@ require __DIR__ . '/header.php';
|
||||
<?php foreach ($people as $debtor): ?>
|
||||
<tr>
|
||||
<th class="sticky-col bg-body"><?= htmlspecialchars($debtor) ?></th>
|
||||
<?php foreach ($people as $creditor):
|
||||
<?php foreach ($people as $creditor):
|
||||
$val = $matrix[$debtor][$creditor] ?? 0;
|
||||
$display = ($debtor === $creditor) || $val == 0 ? '—' : number_format($val, 2, ',', ' ') . ' €';
|
||||
$cellClass = $val > 0 ? 'gift-cell-danger' : 'gift-cell-muted';
|
||||
@@ -242,26 +241,27 @@ require __DIR__ . '/header.php';
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 1. Modale CRUD Cadeau -->
|
||||
<div id="pf-gift-modal" class="pf-modal">
|
||||
<div class="pf-modal-content gift-modal-custom-content">
|
||||
<div class="gift-modal-custom-header">
|
||||
<h3 id="modalTitle" class="pf-modal-title gift-modal-custom-title">Cadeau</h3>
|
||||
<button type="button" class="btn-modal-close gift-modal-close-btn">×</button>
|
||||
</div>
|
||||
|
||||
|
||||
<form method="post" action="/modules/gift-list/save-gift.php" id="giftForm">
|
||||
<input type="hidden" name="year" value="<?= $year ?>">
|
||||
<input type="hidden" name="child_name" id="modalChild">
|
||||
<input type="hidden" name="occasion" id="modalOccasion">
|
||||
<input type="hidden" name="action" id="modalAction">
|
||||
<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_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_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="modal-footer gift-modal-custom-footer">
|
||||
<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>
|
||||
@@ -270,33 +270,86 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Modale Paramètres Fêtes -->
|
||||
<div class="gift-settings-backdrop" id="modal-gift-settings">
|
||||
<div class="gift-settings-modal">
|
||||
<div class="gift-settings-header">
|
||||
<h3>⚙️ <?= tr('gift_settings_title') ?? 'Configuration' ?></h3>
|
||||
<button class="gift-settings-close" id="btn-close-gift-settings">×</button>
|
||||
<!-- 2. Modale Paramètres (Occasions & Proches) -->
|
||||
<div id="modal-gift-settings" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 600px; width: 90%;">
|
||||
|
||||
<div class="pf-modal-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<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()">×</button>
|
||||
</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>
|
||||
<form id="form-add-occasion" class="gift-add-form">
|
||||
<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 id="gl-pane-occasions" class="gl-pane active" style="display: block; animation: fadeIn 0.3s ease;">
|
||||
<div id="occasions-list" style="margin-bottom: 20px;">⏳ <?= tr('loading') ?></div>
|
||||
|
||||
<div class="modal-footer gift-modal-custom-footer" style="padding-top: 1rem; border-top: 1px solid var(--border-light);">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
<hr style="border:0; border-top: 1px solid var(--border-light); margin: 20px 0;">
|
||||
|
||||
<h5 style="margin: 0 0 15px 0; font-size: 1rem; color: var(--text-main);"><?= tr('gift_settings_add_occ') ?></h5>
|
||||
<form id="form-add-occasion" style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<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>
|
||||
@@ -306,6 +359,7 @@ require __DIR__ . '/header.php';
|
||||
// ==========================================
|
||||
// 1. GESTION DU COMPOSANT MULTI-SELECT VANILLA
|
||||
// ==========================================
|
||||
|
||||
function toggleMS(listId, triggerEl) {
|
||||
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'); });
|
||||
@@ -324,7 +378,7 @@ document.addEventListener('click', e => {
|
||||
function handleMSChange(cb, type) {
|
||||
const container = document.getElementById('ms-' + type + '-list');
|
||||
if (!container) return;
|
||||
|
||||
|
||||
if (cb.value === 'all' && cb.checked) {
|
||||
container.querySelectorAll('input[type="checkbox"]:not([value="all"])').forEach(i => i.checked = false);
|
||||
} else if (cb.checked) {
|
||||
@@ -337,10 +391,10 @@ function handleMSChange(cb, type) {
|
||||
if (allCb) allCb.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const checkedSpecifics = container.querySelectorAll('input[type="checkbox"]:checked:not([value="all"])');
|
||||
const labelEl = document.getElementById('ms-' + type + '-label');
|
||||
|
||||
|
||||
if (labelEl) {
|
||||
if (checkedSpecifics.length === 0) {
|
||||
let defaultText = 'Tous';
|
||||
@@ -362,23 +416,23 @@ function applyGiftFilters() {
|
||||
const cList = document.getElementById('ms-child-list');
|
||||
const aList = document.getElementById('ms-adult-list');
|
||||
if (!cList || !aList) return;
|
||||
|
||||
|
||||
const allChildCb = cList.querySelector('input[value="all"]');
|
||||
const cAll = allChildCb ? allChildCb.checked : true;
|
||||
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 aAll = allAdultCb ? allAdultCb.checked : true;
|
||||
const aVals = Array.from(aList.querySelectorAll('input[type="checkbox"]:checked:not([value="all"])')).map(i => i.value);
|
||||
|
||||
document.querySelectorAll('.js-occ-section').forEach(occSec => {
|
||||
let occHasVisible = false;
|
||||
|
||||
|
||||
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);
|
||||
let childHasVisibleCard = false;
|
||||
|
||||
|
||||
if (matchChild) {
|
||||
childSec.querySelectorAll('.js-gift-card').forEach(card => {
|
||||
const aName = card.getAttribute('data-adult');
|
||||
@@ -386,23 +440,23 @@ function applyGiftFilters() {
|
||||
card.style.display = matchAdult ? 'flex' : 'none';
|
||||
if (matchAdult) childHasVisibleCard = true;
|
||||
});
|
||||
|
||||
|
||||
childSec.querySelectorAll('.js-pill-adult').forEach(pill => {
|
||||
const aName = pill.getAttribute('data-adult');
|
||||
const matchAdult = aAll || (aVals.indexOf(aName) !== -1);
|
||||
pill.style.opacity = matchAdult ? '1' : '0.2';
|
||||
});
|
||||
|
||||
|
||||
const empty = childSec.querySelector('.js-empty-state');
|
||||
if (empty) empty.style.display = childHasVisibleCard ? 'none' : 'block';
|
||||
|
||||
|
||||
childSec.style.display = 'block';
|
||||
occHasVisible = true;
|
||||
} else {
|
||||
childSec.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
occSec.style.display = occHasVisible ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
@@ -449,16 +503,16 @@ document.body.addEventListener('click', async function(e) {
|
||||
const occCode = btnAdd.dataset.occ;
|
||||
const adultsList = JSON.parse(btnAdd.dataset.adults || '[]');
|
||||
|
||||
document.getElementById('modalAction').value = 'create';
|
||||
document.getElementById('modalAction').value = 'create';
|
||||
document.getElementById('modalGiftId').value = '';
|
||||
document.getElementById('modalChild').value = childName;
|
||||
document.getElementById('modalChild').value = childName;
|
||||
document.getElementById('modalOccasion').value = occCode;
|
||||
document.getElementById('modalDesc').value = '';
|
||||
document.getElementById('modalDesc').value = '';
|
||||
document.getElementById('modalAmount').value = '';
|
||||
document.getElementById('modalLink').value = '';
|
||||
|
||||
|
||||
populateSelects(adultsList);
|
||||
|
||||
|
||||
if (adultSelect && payerSelect) {
|
||||
if (adultsList.indexOf('Laia') !== -1) {
|
||||
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';
|
||||
document.getElementById('modalTitle').textContent = titleStr.replace('%s', childName);
|
||||
|
||||
|
||||
if (modal) { modal.classList.add('open'); document.body.classList.add('no-scroll'); }
|
||||
return;
|
||||
}
|
||||
@@ -478,11 +532,11 @@ document.body.addEventListener('click', async function(e) {
|
||||
if (btnEdit) {
|
||||
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('modalChild').value = data.child_name;
|
||||
document.getElementById('modalChild').value = data.child_name;
|
||||
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('modalLink').value = data.product_link;
|
||||
|
||||
@@ -491,16 +545,16 @@ document.body.addEventListener('click', async function(e) {
|
||||
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<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 (!payerExists) payerSelect.appendChild(new Option(payerVal, payerVal));
|
||||
|
||||
|
||||
adultSelect.value = data.adult_name;
|
||||
payerSelect.value = payerVal;
|
||||
}
|
||||
|
||||
|
||||
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'); }
|
||||
return;
|
||||
}
|
||||
@@ -510,11 +564,11 @@ document.body.addEventListener('click', async function(e) {
|
||||
const giftId = btnDel.dataset.id;
|
||||
const msg = window.I18N && window.I18N['gift_confirm_delete'] ? window.I18N['gift_confirm_delete'] : 'Supprimer ce cadeau ?';
|
||||
if (!confirm(msg)) return;
|
||||
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'delete');
|
||||
fd.append('gift_id', giftId);
|
||||
|
||||
|
||||
try {
|
||||
await fetch('/modules/gift-list/save-gift.php', { method: 'POST', body: fd });
|
||||
window.location.reload();
|
||||
@@ -531,7 +585,7 @@ if (giftForm) {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('button[type="submit"]');
|
||||
const oldText = btn.innerText;
|
||||
btn.innerText = '...';
|
||||
btn.innerText = '...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
@@ -539,164 +593,382 @@ if (giftForm) {
|
||||
window.location.reload();
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
btn.innerText = oldText;
|
||||
btn.innerText = oldText;
|
||||
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() {
|
||||
document.getElementById('modal-gift-settings').classList.remove('show');
|
||||
document.getElementById('modal-gift-settings').classList.remove('open');
|
||||
document.body.classList.remove('no-scroll');
|
||||
}
|
||||
|
||||
const btnOpenSettings = document.getElementById('btn-open-gift-settings');
|
||||
if (btnOpenSettings) {
|
||||
btnOpenSettings.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
const modalSettings = document.getElementById('modal-gift-settings');
|
||||
if (modalSettings) {
|
||||
modalSettings.classList.add('show');
|
||||
document.body.classList.add('no-scroll');
|
||||
await loadGiftOccasions();
|
||||
}
|
||||
});
|
||||
}
|
||||
document.getElementById('btn-open-gift-settings')?.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById('modal-gift-settings').classList.add('open');
|
||||
document.body.classList.add('no-scroll');
|
||||
await loadSettingsData();
|
||||
});
|
||||
|
||||
document.getElementById('btn-close-gift-settings')?.addEventListener('click', closeGiftSettings);
|
||||
document.getElementById('btn-cancel-settings')?.addEventListener('click', closeGiftSettings);
|
||||
document.querySelectorAll('.btn-close-settings').forEach(btn => btn.addEventListener('click', closeGiftSettings));
|
||||
|
||||
// Fermeture au clic sur le fond (backdrop)
|
||||
const modalGiftSettings = document.getElementById('modal-gift-settings');
|
||||
if (modalGiftSettings) {
|
||||
modalGiftSettings.addEventListener('click', (e) => {
|
||||
if (e.target === modalGiftSettings) {
|
||||
closeGiftSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
function switchGLTab(tabId) {
|
||||
// Gestion du contenu (Afficher/Cacher)
|
||||
document.getElementById('gl-pane-occasions').style.display = (tabId === 'occasions') ? 'block' : 'none';
|
||||
document.getElementById('gl-pane-proches').style.display = (tabId === 'proches') ? 'block' : 'none';
|
||||
|
||||
// 2. Chargement dynamique de la liste
|
||||
async function loadGiftOccasions() {
|
||||
const container = document.getElementById('occasions-list-container');
|
||||
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';
|
||||
// Gestion du style des boutons d'onglet
|
||||
const btnOcc = document.getElementById('tab-btn-occasions');
|
||||
const btnProche = document.getElementById('tab-btn-proches');
|
||||
|
||||
container.innerHTML = `<div class="gift-loader">${txtLoading}</div>`;
|
||||
|
||||
try {
|
||||
const res = await fetch('/modules/gift-list/api-settings.php?action=get_occasions');
|
||||
const text = await res.text();
|
||||
const data = JSON.parse(text);
|
||||
|
||||
if (!data.ok) throw new Error(data.error);
|
||||
|
||||
if (data.data.length === 0) {
|
||||
container.innerHTML = `<em>${txtEmpty}</em>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="gift-occasions-grid">';
|
||||
data.data.forEach(occ => {
|
||||
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>`;
|
||||
if (tabId === 'occasions') {
|
||||
btnOcc.style.background = 'var(--bg-panel)';
|
||||
btnOcc.style.color = 'var(--primary)';
|
||||
btnOcc.style.boxShadow = 'var(--shadow-sm)';
|
||||
|
||||
btnProche.style.background = 'transparent';
|
||||
btnProche.style.color = 'var(--text-muted)';
|
||||
btnProche.style.boxShadow = 'none';
|
||||
} else {
|
||||
btnProche.style.background = 'var(--bg-panel)';
|
||||
btnProche.style.color = 'var(--primary)';
|
||||
btnProche.style.boxShadow = 'var(--shadow-sm)';
|
||||
|
||||
btnOcc.style.background = 'transparent';
|
||||
btnOcc.style.color = 'var(--text-muted)';
|
||||
btnOcc.style.boxShadow = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Soumission du formulaire d'AJOUT
|
||||
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;
|
||||
let hasSettingsChanged = false;
|
||||
|
||||
const formData = new FormData(formAddOccasion);
|
||||
formData.append('action', 'add_occasion');
|
||||
async function loadSettingsData() {
|
||||
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 {
|
||||
const res = await fetch('/modules/gift-list/api-settings.php', { method: 'POST', body: formData });
|
||||
const text = await res.text();
|
||||
const data = JSON.parse(text);
|
||||
function renderOccasions(occasions) {
|
||||
const container = document.getElementById('occasions-list');
|
||||
if (!occasions.length) { container.innerHTML = `<em class="gift-empty-state">Vide</em>`; return; }
|
||||
|
||||
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();
|
||||
await loadGiftOccasions(); // Rafraîchit juste la liste dans la modale !
|
||||
|
||||
} catch (err) {
|
||||
alert("Erreur: " + err.message);
|
||||
} finally {
|
||||
btn.innerText = oldText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
<div style="display: flex; gap: 4px; margin-left: auto;">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 4. Soumission GLOBALE (Bouton Enregistrer) pour les Checkboxes
|
||||
const formSaveToggles = document.getElementById('form-save-toggles');
|
||||
if (formSaveToggles) {
|
||||
formSaveToggles.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = formSaveToggles.querySelector('button[type="submit"]');
|
||||
const oldText = btn.innerText;
|
||||
btn.innerText = '⏳';
|
||||
btn.disabled = true;
|
||||
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);
|
||||
|
||||
// On récolte l'état de toutes les checkboxes
|
||||
const states = [];
|
||||
formSaveToggles.querySelectorAll('.occ-toggle-cb').forEach(cb => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
// On met à jour en BDD puis on recharge la liste !
|
||||
await pachaFetch('/modules/gift-list/api-settings.php', { method: 'POST', body: fd });
|
||||
loadSettingsData();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 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>
|
||||
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
+87
-1
@@ -280,6 +280,56 @@ return [
|
||||
|
||||
'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_school_holidays' => 'Vacances Escolars',
|
||||
'leg_public_holiday' => 'Festiu',
|
||||
@@ -592,8 +642,23 @@ return [
|
||||
'bud_bar_max_cap' => "Capacitat Màx.",
|
||||
'bud_edit_title' => 'Modificar la transacció',
|
||||
'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',
|
||||
|
||||
// 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 ---
|
||||
'bud_import_csv' => "Importar CSV",
|
||||
@@ -652,6 +717,9 @@ return [
|
||||
'bud_sav_modal_title_add' => 'Introduir un mes',
|
||||
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
|
||||
'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 ---
|
||||
'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_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_target_ph' => 'Ex: 150',
|
||||
'bud_prev_transfer_to' => 'Cap a',
|
||||
|
||||
// --- MODULE BUDGET : AVANCES & TRICOUNT ---
|
||||
'bud_adv_title' => 'Avançaments i Tricount Familiar',
|
||||
@@ -820,6 +890,22 @@ return [
|
||||
'btn_save' => 'Desar',
|
||||
'btn_add' => 'Afegir',
|
||||
'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
|
||||
|
||||
@@ -281,6 +281,56 @@ return [
|
||||
|
||||
'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_school_holidays' => 'School holidays',
|
||||
'leg_public_holiday' => 'Public holiday',
|
||||
@@ -532,6 +582,21 @@ return [
|
||||
'bs_csv_empty_col' => 'Empty',
|
||||
'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 ---
|
||||
'bud_rem_school' => 'School (estimated remaining)',
|
||||
'bud_rem_fmcg' => 'Groceries (estimated remaining)',
|
||||
@@ -630,6 +695,9 @@ return [
|
||||
'bud_reg_none' => 'None',
|
||||
'bud_recap_confirm_delete' => 'Are you sure you want to delete this item?',
|
||||
'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 ---
|
||||
'bud_sav_add_month' => 'Enter a month',
|
||||
@@ -753,6 +821,8 @@ return [
|
||||
'delete' => 'Delete',
|
||||
'btn_cancel' => 'Cancel',
|
||||
'btn_save' => 'Save',
|
||||
'bud_prev_target_ph' => 'E.g., 150',
|
||||
'bud_prev_transfer_to' => 'To',
|
||||
|
||||
// ==========================================
|
||||
// MODULE: GIFTS (gift-list)
|
||||
@@ -806,6 +876,22 @@ return [
|
||||
'btn_add' => 'Add',
|
||||
'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
|
||||
// ==========================================
|
||||
|
||||
+88
-1
@@ -282,7 +282,56 @@ return [
|
||||
'fc_table_renewal' => 'Renouv.',
|
||||
|
||||
'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',
|
||||
@@ -607,6 +656,21 @@ return [
|
||||
'bud_to_define_js' => 'à définir',
|
||||
'bud_select_beneficiary' => 'Choisir un revenu attendu',
|
||||
'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 ---
|
||||
'bud_recap_monthly_title' => 'Récapitulatif Mensuel',
|
||||
@@ -649,6 +713,10 @@ return [
|
||||
'bud_reg_none' => 'Aucune',
|
||||
'bud_recap_confirm_delete' => 'Voulez-vous vraiment supprimer cet élément ?',
|
||||
'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 ---
|
||||
'bud_sav_add_month' => 'Saisir un mois',
|
||||
@@ -704,6 +772,7 @@ return [
|
||||
'bud_sav_selection' => 'Sélection :',
|
||||
'bud_sav_ph_name' => 'Nom du compte',
|
||||
|
||||
|
||||
// JS & Erreurs
|
||||
'bud_sav_confirm_delete_month' => 'Voulez-vous vraiment supprimer toutes les données de %m pour %o ?',
|
||||
'bud_sav_prompt_duplicate' => "Voulez-vous dupliquer les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
|
||||
@@ -766,6 +835,8 @@ return [
|
||||
'bud_adv_ph_desc' => 'Ex: Courses, Facture Électricité...',
|
||||
'bud_adv_confirm_resolve' => 'Confirmer le remboursement complet de 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)
|
||||
@@ -818,6 +889,22 @@ return [
|
||||
'btn_save' => 'Enregistrer',
|
||||
'btn_add' => 'Ajouter',
|
||||
'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
|
||||
|
||||
+93
-557
@@ -1,644 +1,180 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
* 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
|
||||
|
||||
*/
|
||||
|
||||
|
||||
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
|
||||
|
||||
|
||||
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||
|
||||
$db_user = getenv('DB_USER') ?: 'househub';
|
||||
|
||||
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
|
||||
|
||||
echo "<h1>🚀 Début de la 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 {
|
||||
|
||||
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
|
||||
|
||||
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
|
||||
foreach ($families as $family) {
|
||||
|
||||
$dbName = $family['db_name'];
|
||||
|
||||
echo "<h2>Famille : {$family['name']} ($dbName)</h2>";
|
||||
|
||||
|
||||
echo "<h2>🏡 Famille : {$family['name']} ($dbName)</h2>";
|
||||
|
||||
try {
|
||||
|
||||
$pdo = new PDO(
|
||||
|
||||
"mysql:host=$db_host;dbname=$dbName;charset=utf8mb4",
|
||||
|
||||
$db_user, $db_pass,
|
||||
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// 1. UPDATE DE pf_people (Juste la date de naissance et l'état actif)
|
||||
|
||||
// 2. EXÉCUTION DU SQL (CRÉATION TABLES MANQUANTES + DONNÉES PAR DÉFAUT)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
$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 {
|
||||
|
||||
$pdo->exec("ALTER TABLE `pf_vehicles` ADD COLUMN `consumption` decimal(4,2) DEFAULT NULL COMMENT 'Consommation L/100km' AFTER `fuel_type`");
|
||||
|
||||
echo "✅ Colonne consumption ajoutée au Garage.<br>";
|
||||
|
||||
$pdo->exec($sqlContent);
|
||||
echo "✅ Structure de base validée (les tables manquantes ont été créées).<br>";
|
||||
} catch (PDOException $e) {
|
||||
|
||||
echo "➖ pf_vehicles déjà à jour.<br>";
|
||||
|
||||
echo "⚠️ Avertissement SQL brut : " . $e->getMessage() . "<br>";
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// 6. MIGRATION DE L'HISTORIQUE CALENDRIER (FINI LE CODÉ EN DUR)
|
||||
|
||||
// 3. DIFFING (AJOUT DYNAMIQUE DES COLONNES MANQUANTES)
|
||||
// ---------------------------------------------------------
|
||||
$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");
|
||||
|
||||
$helperId = $stmtHelper->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'");
|
||||
|
||||
echo "✅ Historique des absences Nounou migré dynamiquement.<br>";
|
||||
|
||||
foreach ($expectedCols as $colName => $colDef) {
|
||||
if (!in_array($colName, $existingCols)) {
|
||||
$pdo->exec("ALTER TABLE `$table` ADD COLUMN `$colName` $colDef");
|
||||
echo "➕ Nouvelle colonne ajoutée : <b>$table.$colName</b><br>";
|
||||
$colsAdded++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($colsAdded === 0) {
|
||||
echo "✅ Toutes les colonnes de toutes les tables sont déjà à jour.<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>";
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
|
||||
// 7. MIGRATION DÉFINITIVE : STRUCTURE DU BUDGET
|
||||
|
||||
// 4. MIGRATION DES DONNÉES (TRANSFORMATIONS HISTORIQUES)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// A. Changement du type de la colonne (ENUM -> VARCHAR)
|
||||
|
||||
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)
|
||||
$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;
|
||||
|
||||
|
||||
|
||||
// 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");
|
||||
|
||||
|
||||
|
||||
// 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 = '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%')");
|
||||
|
||||
|
||||
|
||||
// 3. On sécurise les éventuelles estimations restantes dans AUTRES
|
||||
|
||||
$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'");
|
||||
|
||||
|
||||
|
||||
// C. Nettoyage des virgules orphelines dans mapping_keywords (suppression des vieux hacks)
|
||||
|
||||
// D. Nettoyage des virgules orphelines dans mapping_keywords
|
||||
$budgetCodes = ['INCOME', 'FMCG', 'FUEL', 'SCHOOL', 'HEALTH', 'FIXED', 'SAVINGS', 'AUTRES'];
|
||||
|
||||
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 = TRIM(BOTH ',' FROM REPLACE(REPLACE(mapping_keywords, ' ', ''), ',,', ','))");
|
||||
|
||||
|
||||
|
||||
// D. Migration de l'historique des dépenses et des règles (Anciens libellés FR/EN -> Nouveaux Codes Dynamiques)
|
||||
|
||||
// E. Migration de l'historique des dépenses et des règles d'import
|
||||
$budgetMapping = [
|
||||
|
||||
'Income' => 'INCOME',
|
||||
|
||||
'FMCG' => 'FMCG',
|
||||
|
||||
'Essence' => 'FUEL',
|
||||
|
||||
'School' => 'SCHOOL',
|
||||
|
||||
'Frais' => 'FIXED',
|
||||
|
||||
'LivretA' => 'SAVINGS',
|
||||
|
||||
'Autres' => 'AUTRES'
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
$stmtExp = $pdo->prepare("UPDATE pf_expenses SET category = ? WHERE category = ?");
|
||||
|
||||
$stmtRules = $pdo->prepare("UPDATE pf_import_rules SET category = ? WHERE category = ?");
|
||||
|
||||
|
||||
|
||||
$expCount = 0;
|
||||
|
||||
$rulesCount = 0;
|
||||
|
||||
|
||||
|
||||
foreach ($budgetMapping as $old => $newCode) {
|
||||
|
||||
$stmtExp->execute([$newCode, $old]);
|
||||
|
||||
$expCount += $stmtExp->rowCount();
|
||||
|
||||
|
||||
|
||||
$stmtRules->execute([$newCode, $old]);
|
||||
|
||||
$rulesCount += $stmtRules->rowCount();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
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>";
|
||||
|
||||
|
||||
echo "✅ Transformations Data terminées ($itemsMigres prévisions remappées).<hr>";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
|
||||
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>";
|
||||
|
||||
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<hr>";
|
||||
}
|
||||
} // FIN DE LA BOUCLE FOREACH FAMILLES
|
||||
|
||||
} // FIN DE LA BOUCLE FOREACH
|
||||
|
||||
|
||||
|
||||
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>";
|
||||
|
||||
|
||||
echo "<h1>🎉 Synchronisation et Migration terminées avec succès !</h1>";
|
||||
|
||||
} catch (Exception $e) {
|
||||
|
||||
die("❌ Erreur fatale Meta DB : " . $e->getMessage());
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
?>
|
||||
+542
-232
@@ -37,6 +37,7 @@
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
/* Thèmes des tableaux */
|
||||
.nens-title.theme-pol {
|
||||
color: #1e3a8a !important;
|
||||
background: #eaf2ff !important;
|
||||
@@ -147,6 +148,7 @@
|
||||
z-index: 10 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.month-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
@@ -261,13 +263,13 @@ td:hover .btn-cell-delete {
|
||||
padding: 18px 16px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-outer-spin-button,
|
||||
input[type="number"]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
@@ -373,7 +375,7 @@ input[type="number"] {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Configuration Chromatique Parent 1 */
|
||||
/* Configuration Chromatique Parent 1 & 2 */
|
||||
.txt-p1,
|
||||
.txt-alex {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Configuration Chromatique Parent 2 */
|
||||
.txt-p2 {
|
||||
color: color-mix(in srgb, var(--p2-main, #f59e0b) 85%, #000000) !important;
|
||||
font-weight: bold;
|
||||
@@ -503,8 +504,6 @@ input.txt-p2:focus {
|
||||
color: var(--text-main) !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 {
|
||||
text-align: left !important;
|
||||
font-weight: bold !important;
|
||||
@@ -762,7 +761,6 @@ tr:hover .row-actions {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* --- Style de base du bouton de validation (Générique) --- */
|
||||
.recap-table .pf-btn.btn-small {
|
||||
background: #ffffff !important;
|
||||
font-size: 0.7rem !important;
|
||||
@@ -775,8 +773,6 @@ tr:hover .row-actions {
|
||||
transform: none !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Couleurs dynamiques adaptées au Parent 1 (ex: Féfé) */
|
||||
.col-p1 .pf-btn.btn-small {
|
||||
color: var(--p1-main, #0891b2) !important;
|
||||
border: 1px solid var(--p1-main, #0891b2) !important;
|
||||
@@ -788,8 +784,6 @@ tr:hover .row-actions {
|
||||
#ffffff
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Couleurs dynamiques adaptées au Parent 2 (ex: Laia) */
|
||||
.col-p2 .pf-btn.btn-small {
|
||||
color: var(--p2-main, #f59e0b) !important;
|
||||
border: 1px solid var(--p2-main, #f59e0b) !important;
|
||||
@@ -802,7 +796,6 @@ tr:hover .row-actions {
|
||||
) !important;
|
||||
}
|
||||
|
||||
/* Grand Totaux Structuraux */
|
||||
.row-grand-total td {
|
||||
background-color: #1e293b;
|
||||
color: white;
|
||||
@@ -1094,7 +1087,544 @@ body.sum-mode-active .sum-target {
|
||||
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) {
|
||||
.budget-view {
|
||||
padding: 10px !important;
|
||||
@@ -1268,223 +1798,3 @@ body.sum-mode-active .sum-target {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,16 @@ try {
|
||||
|
||||
// 4. Salaires (Année en cours)
|
||||
$currentYear = (int)date('Y');
|
||||
$salaries = $pdo->query("SELECT * FROM pf_salary_config WHERE year = $currentYear ORDER BY person ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$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)
|
||||
$foyerData = $pdo->query("SELECT currency, csv_mapping FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<?php
|
||||
// modules/budget/views/budget_prev.php
|
||||
|
||||
// 1. Chargement dynamique des parents
|
||||
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
|
||||
// 1. Chargement dynamique des ADULTES de la famille
|
||||
$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);
|
||||
|
||||
$p1_name = $budgetParents[0]['name'] ?? 'Parent 1';
|
||||
$p2_name = $budgetParents[1]['name'] ?? 'Parent 2';
|
||||
// Sécurité : au cas où aucun adulte n'est trouvé, on évite un crash
|
||||
if (empty($budgetParents)) {
|
||||
$budgetParents[] = ['id' => 0, 'name' => 'Utilisateur', 'color' => '#0891b2'];
|
||||
}
|
||||
|
||||
$currentYear = date('Y');
|
||||
|
||||
// Cartographie dynamique
|
||||
// Cartographie dynamique (Supporte 1, 2 ou N adultes)
|
||||
$parentMapping = [];
|
||||
foreach ($budgetParents as $index => $parent) {
|
||||
$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
|
||||
$salaryConfig = [];
|
||||
$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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (!isset($salaryConfig[$map['name']])) {
|
||||
$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;
|
||||
}
|
||||
|
||||
// --- 🧠 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");
|
||||
$activeAdvances = $stmtAdvancesList->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$advTotal = [$p1_name => 0, $p2_name => 0];
|
||||
$livretTotal = [$p1_name => 0, $p2_name => 0];
|
||||
$advTotal = [];
|
||||
$livretTotal = [];
|
||||
$labelsCC = [];
|
||||
$labelsLivret = [];
|
||||
|
||||
$labelsCC = [$p1_name => [], $p2_name => []];
|
||||
$labelsLivret = [$p1_name => [], $p2_name => []];
|
||||
foreach ($parentMapping as $map) {
|
||||
$advTotal[$map['name']] = 0;
|
||||
$livretTotal[$map['name']] = 0;
|
||||
$labelsCC[$map['name']] = [];
|
||||
$labelsLivret[$map['name']] = [];
|
||||
}
|
||||
|
||||
foreach ($activeAdvances as $adv) {
|
||||
$p = $adv['payer'];
|
||||
$amt = (float)$adv['amount'];
|
||||
$labelStr = htmlspecialchars($adv['description']) . ' (' . number_format($amt, 0, ',', ' ') . '€)';
|
||||
|
||||
// Si un ancien nom traîne en base, on l'initialise
|
||||
if (!isset($advTotal[$p])) {
|
||||
$advTotal[$p] = 0; $livretTotal[$p] = 0;
|
||||
$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 = '';
|
||||
if (($advTotal[$p1_name] ?? 0) > ($advTotal[$p2_name] ?? 0)) $owedTo = $p1_name;
|
||||
elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_name;
|
||||
if (count($parentMapping) >= 2) {
|
||||
$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);
|
||||
$rowClass = $isIndicative ? 'row-indicative' : '';
|
||||
$inputClass = $isIndicative ? 'ignore-calc' : '';
|
||||
|
||||
|
||||
// Retrocompatibilité pour les anciens noms d'économies hardcodés
|
||||
$catDisplayName = $cat['name'];
|
||||
if ($catDisplayName === 'Eco P1') $catDisplayName = 'Eco ' . $p1_name;
|
||||
if ($catDisplayName === 'Eco P2') $catDisplayName = 'Eco ' . $p2_name;
|
||||
if (isset($parentMapping[0]) && $catDisplayName === 'Eco P1') $catDisplayName = 'Eco ' . $parentMapping[0]['name'];
|
||||
if (isset($parentMapping[1]) && $catDisplayName === 'Eco P2') $catDisplayName = 'Eco ' . $parentMapping[1]['name'];
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>">
|
||||
<td class="col-sticky">
|
||||
@@ -323,6 +344,7 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</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="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>
|
||||
@@ -332,57 +354,39 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</div>
|
||||
|
||||
<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;">
|
||||
<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>
|
||||
<?php foreach ($parentMapping as $idx => $map):
|
||||
$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="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>
|
||||
<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])): ?>
|
||||
<div style="font-size: 0.75rem; color: #0e7490; margin-top: 6px; line-height: 1.4; font-style: italic;">
|
||||
<?= implode(', ', $labelsCC[$p1_name]) ?>
|
||||
<?php if (!empty($labelsCC[$pName])): ?>
|
||||
<div style="font-size: 0.75rem; color: <?= $colorMain ?>; margin-top: 6px; line-height: 1.4; font-style: italic;">
|
||||
<?= implode(', ', $labelsCC[$pName]) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if(($livretTotal[$p1_name] ?? 0) > 0): ?>
|
||||
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed rgba(8, 145, 178, 0.3);">
|
||||
<div style="font-size: 1.2rem; font-weight: 800; color: #4338ca; font-family: monospace;">+ <?= number_format($livretTotal[$p1_name], 2, ',', ' ') ?> €</div>
|
||||
<small style="color: #4338ca; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
|
||||
<?php if (!empty($labelsLivret[$p1_name])): ?>
|
||||
<div style="font-size: 0.75rem; color: #3730a3; margin-top: 4px; line-height: 1.4;">
|
||||
<?= implode(', ', $labelsLivret[$p1_name]) ?>
|
||||
</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]) ?>
|
||||
<?php if(($livretTotal[$pName] ?? 0) > 0): ?>
|
||||
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed <?= $bdClass ?>;">
|
||||
<div style="font-size: 1.2rem; font-weight: 800; color: <?= $colorDark ?>; font-family: monospace;">+ <?= number_format($livretTotal[$pName], 2, ',', ' ') ?> €</div>
|
||||
<small style="color: <?= $colorDark ?>; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
|
||||
<?php if (!empty($labelsLivret[$pName])): ?>
|
||||
<div style="font-size: 0.75rem; color: <?= $colorDark ?>; margin-top: 4px; line-height: 1.4;">
|
||||
<?= implode(', ', $labelsLivret[$pName]) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</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="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>
|
||||
</thead>
|
||||
<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>
|
||||
<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>
|
||||
<?= htmlspecialchars($adv['description']) ?>
|
||||
<?php if ($adv['from_savings']): ?>
|
||||
@@ -441,11 +454,19 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; // Fin du Tricount conditionnel ?>
|
||||
|
||||
<?php
|
||||
$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;
|
||||
foreach($cats as $c) {
|
||||
$t = trim($c['transfer_dest'] ?? '');
|
||||
@@ -506,33 +527,38 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="addCatModal" class="pf-modal">
|
||||
<div class="pf-modal-content">
|
||||
<div class="prev-header-left">
|
||||
<h3 class="pf-modal-title"><?= tr('bud_prev_new_line_title') ?></h3>
|
||||
<button onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');">×</button>
|
||||
<div id="addCatModal" class="pf-modal" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');">
|
||||
<div class="pf-modal-content" onclick="event.stopPropagation()">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<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>
|
||||
|
||||
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
|
||||
<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>
|
||||
<input type="text" name="name" class="pf-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?? 'Objectif Mensuel (€)' ?></label>
|
||||
<input type="number" step="1" name="target" class="pf-input" placeholder="Ex: 150">
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<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 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">
|
||||
<option value="" selected>-- <?= tr('bud_prev_none') ?? 'Aucune' ?> --</option>
|
||||
<option value="vers L.Pol">vers L.Pol</option>
|
||||
<option value="vers L.Pep">vers L.Pep</option>
|
||||
<option value="vers L.Perso">vers L.Perso</option>
|
||||
<option value="vers commune">vers commune</option>
|
||||
<option value="">-- <?= tr('bud_prev_none') ?> --</option>
|
||||
<?php foreach ($bankAccounts as $accName): ?>
|
||||
<option value="vers <?= htmlspecialchars($accName) ?>"><?= tr('bud_prev_transfer_to') ?> <?= htmlspecialchars($accName) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label>
|
||||
<select name="holiday_id" class="pf-input">
|
||||
<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; ?>
|
||||
</select>
|
||||
</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="submit" class="pf-btn"><?= tr('bud_add_title') ?></button>
|
||||
</div>
|
||||
@@ -549,34 +576,39 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editCatModal" class="pf-modal">
|
||||
<div class="pf-modal-content">
|
||||
<div class="prev-header-left">
|
||||
<h3 class="pf-modal-title"><?= tr('bud_prev_edit_line_title') ?></h3>
|
||||
<button onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');">×</button>
|
||||
<div id="editCatModal" class="pf-modal" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');">
|
||||
<div class="pf-modal-content" onclick="event.stopPropagation()">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<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>
|
||||
|
||||
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
|
||||
<input type="hidden" name="action" value="update_category">
|
||||
<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>
|
||||
<input type="text" name="name" id="edit_cat_name" class="pf-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?? 'Objectif Mensuel (€)' ?></label>
|
||||
<input type="number" step="1" name="target" id="edit_cat_target" class="pf-input" placeholder="Ex: 150">
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<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 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">
|
||||
<option value="">-- <?= tr('bud_prev_none') ?? 'Aucune' ?> --</option>
|
||||
<option value="vers L.Pol">vers L.Pol</option>
|
||||
<option value="vers L.Pep">vers L.Pep</option>
|
||||
<option value="vers L.Perso">vers L.Perso</option>
|
||||
<option value="vers commune">vers commune</option>
|
||||
<option value="">-- <?= tr('bud_prev_none') ?> --</option>
|
||||
<?php foreach ($bankAccounts as $accName): ?>
|
||||
<option value="vers <?= htmlspecialchars($accName) ?>"><?= tr('bud_prev_transfer_to') ?> <?= htmlspecialchars($accName) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label>
|
||||
<select name="holiday_id" id="edit_cat_holiday" class="pf-input">
|
||||
<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; ?>
|
||||
</select>
|
||||
</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="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||
</div>
|
||||
@@ -604,8 +637,9 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('bud_adv_who_paid') ?></label>
|
||||
<select name="payer" class="pf-input" required>
|
||||
<option value="<?= htmlspecialchars($p1_name) ?>"><?= htmlspecialchars($p1_name) ?></option>
|
||||
<option value="<?= htmlspecialchars($p2_name) ?>"><?= htmlspecialchars($p2_name) ?></option>
|
||||
<?php foreach ($parentMapping as $map): ?>
|
||||
<option value="<?= htmlspecialchars($map['name']) ?>"><?= htmlspecialchars($map['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<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">
|
||||
<label class="pf-label"><?= tr('bud_adv_who_paid') ?></label>
|
||||
<select name="payer" id="edit_adv_payer" class="pf-input" required>
|
||||
<option value="<?= htmlspecialchars($p1_name) ?>"><?= htmlspecialchars($p1_name) ?></option>
|
||||
<option value="<?= htmlspecialchars($p2_name) ?>"><?= htmlspecialchars($p2_name) ?></option>
|
||||
<?php foreach ($parentMapping as $map): ?>
|
||||
<option value="<?= htmlspecialchars($map['name']) ?>"><?= htmlspecialchars($map['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
@@ -684,20 +719,6 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
|
||||
<script>
|
||||
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.parentMapping = <?= json_encode($parentMapping) ?>;
|
||||
window.CONFIG.CURRENCY = '<?= defined('CURRENCY') ? CURRENCY : "€" ?>';
|
||||
@@ -746,7 +767,7 @@ function duplicateMonth() {
|
||||
const parentMap = window.CONFIG.parentMapping;
|
||||
|
||||
if (!sourceDateStr) {
|
||||
alert(window.I18N['bud_prev_err_no_history']);
|
||||
alert(tr('bud_prev_err_no_history'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -757,7 +778,7 @@ function duplicateMonth() {
|
||||
|
||||
const sourceName = formatMonth(sourceDateStr);
|
||||
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;
|
||||
|
||||
@@ -883,10 +904,9 @@ function updateSummaryTable() {
|
||||
function saveData(action, data) {
|
||||
const formData = new FormData();
|
||||
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]);
|
||||
|
||||
// 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 });
|
||||
}
|
||||
|
||||
@@ -894,7 +914,7 @@ async function validateTransfers(personCss, month) {
|
||||
const parentMap = window.CONFIG.parentMapping.find(m => m.css === personCss);
|
||||
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;
|
||||
|
||||
const formData = new FormData();
|
||||
@@ -908,10 +928,10 @@ async function validateTransfers(personCss, month) {
|
||||
if(result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(window.I18N['bud_err_tech'] + " : " + result.error);
|
||||
alert(tr('bud_err_tech') + " : " + result.error);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
} else {
|
||||
alert(window.I18N['bud_err_tech'] + " : " + data.error);
|
||||
alert(tr('bud_err_tech') + " : " + data.error);
|
||||
}
|
||||
} catch(e) {
|
||||
alert(window.I18N['bud_err_tech']);
|
||||
alert(tr('bud_err_tech'));
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
formData.append('action', 'delete_category');
|
||||
formData.append('id', id);
|
||||
@@ -980,29 +1000,28 @@ async function handleAdvanceSubmit(event, form) {
|
||||
|
||||
const endpoint = form.getAttribute('action');
|
||||
const formData = new FormData(form);
|
||||
formData.append('ajax', '1'); // <-- Correction de l'erreur d'empty string
|
||||
formData.append('ajax', '1');
|
||||
|
||||
try {
|
||||
// Utilisation robuste de pachaFetch
|
||||
const result = await pachaFetch(endpoint, { method: 'POST', body: formData });
|
||||
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} 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.innerHTML = oldText;
|
||||
}
|
||||
} catch (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.innerHTML = oldText;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
fd.append('action', 'resolve_advance');
|
||||
fd.append('id', id);
|
||||
@@ -1017,7 +1036,7 @@ async function executeResolveAdvance(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();
|
||||
fd.append('action', 'delete_advance');
|
||||
fd.append('id', id);
|
||||
@@ -1087,7 +1106,6 @@ document.addEventListener('click', function(e) {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', recalcAllAllocations);
|
||||
|
||||
// Soumission standard des modales de catégories avec pachaFetch
|
||||
document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => {
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -1097,7 +1115,7 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = '⏳ ...';
|
||||
const formData = new FormData(form);
|
||||
formData.append('ajax', '1'); // Toujours sécuriser
|
||||
formData.append('ajax', '1');
|
||||
const actionUrl = form.getAttribute('action');
|
||||
|
||||
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');
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
||||
alert((tr('bud_err_tech') || 'Erreur') + " : " + (result.error || "Inconnue"));
|
||||
}
|
||||
} catch (error) {
|
||||
alert(window.I18N['bud_err_tech'] || "Une erreur technique est survenue.");
|
||||
alert(tr('bud_err_tech') || "Une erreur technique est survenue.");
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = originalText;
|
||||
|
||||
@@ -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>
|
||||
@@ -13,7 +13,7 @@ let localCareModes = [];
|
||||
async function openCalendarSettings() {
|
||||
try {
|
||||
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);
|
||||
|
||||
@@ -63,29 +63,34 @@ function closeCalendarSettings() {
|
||||
document.body.classList.remove("no-scroll");
|
||||
}
|
||||
|
||||
function switchCalendarTab(tabName) {
|
||||
document
|
||||
.querySelectorAll(".cal-settings-pane")
|
||||
.forEach((p) => (p.style.display = "none"));
|
||||
function switchCalendarTab(tabId) {
|
||||
// 1. On réinitialise l'affichage
|
||||
document
|
||||
.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") {
|
||||
document.getElementById("cal-pane-foyer").style.display = "block";
|
||||
document.getElementById("tab-btn-foyer").classList.add("active");
|
||||
} else if (tabName === "affichage") {
|
||||
document.getElementById("cal-pane-affichage").style.display = "block";
|
||||
document.getElementById("tab-btn-affichage").classList.add("active");
|
||||
} else {
|
||||
document.getElementById("cal-pane-membres").style.display = "block";
|
||||
document.getElementById("tab-btn-membres").classList.add("active");
|
||||
loadMemberConfigView();
|
||||
// 2. On active l'onglet cliqué
|
||||
document.getElementById(`tab-btn-${tabId}`).classList.add("active");
|
||||
document.getElementById(`cal-pane-${tabId}`).style.display = "block";
|
||||
|
||||
// 3. Actions spécifiques
|
||||
if (tabId === "foyer") {
|
||||
loadLeaveCatalog();
|
||||
} else if (tabId === "membres") {
|
||||
// On charge la vue du membre sélectionné par défaut dans la liste
|
||||
if (document.getElementById("selectCalMember").value) {
|
||||
loadMemberConfigView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderCareModeTags() {
|
||||
const container = document.getElementById("careModesContainer");
|
||||
if (!container) return; // 🛑 LA BARRIÈRE DE SÉCURITÉ EST ICI
|
||||
|
||||
container.innerHTML = localCareModes
|
||||
.map(
|
||||
(mode, i) => `
|
||||
@@ -97,8 +102,11 @@ function renderCareModeTags() {
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Ajoutons la même sécurité pour l'ajout au cas où :
|
||||
function addCareModeTag() {
|
||||
const input = document.getElementById("inputNewCareMode");
|
||||
if (!input) return; // 🛑 SÉCURITÉ
|
||||
|
||||
const val = input.value.trim();
|
||||
if (val && !localCareModes.includes(val)) {
|
||||
localCareModes.push(val);
|
||||
@@ -124,7 +132,7 @@ async function submitCalFoyer(e) {
|
||||
formData.append("care_modes", JSON.stringify(localCareModes));
|
||||
|
||||
const res = await pachaFetch(
|
||||
"/modules/family-calendar/includes/api/settings.php",
|
||||
"/modules/family-calendar/includes/api/calendar-settings.php",
|
||||
{ method: "POST", body: formData },
|
||||
);
|
||||
if (!res.success) throw new Error(res.error);
|
||||
@@ -142,139 +150,195 @@ async function submitCalFoyer(e) {
|
||||
}
|
||||
}
|
||||
|
||||
window.addLeaveRowToMatrix = function () {
|
||||
const tbody = document.getElementById("tbodyLeaveMatrix");
|
||||
const empty = document.getElementById("emptyMatrixRow");
|
||||
if (empty) empty.remove();
|
||||
if (!tbody) return;
|
||||
window.submitChildCareModes = async function () {
|
||||
if (!window.currentSelectedMemberId) return;
|
||||
const checkboxes = document.querySelectorAll(".js-care-mode-cb:checked");
|
||||
const selectedModes = Array.from(checkboxes).map((cb) => cb.value);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("action", "save_child_care_modes");
|
||||
formData.append("person_id", window.currentSelectedMemberId);
|
||||
formData.append("care_modes", JSON.stringify(selectedModes));
|
||||
|
||||
const rowTypeOptions = (calGlobalData.leave_types || [])
|
||||
.map((lt) => `<option value="${lt.code}">${lt.code} - ${lt.label}</option>`)
|
||||
.join("");
|
||||
const res = await pachaFetch(
|
||||
"/modules/family-calendar/includes/api/calendar-settings.php",
|
||||
{ method: "POST", body: formData },
|
||||
);
|
||||
if (!res.success) throw new Error(res.error);
|
||||
if (window.showToast)
|
||||
showToast(tr("fc_child_saved") || "Sauvegardé !", "success");
|
||||
else alert(tr("fc_child_saved") || "Sauvegardé !");
|
||||
|
||||
const trRow = document.createElement("tr");
|
||||
trRow.className = "js-leave-row";
|
||||
trRow.style.borderBottom = "1px solid var(--border-light)";
|
||||
trRow.innerHTML = `
|
||||
<td style="padding: 6px 2px;">
|
||||
<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>
|
||||
${rowTypeOptions}
|
||||
</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);
|
||||
const currentPerson = calGlobalData.people.find(
|
||||
(k) => parseInt(k.id) === window.currentSelectedMemberId,
|
||||
);
|
||||
if (currentPerson) {
|
||||
currentPerson.care_modes = JSON.stringify(selectedModes);
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Erreur: " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
window.populateCalendarSettings = function (settings) {
|
||||
if (!settings) return;
|
||||
const viewSelect = document.getElementById("setCalView");
|
||||
if (viewSelect && settings.calendar_default_view)
|
||||
viewSelect.value = settings.calendar_default_view;
|
||||
const firstDaySelect = document.getElementById("setCalFirstDay");
|
||||
if (firstDaySelect && settings.calendar_first_day !== undefined)
|
||||
firstDaySelect.value = settings.calendar_first_day;
|
||||
const hoursInput = document.getElementById("setCalHours");
|
||||
if (hoursInput && settings.calendar_working_hours)
|
||||
hoursInput.value = settings.calendar_working_hours;
|
||||
};
|
||||
// ==========================================
|
||||
// NOUVEAU CATALOGUE GLOBAL (FOYER)
|
||||
// ==========================================
|
||||
let globalLeaveCatalog = [];
|
||||
|
||||
window.loadAdultConfigView = function (memberId, zoneElement) {
|
||||
let userLeaves = calGlobalData.leaves[memberId] || [];
|
||||
async function loadLeaveCatalog() {
|
||||
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
|
||||
.map((l) => {
|
||||
let dateDisp = "";
|
||||
if (l.date) {
|
||||
const parts = l.date.split("-");
|
||||
if (parts.length >= 3) dateDisp = `${parts[2]}/${parts[1]}`;
|
||||
}
|
||||
function renderLeaveCatalog(types) {
|
||||
const container = document.getElementById("leaveTypesContainer");
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
|
||||
let rowTypeOptions = (calGlobalData.leave_types || [])
|
||||
.map(
|
||||
(lt) =>
|
||||
`<option value="${lt.code}" ${lt.code === l.type ? "selected" : ""}>${lt.code} - ${lt.label}</option>`,
|
||||
)
|
||||
.join("");
|
||||
if (types.length === 0) {
|
||||
container.innerHTML = `<p class="pf-muted-note">Aucun congé configuré.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!calGlobalData.leave_types.some((lt) => lt.code === l.type)) {
|
||||
rowTypeOptions += `<option value="${l.type}" selected>${l.type} (${tr("fc_leave_obsolete") || "Obsolète"})</option>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<tr class="js-leave-row" style="border-bottom: 1px solid var(--border-light);">
|
||||
<td style="padding: 6px 2px;">
|
||||
<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>
|
||||
${rowTypeOptions}
|
||||
</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" ${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>
|
||||
types.forEach((lt) => {
|
||||
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>
|
||||
<span style="font-weight: bold; color: var(--text-main);">${lt.label}</span>
|
||||
<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>
|
||||
</div>
|
||||
<div style="display: flex; gap: 4px;">
|
||||
<button type="button" class="pf-btn btn-secondary" style="padding: 4px 8px;" onclick="editLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')">✏️</button>
|
||||
<button type="button" class="pf-btn btn-secondary" style="padding: 4px 8px; color: var(--danger);" onclick="deleteLeaveType('${lt.code}', '${lt.label.replace(/'/g, "\\'")}')">🗑️</button>
|
||||
</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 zone = document.getElementById("memberConfigZone");
|
||||
if (!select || !select.value || !zone) return;
|
||||
|
||||
window.currentSelectedMemberId = parseInt(select.value);
|
||||
const personId = parseInt(select.value);
|
||||
window.currentSelectedMemberId = personId;
|
||||
const role = (
|
||||
select.options[select.selectedIndex].dataset.role || ""
|
||||
).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") {
|
||||
const currentPerson = calGlobalData.people.find(
|
||||
(k) => parseInt(k.id) === window.currentSelectedMemberId,
|
||||
(k) => parseInt(k.id) === personId,
|
||||
);
|
||||
let savedModes = [];
|
||||
try {
|
||||
@@ -296,83 +360,145 @@ window.loadMemberConfigView = function () {
|
||||
.join("");
|
||||
|
||||
zone.innerHTML = `
|
||||
<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>
|
||||
<div style="display:flex; flex-direction:column; gap:6px;">
|
||||
${modesHtml || `<em>Aucun mode de garde configuré dans le foyer.</em>`}
|
||||
</div>
|
||||
<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);
|
||||
<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>
|
||||
<div style="display:flex; flex-direction:column; gap:6px;">
|
||||
${modesHtml || `<em>Aucun mode de garde configuré dans le foyer.</em>`}
|
||||
</div>
|
||||
<button class="pf-btn pf-btn-primary" style="margin-top:15px; width:100%;" onclick="submitChildCareModes()">${tr("btn_save_rights") || "Enregistrer"}</button>
|
||||
`;
|
||||
}
|
||||
};
|
||||
// === 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 {
|
||||
const formData = new FormData();
|
||||
formData.append("action", "save_child_care_modes");
|
||||
formData.append("person_id", window.currentSelectedMemberId);
|
||||
formData.append("care_modes", JSON.stringify(selectedModes));
|
||||
|
||||
const res = await pachaFetch(
|
||||
"/modules/family-calendar/includes/api/settings.php",
|
||||
{ method: "POST", body: formData },
|
||||
"/modules/family-calendar/includes/api/calendar-settings.php",
|
||||
{ method: "POST", body: fd },
|
||||
);
|
||||
if (!res.success) throw new Error(res.error);
|
||||
if (window.showToast)
|
||||
showToast(tr("fc_child_saved") || "Sauvegardé !", "success");
|
||||
else alert(tr("fc_child_saved") || "Sauvegardé !");
|
||||
|
||||
const currentPerson = calGlobalData.people.find(
|
||||
(k) => parseInt(k.id) === window.currentSelectedMemberId,
|
||||
);
|
||||
if (currentPerson) {
|
||||
currentPerson.care_modes = JSON.stringify(selectedModes);
|
||||
}
|
||||
if (window.showToast) showToast("Congé attribué avec succès !", "success");
|
||||
loadMemberConfigView();
|
||||
} catch (err) {
|
||||
alert("Erreur: " + err.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function submitMemberLeaves() {
|
||||
if (!currentSelectedMemberId || isNaN(currentSelectedMemberId)) {
|
||||
alert("Erreur technique : Aucun membre valide sélectionné.");
|
||||
// Supprimer un congé d'un membre
|
||||
async function deleteMemberLeave(leaveId, personId) {
|
||||
if (!confirm(tr("confirm_delete") || "Retirer ce congé pour ce membre ?"))
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("action", "delete_person_leave");
|
||||
fd.append("id", leaveId);
|
||||
|
||||
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(
|
||||
"/modules/family-calendar/includes/api/settings.php",
|
||||
{ method: "POST", body: formData },
|
||||
"/modules/family-calendar/includes/api/calendar-settings.php",
|
||||
{ method: "POST", body: fd },
|
||||
);
|
||||
if (!res.success) throw new Error(res.error);
|
||||
|
||||
if (window.showToast)
|
||||
showToast(tr("fc_matrix_saved") || "Matrice enregistrée", "success");
|
||||
else alert(tr("fc_matrix_saved") || "Matrice enregistrée");
|
||||
|
||||
closeCalendarSettings();
|
||||
setTimeout(openCalendarSettings, 300);
|
||||
if (window.showToast) showToast("Congé retiré !", "success");
|
||||
loadMemberConfigView();
|
||||
} catch (err) {
|
||||
alert("Erreur : " + err.message);
|
||||
alert("Erreur: " + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +576,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
try {
|
||||
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) {
|
||||
window.calGlobalData = res.data;
|
||||
|
||||
+99
-81
@@ -7,19 +7,14 @@ header('Content-Type: application/json');
|
||||
$action = $_REQUEST['action'] ?? '';
|
||||
|
||||
try {
|
||||
// ─── 1. RECUPÉRATION COMPLÈTE DES PARAMÈTRES ───
|
||||
// ─── 1. RECUPÉRATION COMPLÈTE DES PARAMÈTRES (INITIALISATION) ───
|
||||
if ($action === 'get_all') {
|
||||
// A. Options globales du foyer
|
||||
$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' => '[]'];
|
||||
|
||||
// B. Liste des membres de la famille (Triés par rôle pour grouper Parents, Enfants, Helpers)
|
||||
// 🟢 CORRECTION : Ajout de la colonne `care_modes` et `color` pour le Javascript !
|
||||
$stmtPeople = $pdo->query("SELECT id, name, role, care_modes, color FROM pf_people WHERE is_active = 1 ORDER BY role ASC, name ASC");
|
||||
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// C. Matrice des congés par personne
|
||||
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance FROM pf_person_leave_meta");
|
||||
$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'");
|
||||
$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 = [
|
||||
'calendar_default_view' => $calendarSettingsRaw['calendar_default_view'] ?? 'month',
|
||||
'calendar_first_day' => $calendarSettingsRaw['calendar_first_day'] ?? '1',
|
||||
'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00'
|
||||
];
|
||||
|
||||
// E. NOUVEAU : Récupération du catalogue des types de congés de la famille
|
||||
// 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");
|
||||
$leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
@@ -58,7 +51,7 @@ try {
|
||||
'people' => $people,
|
||||
'leaves' => $leavesMap,
|
||||
'calendar_settings' => $calendarSettings,
|
||||
'leave_types' => $leaveTypes // On envoie le catalogue au JS !
|
||||
'leave_types' => $leaveTypes
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
@@ -70,7 +63,6 @@ try {
|
||||
|
||||
$zone = trim($_POST['zone_scolaire'] ?? 'C');
|
||||
$modesRaw = $_POST['care_modes'] ?? '[]';
|
||||
|
||||
$modesArray = json_encode(array_values(array_unique(array_filter(json_decode($modesRaw, true)))));
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET zone_scolaire = ?, care_modes = ? WHERE id = 1");
|
||||
@@ -80,74 +72,7 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// ─── 3. SAUVEGARDE DES CONGÉS D'UN MEMBRE (ONGLET 2) ───
|
||||
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 ───
|
||||
// ─── 3. SAUVEGARDE DES MODES DE GARDE D'UN ENFANT ───
|
||||
if ($action === 'save_child_care_modes') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||
|
||||
@@ -164,7 +89,100 @@ try {
|
||||
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) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
@@ -1,51 +1,84 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/db.php';
|
||||
require __DIR__ . '/../../includes/auth.php';
|
||||
require __DIR__ . '/../../includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||
|
||||
try {
|
||||
// 1. Lire toutes les fêtes
|
||||
if ($action === 'get_occasions') {
|
||||
$stmt = $pdo->query("SELECT * FROM pf_gift_occasions ORDER BY month_date ASC, id ASC");
|
||||
echo json_encode(['ok' => true, 'data' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
|
||||
if ($action === 'get_all') {
|
||||
$occ = $pdo->query("SELECT * FROM pf_gift_occasions ORDER BY sort_order ASC, id ASC")->fetchAll();
|
||||
$proches = $pdo->query("SELECT * FROM pf_people WHERE role IN ('proche_adulte', 'proche_enfant') ORDER BY name ASC")->fetchAll();
|
||||
echo json_encode(['success' => true, 'data' => ['occasions' => $occ, 'proches' => $proches]]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Ajouter une nouvelle fête (Renvoie l'ID pour rafraîchir la modale)
|
||||
if ($action === 'add_occasion') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$month_date = trim($_POST['month_date'] ?? '');
|
||||
if ($action === 'save_occasion') {
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$name = trim($_POST['name']);
|
||||
$icon = trim($_POST['icon']) ?: '🎁';
|
||||
$month_date = trim($_POST['month_date']) ?: null;
|
||||
|
||||
if (empty($name)) throw new Exception("Name required");
|
||||
|
||||
$code = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $name));
|
||||
$code = substr($code, 0, 20);
|
||||
if (empty($name)) throw new Exception("Le nom est obligatoire.");
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_gift_occasions (code, name, month_date, is_active) VALUES (?, ?, ?, 1)");
|
||||
$stmt->execute([$code, $name, $month_date ?: null]);
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 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']]);
|
||||
}
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare("UPDATE pf_gift_occasions SET name=?, icon=?, month_date=? WHERE id=?");
|
||||
$stmt->execute([$name, $icon, $month_date, $id]);
|
||||
} else {
|
||||
$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]);
|
||||
}
|
||||
echo json_encode(['ok' => true]);
|
||||
echo json_encode(['success' => true]);
|
||||
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) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
+161
-339
@@ -6,7 +6,6 @@
|
||||
margin: 0;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.cl-titlebar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -17,7 +16,6 @@
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.cl-view-switch {
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
@@ -25,7 +23,6 @@
|
||||
border: 1px solid var(--border-light);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.cl-view-btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
@@ -35,13 +32,12 @@
|
||||
color: var(--text-muted);
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.cl-view-btn.is-active {
|
||||
background: var(--text-main);
|
||||
color: var(--bg-panel);
|
||||
}
|
||||
|
||||
/* === FILTRES STICKY === */
|
||||
/* === FILTRES === */
|
||||
.pf-filter-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -52,7 +48,6 @@
|
||||
z-index: 50;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pf-filter-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
@@ -62,35 +57,6 @@
|
||||
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 === */
|
||||
.pf-child-section {
|
||||
margin-bottom: 30px;
|
||||
@@ -226,18 +192,69 @@
|
||||
z-index: 2;
|
||||
border-right: 2px solid var(--border-light);
|
||||
}
|
||||
|
||||
.cl-occasion-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
.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;
|
||||
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 {
|
||||
display: flex;
|
||||
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 === */
|
||||
@@ -355,79 +372,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Header & Settings Button */
|
||||
.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 */
|
||||
/* === MODALE EDITION CADEAU === */
|
||||
.gift-modal-custom-content {
|
||||
max-width: 500px;
|
||||
width: 95%;
|
||||
@@ -459,249 +404,126 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
/* Modale Paramètres Occasions (Bottom Sheet style) */
|
||||
.gift-settings-backdrop {
|
||||
.gl-pane {
|
||||
display: none;
|
||||
align-items: flex-end; /* Bottom sheet on mobile */
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
.gift-settings-modal {
|
||||
width: 100%;
|
||||
border-radius: 12px 12px 0 0;
|
||||
margin: 0;
|
||||
.gl-pane.active {
|
||||
display: block;
|
||||
}
|
||||
.gift-settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.gl-pane-title {
|
||||
margin-top: 0;
|
||||
color: var(--text-main);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.gift-settings-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
.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 {
|
||||
.gl-pane-desc {
|
||||
font-size: 0.85rem;
|
||||
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 {
|
||||
text-align: center;
|
||||
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);
|
||||
font-style: italic;
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
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-center {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.gift-settings-modal {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 14px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
/* =========================================
|
||||
COMPOSANTS MODALE & EDITION IN-PLACE
|
||||
========================================= */
|
||||
|
||||
/* Sélecteur d'émojis horizontal */
|
||||
.emoji-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* Aligner le bouton sur celui de Todo */
|
||||
.gift-add-form .btn-primary {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
.emoji-option {
|
||||
cursor: pointer;
|
||||
width: 38px;
|
||||
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);
|
||||
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
@@ -89,14 +89,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$meta_pdo->prepare(
|
||||
"INSERT INTO users (username, password_hash, display_name, family_id) VALUES (?, ?, ?, ?)"
|
||||
)->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'] = [
|
||||
'id' => (int)$meta_pdo->lastInsertId(),
|
||||
'id' => $user_id,
|
||||
'username' => $username,
|
||||
'display_name' => $display_name,
|
||||
'family_id' => (int)$family['id'],
|
||||
];
|
||||
$_SESSION['family_db'] = $family['db_name'];
|
||||
$_SESSION['family_db'] = $tenant_db;
|
||||
session_regenerate_id(true);
|
||||
header('Location: /index.php');
|
||||
exit;
|
||||
@@ -129,6 +137,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
)->execute([$username, $hash, $display_name, $family_id]);
|
||||
$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();
|
||||
|
||||
$_SESSION['user'] = [
|
||||
@@ -169,7 +182,6 @@ require __DIR__ . '/header.php';
|
||||
<div class="pf-login-error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Onglets -->
|
||||
<div style="display:flex;gap:8px;margin-bottom:24px">
|
||||
<button id="tab-create" onclick="switchTab('create')"
|
||||
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>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire commun -->
|
||||
<form method="post" action="/register.php" id="reg-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||
<input type="hidden" name="action" id="reg-action" value="create">
|
||||
@@ -208,7 +219,6 @@ require __DIR__ . '/header.php';
|
||||
placeholder="••••••" autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<!-- Champs spécifiques : Nouvel espace -->
|
||||
<div id="section-create">
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Nom de la famille / foyer</label>
|
||||
@@ -218,7 +228,6 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Champs spécifiques : Rejoindre -->
|
||||
<div id="section-join" style="display:none">
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label">Code d'invitation</label>
|
||||
@@ -256,4 +265,4 @@ switchTab('join');
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
+1
-1
@@ -630,7 +630,7 @@ require __DIR__ . '/header.php';
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($_SESSION['user']['is_admin']): ?>
|
||||
<?php if (!empty($_SESSION['user']['is_admin'])): ?>
|
||||
<div class="pf-settings-admin-link">
|
||||
<a href="/admin/">→ Panneau d'administration</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user