@@ -19,8 +19,7 @@ try {
|
||||
// 2. Catégories
|
||||
$categories = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type ASC, label ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. Règles d'import (avec le nom de la catégorie correspondante)
|
||||
// Utilisation de COLLATE pour éviter l'erreur 1267 de mix de collations
|
||||
// 3. Règles d'import
|
||||
$rules = $pdo->query("
|
||||
SELECT r.*, c.label as cat_label
|
||||
FROM pf_import_rules r
|
||||
@@ -32,73 +31,60 @@ try {
|
||||
$currentYear = (int)date('Y');
|
||||
$salaries = $pdo->query("SELECT * FROM pf_salary_config WHERE year = $currentYear ORDER BY person ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Récupération de la devise du foyer (par défaut € si non définie)
|
||||
// Lecture directe de la colonne currency dans pf_foyer_settings
|
||||
$currencySetting = $pdo->query("SELECT currency FROM pf_foyer_settings LIMIT 1")->fetchColumn() ?: '€';
|
||||
// 5. Paramètres Foyer (Devise + Mapping CSV)
|
||||
$foyerData = $pdo->query("SELECT currency, csv_mapping FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
||||
$currencySetting = $foyerData['currency'] ?? '€';
|
||||
$csvMapping = !empty($foyerData['csv_mapping']) ? json_decode($foyerData['csv_mapping'], true) : null;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'accounts' => $accounts,
|
||||
'categories' => $categories,
|
||||
'rules' => $rules,
|
||||
'salaries' => $salaries,
|
||||
'year' => $currentYear,
|
||||
'currency' => $currencySetting
|
||||
'accounts' => $accounts,
|
||||
'categories' => $categories,
|
||||
'rules' => $rules,
|
||||
'salaries' => $salaries,
|
||||
'year' => $currentYear,
|
||||
'currency' => $currencySetting,
|
||||
'csv_mapping' => $csvMapping
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DES COMPTES BANCAIRES ---
|
||||
|
||||
if ($action === 'add_account') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$type = $_POST['type'] ?? 'checking';
|
||||
// On met is_default à 0 par défaut pour les nouveaux comptes
|
||||
|
||||
if (empty($name)) throw new Exception("Le nom du compte est obligatoire.");
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES (?, ?, 0)");
|
||||
$stmt->execute([$name, $type]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete_account') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
|
||||
// Sécurité : on empêche de supprimer un compte s'il n'en reste qu'un seul
|
||||
$count = $pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn();
|
||||
if ($count <= 1) throw new Exception("Impossible de supprimer le dernier compte.");
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_bank_accounts WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DES CATÉGORIES ---
|
||||
|
||||
if ($action === 'add_category') {
|
||||
$code = strtoupper(trim($_POST['code'] ?? ''));
|
||||
$label = trim($_POST['label'] ?? '');
|
||||
$type = $_POST['type'] ?? 'Expense';
|
||||
$color = $_POST['color'] ?? '#cccccc';
|
||||
$icon = trim($_POST['icon'] ?? '📌');
|
||||
|
||||
if (empty($code) || empty($label)) throw new Exception("Le code et le libellé sont obligatoires.");
|
||||
|
||||
// Vérification anti-doublon sur le code
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_budget_categories WHERE code = ?");
|
||||
$check->execute([$code]);
|
||||
if ($check->fetchColumn() > 0) throw new Exception("Ce code de catégorie existe déjà.");
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$code, $label, $type, $color, $icon]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
@@ -107,27 +93,20 @@ try {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_budget_categories WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DES RÈGLES D'IMPORT ---
|
||||
|
||||
if ($action === 'add_rule') {
|
||||
$keyword = strtoupper(trim($_POST['keyword'] ?? ''));
|
||||
$category = trim($_POST['category'] ?? ''); // Le code de la catégorie (ex: FMCG)
|
||||
|
||||
$category = trim($_POST['category'] ?? '');
|
||||
if (empty($keyword) || empty($category)) throw new Exception("Le mot-clé et la catégorie sont obligatoires.");
|
||||
|
||||
// Vérification anti-doublon
|
||||
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_import_rules WHERE keyword = ?");
|
||||
$check->execute([$keyword]);
|
||||
if ($check->fetchColumn() > 0) throw new Exception("Une règle pour ce mot-clé existe déjà.");
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?)");
|
||||
$stmt->execute([$keyword, $category]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
@@ -136,36 +115,46 @@ try {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_import_rules WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DES SALAIRES ---
|
||||
|
||||
if ($action === 'save_salary') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$salary = (float)($_POST['salary'] ?? 0);
|
||||
$mensualite = (float)($_POST['mensualite'] ?? 0);
|
||||
|
||||
if ($id <= 0) throw new Exception("ID de configuration de salaire invalide.");
|
||||
|
||||
// Mise à jour de la ligne pour l'année en cours
|
||||
$stmt = $pdo->prepare("UPDATE pf_salary_config SET salary = ?, mensualite = ? WHERE id = ?");
|
||||
$stmt->execute([$salary, $mensualite, $id]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
// --- GESTION DE LA DEVISE GLOBALE ---
|
||||
|
||||
// --- GESTION DE LA DEVISE GLOBALE ---
|
||||
if ($action === 'save_currency') {
|
||||
$currency = trim($_POST['currency'] ?? '€');
|
||||
|
||||
// Mise à jour directe de la colonne currency pour le foyer
|
||||
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?");
|
||||
$stmt->execute([$currency]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GESTION DU FORMAT CSV ---
|
||||
if ($action === 'save_csv_mapping') {
|
||||
$mapping = [
|
||||
'delimiter' => $_POST['csv_delimiter'] ?? ';',
|
||||
'date_format' => $_POST['csv_date_format'] ?? 'd/m/Y',
|
||||
'col_date' => (int)($_POST['csv_col_date'] ?? 0),
|
||||
'col_label' => (int)($_POST['csv_col_label'] ?? 1),
|
||||
'amount_type' => $_POST['csv_amount_type'] ?? 'single',
|
||||
'col_debit' => (int)($_POST['csv_col_debit'] ?? 8),
|
||||
'col_credit' => (int)($_POST['csv_col_credit'] ?? 9),
|
||||
'col_ref' => (int)($_POST['csv_col_ref'] ?? 3)
|
||||
];
|
||||
$jsonContent = json_encode($mapping);
|
||||
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET csv_mapping = ?");
|
||||
$stmt->execute([$jsonContent]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@ while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
||||
|
||||
if ($role === 'parent') {
|
||||
$familyParents[] = $row['name'];
|
||||
} elseif ($role === 'nounou') {
|
||||
} elseif ($role === 'nounou' || $role === 'helper') {
|
||||
// On ignore la nounou dans l'épargne
|
||||
continue;
|
||||
} else {
|
||||
// Fallback : Si c'est 'enfant', 'user', ou vide, ça va dans l'onglet Enfants
|
||||
$familyKids[] = $row['name'];
|
||||
}
|
||||
}
|
||||
@@ -23,8 +22,8 @@ while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
||||
// Sécurité anti-page blanche si la BDD est mal configurée
|
||||
if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2'];
|
||||
|
||||
$requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'Nens');
|
||||
$ownersToDisplay = ($requestedOwner === 'Nens') ? $familyKids : [$requestedOwner];
|
||||
$requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'KIDS');
|
||||
$ownersToDisplay = ($requestedOwner === 'KIDS') ? $familyKids : [$requestedOwner];
|
||||
|
||||
// --- RÉCUPÉRATION CONFIGURATION DES MOIS ---
|
||||
$cycleConfigs = [];
|
||||
@@ -47,23 +46,21 @@ function getMonthName($dateString) {
|
||||
<div class="budget-view">
|
||||
<div class="view-header">
|
||||
<div class="owner-tabs">
|
||||
<!-- Boucle dynamique sur les parents -->
|
||||
<?php foreach ($familyParents as $p): ?>
|
||||
<a href="?tab=epargne&owner=<?= urlencode($p) ?>" class="owner-tab <?= $requestedOwner === $p ? 'active' : '' ?>">
|
||||
<?= htmlspecialchars($p) ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<!-- Onglet Enfants -->
|
||||
<?php if (!empty($familyKids)): ?>
|
||||
<a href="?tab=epargne&owner=Nens" class="owner-tab <?= $requestedOwner === 'Nens' ? 'active' : '' ?>">
|
||||
<?= tr('budget_tab_kids') ?? 'Nens 👶' ?>
|
||||
<a href="?tab=epargne&owner=KIDS" class="owner-tab <?= $requestedOwner === 'KIDS' ? 'active' : '' ?>">
|
||||
<?= tr('budget_tab_kids') ?? 'Enfants 👶' ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php foreach ($ownersToDisplay as $currentOwner):
|
||||
<?php foreach ($ownersToDisplay as $index => $currentOwner):
|
||||
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
|
||||
$stmt->execute([$currentOwner]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
@@ -83,28 +80,29 @@ function getMonthName($dateString) {
|
||||
$months = array_slice($months, 0, 7);
|
||||
sort($allCategories);
|
||||
|
||||
// Définition de la classe couleur selon le propriétaire
|
||||
$ownerTextClass = 'txt-global';
|
||||
// 🔄 CORRECTION ICI : Création d'un nom "safe" sans espace pour les classes CSS
|
||||
$safeOwnerCls = htmlspecialchars(str_replace(' ', '_', $currentOwner), ENT_QUOTES);
|
||||
?>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'KIDS' && $index > 0) ? '40px' : '0' ?>;">
|
||||
<div style="flex-grow: 1;">
|
||||
<?php if ($requestedOwner === 'Nens'):
|
||||
$themeClass = 'theme-' . strtolower($currentOwner);
|
||||
<?php if ($requestedOwner === 'KIDS'):
|
||||
$themeClass = 'theme-' . strtolower($safeOwnerCls);
|
||||
?>
|
||||
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
|
||||
<?= $currentOwner ?>
|
||||
<?= htmlspecialchars($currentOwner) ?>
|
||||
</h3>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<?php if (!empty($months)): ?>
|
||||
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= $currentOwner ?>')" class="pf-btn btn-secondary">
|
||||
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn btn-secondary">
|
||||
🔁 <?= tr('bud_sav_add_one_month') ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn">
|
||||
<button onclick="openCustomSavingsModal('<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn">
|
||||
+ <?= tr('bud_sav_add_month') ?>
|
||||
</button>
|
||||
</div>
|
||||
@@ -116,7 +114,7 @@ function getMonthName($dateString) {
|
||||
<p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
|
||||
<table class="pf-table savings-table nens-table theme-<?= strtolower($safeOwnerCls) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></th>
|
||||
@@ -135,11 +133,11 @@ function getMonthName($dateString) {
|
||||
<div class="month-actions" style="justify-content: center; width: 100%;">
|
||||
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>"
|
||||
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
|
||||
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", JSON.parse(this.getAttribute("data-json")))'>
|
||||
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>", JSON.parse(this.getAttribute("data-json")))'>
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_delete_month') ?>"
|
||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
|
||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')"
|
||||
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
|
||||
🗑️
|
||||
</button>
|
||||
@@ -158,11 +156,11 @@ function getMonthName($dateString) {
|
||||
<td class="text-center" style="padding:4px;">
|
||||
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
||||
<input type="number" step="0.01"
|
||||
class="prev-input total-input-<?= $currentOwner ?>-<?= $month ?>"
|
||||
class="prev-input total-input-<?= $safeOwnerCls ?>-<?= $month ?>"
|
||||
style="width: 70px; font-weight:bold; color:#2563eb;"
|
||||
value="<?= $val != 0 ? round($val) : '' ?>"
|
||||
placeholder="0"
|
||||
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= $currentOwner ?>', this)">
|
||||
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
|
||||
<span style="color:#2563eb; font-weight:bold; font-size:0.9rem;">€</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -178,11 +176,11 @@ function getMonthName($dateString) {
|
||||
<td class="text-center" style="padding:4px;">
|
||||
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
||||
<input type="number" step="0.01"
|
||||
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $currentOwner ?>-<?= $month ?>"
|
||||
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $safeOwnerCls ?>-<?= $month ?>"
|
||||
style="width: 70px;"
|
||||
value="<?= $amount != 0 ? round($amount) : '' ?>"
|
||||
placeholder="-"
|
||||
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>', this)">
|
||||
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
|
||||
</div>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
@@ -197,7 +195,7 @@ function getMonthName($dateString) {
|
||||
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
||||
$extra = $total - $sum;
|
||||
?>
|
||||
<td class="text-center font-bold sum-target" id="extra_<?= $currentOwner ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
|
||||
<td class="text-center font-bold sum-target" id="extra_<?= $safeOwnerCls ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
|
||||
<?= number_format($extra, 0, ',', ' ') ?> €
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
@@ -289,6 +287,9 @@ window.I18N = {
|
||||
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>
|
||||
};
|
||||
|
||||
// Sécurisation de la devise pour la calculatrice
|
||||
const systemCurrency = (typeof window.CONFIG !== 'undefined' && window.CONFIG.CURRENCY) ? window.CONFIG.CURRENCY : '€';
|
||||
|
||||
// --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT ---
|
||||
const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>;
|
||||
|
||||
@@ -306,16 +307,19 @@ function updateEpargneCell(month, category, owner, inputEl) {
|
||||
body: formData
|
||||
}).catch(err => alert(window.I18N['bud_err_tech'] || 'Erreur technique'));
|
||||
|
||||
const totalInput = document.querySelector(`.total-input-${owner}-${month}`);
|
||||
// Gère les IDs proprement
|
||||
const safeOwnerClass = owner.replace(/\s+/g, '_');
|
||||
|
||||
const totalInput = document.querySelector(`.total-input-${CSS.escape(safeOwnerClass)}-${month}`);
|
||||
const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0;
|
||||
|
||||
let sumCats = 0;
|
||||
document.querySelectorAll(`.cat-input-${owner}-${month}`).forEach(inp => {
|
||||
document.querySelectorAll(`.cat-input-${CSS.escape(safeOwnerClass)}-${month}`).forEach(inp => {
|
||||
sumCats += parseFloat(inp.value) || 0;
|
||||
});
|
||||
|
||||
const extra = totalVal - sumCats;
|
||||
const extraCell = document.getElementById(`extra_${owner}_${month}`);
|
||||
const extraCell = document.getElementById(`extra_${safeOwnerClass}_${month}`);
|
||||
|
||||
if (extraCell) {
|
||||
extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €';
|
||||
@@ -557,7 +561,7 @@ function updateSumResult() {
|
||||
total += val;
|
||||
});
|
||||
|
||||
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + systemCurrency;
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
|
||||
+147
-247
@@ -1,35 +1,32 @@
|
||||
<?php
|
||||
// modules/budget/views/recap.php
|
||||
|
||||
// 1. Récupération des Items du Budget
|
||||
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC");
|
||||
$items = $stmt->fetchAll();
|
||||
// 1. Récupération des Catégories dynamiques
|
||||
$stmtCats = $pdo->query("SELECT code, label, icon, type FROM pf_budget_categories ORDER BY type DESC, label ASC");
|
||||
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 2. Déterminer quel est le mois de gestion "ouvert" par défaut
|
||||
// 2. Récupération des Salaires/Mensualités configurés (Revenus automatiques)
|
||||
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y');
|
||||
$stmtSalaries = $pdo->prepare("SELECT person, mensualite FROM pf_salary_config WHERE year = ?");
|
||||
$stmtSalaries->execute([$currentYear]);
|
||||
$salaries = $stmtSalaries->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. Récupération des Items du Budget (Charges Fixes et Estimations)
|
||||
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY is_estimate ASC, sort_order ASC, name ASC");
|
||||
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 4. Gestion du mois actif
|
||||
$stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1");
|
||||
$defaultActiveMonth = $stmtActive->fetchColumn();
|
||||
if (!$defaultActiveMonth) {
|
||||
$defaultActiveMonth = date('Y-m-01');
|
||||
}
|
||||
|
||||
// 3. Assigner le mois et l'année en fonction du mois ouvert
|
||||
$defaultActiveMonth = $stmtActive->fetchColumn() ?: date('Y-m-01');
|
||||
$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth));
|
||||
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth));
|
||||
$viewMonthDate = "$currentYear-$currentMonth-01";
|
||||
|
||||
$sqlReal = "SELECT budget_item_id, SUM(amount) as total_real
|
||||
FROM pf_expenses
|
||||
WHERE gestion_month = ? AND budget_item_id IS NOT NULL
|
||||
GROUP BY budget_item_id";
|
||||
$stmtReal = $pdo->prepare($sqlReal);
|
||||
// 5. Récupération du Réel (Dépenses)
|
||||
$stmtReal = $pdo->prepare("SELECT budget_item_id, SUM(amount) as total_real FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NOT NULL GROUP BY budget_item_id");
|
||||
$stmtReal->execute([$viewMonthDate]);
|
||||
$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR); // Retourne un tableau [id => total]
|
||||
$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
$sqlCatReal = "SELECT category, SUM(amount) as total_real
|
||||
FROM pf_expenses
|
||||
WHERE gestion_month = ? AND budget_item_id IS NULL
|
||||
GROUP BY category";
|
||||
$stmtCatReal = $pdo->prepare($sqlCatReal);
|
||||
$stmtCatReal = $pdo->prepare("SELECT category, SUM(amount) as total_real FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL GROUP BY category");
|
||||
$stmtCatReal->execute([$viewMonthDate]);
|
||||
$catTotals = $stmtCatReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
@@ -38,8 +35,7 @@ $stmtLabels->execute([$viewMonthDate]);
|
||||
$unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||
$monthTranslationKey = 'month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT);
|
||||
$currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear;
|
||||
$currentMonthName = tr('month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT)) . ' ' . $currentYear;
|
||||
|
||||
$totalDepenses = 0;
|
||||
$totalRevenus = 0;
|
||||
@@ -64,46 +60,60 @@ $totalRevenus = 0;
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($salaries as $salary):
|
||||
$mensualite = (float)$salary['mensualite'];
|
||||
$totalRevenus += $mensualite;
|
||||
?>
|
||||
<tr class="row-income" style="border-bottom:1px solid var(--border-light); background:#f0fdf4;">
|
||||
<td style="padding:15px;">
|
||||
<strong>Apport <?= htmlspecialchars($salary['person']) ?></strong>
|
||||
<span title="Géré automatiquement depuis les paramètres des Salaires" style="font-size:0.8rem; cursor:help;">⚙️</span>
|
||||
</td>
|
||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:#10b981;">
|
||||
+ <?= number_format($mensualite, 2, ',', ' ') ?> €
|
||||
</td>
|
||||
<td style="padding:15px;">
|
||||
<span class="badge-type" style="background:#dcfce7; color:#16a34a; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600;">Fixe (Auto)</span>
|
||||
</td>
|
||||
<td style="padding:15px; color:#64748b; font-weight:bold;">-</td>
|
||||
<td style="padding:15px;">
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;">
|
||||
Auto
|
||||
</div>
|
||||
</td>
|
||||
<td style="padding:15px; text-align:right;">
|
||||
<small style="color:#94a3b8;">Via Paramètres</small>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php foreach ($items as $item):
|
||||
// --- 1. CALCUL DES TOTAUX PRÉVUS ---
|
||||
$targetAbs = abs((float)$item['amount']);
|
||||
$amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs;
|
||||
$totalDepenses += $amountToAdd;
|
||||
|
||||
if ($item['category'] === 'income') $totalRevenus += $amountToAdd;
|
||||
else $totalDepenses += $amountToAdd;
|
||||
|
||||
// --- 2. CALCUL DU RÉEL (Logique optimisée) ---
|
||||
$realSum = 0;
|
||||
$hasMatchingExpense = false;
|
||||
|
||||
// A. Correspondance directe par ID
|
||||
// A. Correspondance par ID direct
|
||||
if (isset($realTotals[$item['id']])) {
|
||||
$realSum = $realTotals[$item['id']];
|
||||
$hasMatchingExpense = true;
|
||||
}
|
||||
// B. Correspondance par catégorie système (Multi-tenant via Mots-clés)
|
||||
else {
|
||||
$catKey = null;
|
||||
if (!empty($item['mapping_keywords'])) {
|
||||
if (stripos($item['mapping_keywords'], 'School') !== false) $catKey = 'School';
|
||||
elseif (stripos($item['mapping_keywords'], 'Essence') !== false) $catKey = 'Essence';
|
||||
elseif (stripos($item['mapping_keywords'], 'FMCG') !== false) $catKey = 'FMCG';
|
||||
}
|
||||
|
||||
if ($catKey && isset($catTotals[$catKey])) {
|
||||
$realSum = $catTotals[$catKey];
|
||||
$hasMatchingExpense = true;
|
||||
}
|
||||
// C. Correspondance par mots-clés classiques (sur les dépenses non liées)
|
||||
elseif (!empty($item['mapping_keywords'])) {
|
||||
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
||||
foreach ($unlinkedExpenses as $uexp) {
|
||||
foreach ($keywords as $kw) {
|
||||
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
|
||||
$realSum += (float)$uexp['amount'];
|
||||
$hasMatchingExpense = true;
|
||||
break;
|
||||
}
|
||||
// B. Correspondance par Catégorie système
|
||||
elseif (!empty($item['category']) && isset($catTotals[$item['category']])) {
|
||||
$realSum = $catTotals[$item['category']];
|
||||
$hasMatchingExpense = true;
|
||||
}
|
||||
// C. Correspondance par mots-clés bancaires
|
||||
elseif (!empty($item['mapping_keywords'])) {
|
||||
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
||||
foreach ($unlinkedExpenses as $uexp) {
|
||||
foreach ($keywords as $kw) {
|
||||
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
|
||||
$realSum += (float)$uexp['amount'];
|
||||
$hasMatchingExpense = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,79 +121,55 @@ $totalRevenus = 0;
|
||||
|
||||
$realAbs = abs($realSum);
|
||||
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
|
||||
|
||||
$rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense';
|
||||
if ($item['is_estimate']) $rowClass .= ' row-estimate';
|
||||
$rowClass = 'row-expense' . ($item['is_estimate'] ? ' row-estimate' : '');
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid var(--border-light);">
|
||||
<td style="padding:15px;">
|
||||
<strong><?= htmlspecialchars($item['name']) ?></strong>
|
||||
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">('.tr('bud_est_short').')</small>' : '' ?>
|
||||
|
||||
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">(Variable)</small>' : '' ?>
|
||||
<?php if(!empty($item['mapping_keywords'])): ?>
|
||||
<span title="<?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if(!empty($item['reg_month'])): ?>
|
||||
<div style="font-size:0.75rem; color:#94a3b8; font-style:italic; margin-top:2px;">
|
||||
📅 <?= sprintf(tr('bud_reg_planned_in'), tr('month_'.str_pad(array_search($item['reg_month'], $moisFr), 2, '0', STR_PAD_LEFT))) ?>
|
||||
</div>
|
||||
<span title="Reconnaissance bancaire : <?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:<?= $item['category']==='income'?'#10b981':'#1e293b' ?>;">
|
||||
<?= number_format($targetAbs, 2, ',', ' ') ?> €
|
||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:#1e293b;">
|
||||
- <?= number_format($targetAbs, 2, ',', ' ') ?> €
|
||||
|
||||
<?php if ($hasMatchingExpense): ?>
|
||||
<?php
|
||||
// Le "Gap" est la différence visuelle.
|
||||
$gap = $realAbs - $targetAbs;
|
||||
|
||||
<?php $gap = $realAbs - $targetAbs;
|
||||
if ($gap > 0.05): ?>
|
||||
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">
|
||||
<?= $item['category'] === 'income' ? tr('bud_bonus') : tr('bud_overrun') ?> : +<?= number_format($gap, 2, ',', ' ') ?> €
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">Dépassé : +<?= number_format($gap, 2, ',', ' ') ?> €</div>
|
||||
<?php elseif ($gap < -0.05): ?>
|
||||
<div style="font-size:0.75rem; color:#f59e0b; font-weight:normal;">
|
||||
<?= tr('bud_remaining') ?> : <?= number_format(abs($gap), 2, ',', ' ') ?> €
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:#f59e0b;">Reste : <?= number_format(abs($gap), 2, ',', ' ') ?> €</div>
|
||||
<?php else: ?>
|
||||
<div style="font-size:0.75rem; color:#10b981; font-weight:normal;">
|
||||
<?= tr('bud_exact_amount') ?> ✓
|
||||
</div>
|
||||
<div style="font-size:0.75rem; color:#10b981;">Atteint ✓</div>
|
||||
<?php endif; ?>
|
||||
<?php elseif ($item['type'] === 'Annuel'): ?>
|
||||
<div style="font-size:0.75rem; color:#94a3b8; font-weight:normal;"><?= tr('bud_per_month_short') ?> <?= number_format($amountToAdd, 2, ',', ' ') ?>/<?= tr('bud_month_short') ?></div>
|
||||
<div style="font-size:0.75rem; color:#94a3b8;"><?= number_format($amountToAdd, 2, ',', ' ') ?>/mois</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td style="padding:15px;">
|
||||
<span class="badge-type <?= strtolower($item['type']) ?>" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
|
||||
<?= tr('bud_freq_'.strtolower($item['type'])) ?>
|
||||
<span class="badge-type" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
|
||||
<?= htmlspecialchars($item['type']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td>
|
||||
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ?: '-' ?></td>
|
||||
|
||||
<td style="padding:15px;">
|
||||
<?php if ($isAutoChecked): ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;">
|
||||
<span>✓</span> <?= tr('bud_state_validated') ?>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;"><span>✓</span> Validé</div>
|
||||
<?php elseif($hasMatchingExpense): ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;">
|
||||
<span>⏳</span> <?= tr('bud_state_partial') ?>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;"><span>⏳</span> Partiel</div>
|
||||
<?php else: ?>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;">
|
||||
<span>○</span> <?= tr('bud_state_waiting') ?>
|
||||
</div>
|
||||
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;"><span>○</span> En attente</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td style="padding:15px; text-align:right;">
|
||||
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
|
||||
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>">✏️</button>
|
||||
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
|
||||
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier">✏️</button>
|
||||
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="Supprimer">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -191,34 +177,30 @@ $totalRevenus = 0;
|
||||
</tbody>
|
||||
<tfoot style="background:#f8fafc;">
|
||||
<tr>
|
||||
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_income_smoothed') ?></strong></td>
|
||||
<td colspan="1" style="padding:15px;"><strong>Total Revenus Lissés</strong></td>
|
||||
<td colspan="5" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenus, 2, ',', ' ') ?> €</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_expenses_smoothed') ?></strong></td>
|
||||
<td colspan="1" style="padding:15px;"><strong>Total Dépenses & Estimations</strong></td>
|
||||
<td colspan="5" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepenses, 2, ',', ' ') ?> €</strong></td>
|
||||
</tr>
|
||||
<tr style="border-top: 2px solid #e2e8f0; background: white;">
|
||||
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong><?= tr('bud_theoretical_balance_recap') ?></strong></td>
|
||||
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong>Reste à Vivre (Équilibre)</strong></td>
|
||||
<?php $balance = $totalRevenus - $totalDepenses; ?>
|
||||
<td colspan="5" style="padding:15px; font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>">
|
||||
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / <?= tr('bud_month_short') ?></strong>
|
||||
<td colspan="5" style="padding:15px; font-size: 1.3em;">
|
||||
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="budget-note" style="margin-top:15px; font-size:0.85rem; color:#64748b;">
|
||||
<p>* <?= tr('bud_recap_footer_note') ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="budgetRecapModal" class="pf-modal" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center;">
|
||||
<div class="pf-modal-content" style="background:white; width:95%; max-width:500px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px; position:relative;">
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;"><?= tr('bud_recap_modal_add') ?></h3>
|
||||
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;">Ajouter une ligne</h3>
|
||||
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">×</button>
|
||||
</div>
|
||||
|
||||
@@ -227,90 +209,65 @@ $totalRevenus = 0;
|
||||
<input type="hidden" name="id" id="item_id">
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_name') ?></label>
|
||||
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="<?= tr('bud_ph_item_name') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_keywords') ?></label>
|
||||
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="<?= tr('bud_ph_keywords') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
|
||||
<small style="color:#64748b; font-size:0.75rem;"><?= tr('bud_help_keywords') ?></small>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Nom de la ligne</label>
|
||||
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="ex: Assurance Auto, Estimation Courses..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_amount_eur') ?></label>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Montant Prévu (€)</label>
|
||||
<input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_day') ?></label>
|
||||
<input type="number" min="1" max="31" name="payment_day" id="item_day" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Type de Dépense</label>
|
||||
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="0">Fixe (Facture, Prélèvement)</option>
|
||||
<option value="1">Variable (Estimation, Enveloppe)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_category') ?></label>
|
||||
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="expense"><?= tr('bud_cat_expense') ?></option>
|
||||
<option value="income"><?= tr('bud_cat_income') ?></option>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Catégorie Cible (La jauge)</label>
|
||||
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;" required>
|
||||
<option value="" disabled selected>-- Choisir --</option>
|
||||
<?php foreach ($dbCategories as $c): ?>
|
||||
<?php if(strtolower($c['type']) === 'expense'): // On n'affiche que les catégories de dépenses ?>
|
||||
<option value="<?= htmlspecialchars($c['code']) ?>">
|
||||
<?= htmlspecialchars($c['icon'] . ' ' . $c['label']) ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_frequency') ?></label>
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Fréquence</label>
|
||||
<select name="type" id="item_type" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="Mensuel"><?= tr('bud_freq_mensuel') ?></option>
|
||||
<option value="Annuel"><?= tr('bud_freq_annuel') ?></option>
|
||||
<option value="Mensuel">Mensuel</option>
|
||||
<option value="Annuel">Annuel</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:15px; margin-bottom:25px;">
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_amount_type') ?></label>
|
||||
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value="0"><?= tr('bud_type_fixed') ?></option>
|
||||
<option value="1"><?= tr('bud_type_variable') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_regularization') ?></label>
|
||||
<select name="reg_month" id="item_reg_month" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
||||
<option value=""><?= tr('bud_reg_none') ?></option>
|
||||
<?php
|
||||
foreach($moisFr as $index => $m) {
|
||||
if($index == 0) continue;
|
||||
echo "<option value='$m'>" . tr('month_'.str_pad($index, 2, '0', STR_PAD_LEFT)) . "</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Mots-clés (Reconnaissance Bancaire)</label>
|
||||
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="ex: NETFLIX, DOMOFINANCE..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
|
||||
<small style="color:#64748b; font-size:0.75rem;">Optionnel. Permet au système de lier automatiquement un import CSV à cette ligne.</small>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
||||
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_save') ?></button>
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top: 25px;">
|
||||
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Annuler</button>
|
||||
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
'bud_recap_modal_add': <?= json_encode(tr('bud_recap_modal_add')) ?>,
|
||||
'bud_recap_modal_edit': <?= json_encode(tr('bud_recap_modal_edit')) ?>,
|
||||
'bud_recap_confirm_delete': <?= json_encode(tr('bud_recap_confirm_delete')) ?>,
|
||||
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>,
|
||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
||||
'bud_saving': <?= json_encode(tr('bud_sav_saving') ?? 'Sauvegarde...') ?>
|
||||
};
|
||||
|
||||
function openRecapModal(mode) {
|
||||
if (mode === "add") {
|
||||
document.getElementById("recapModalTitle").innerText = window.I18N['bud_recap_modal_add'] || 'Ajouter';
|
||||
document.getElementById("recapModalTitle").innerText = 'Ajouter une ligne';
|
||||
document.getElementById("item_id").value = "";
|
||||
document.getElementById("recapForm").reset();
|
||||
}
|
||||
@@ -324,113 +281,56 @@ function closeRecapModal() {
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('budgetRecapModal');
|
||||
if (event.target == modal) {
|
||||
closeRecapModal();
|
||||
}
|
||||
if (event.target == document.getElementById('budgetRecapModal')) closeRecapModal();
|
||||
}
|
||||
|
||||
function editRecapItem(item) {
|
||||
const data = typeof item === "string" ? JSON.parse(item) : item;
|
||||
|
||||
document.getElementById("recapModalTitle").innerText = (window.I18N['bud_recap_modal_edit'] || 'Editer') + " : " + data.name;
|
||||
document.getElementById("recapModalTitle").innerText = "Editer : " + data.name;
|
||||
document.getElementById("item_id").value = data.id;
|
||||
document.getElementById("item_name").value = data.name;
|
||||
document.getElementById("item_keywords").value = data.mapping_keywords || '';
|
||||
document.getElementById("item_amount").value = Math.abs(data.amount);
|
||||
document.getElementById("item_category").value = data.category;
|
||||
document.getElementById("item_type").value = data.type;
|
||||
document.getElementById("item_day").value = data.payment_day;
|
||||
document.getElementById("item_reg_month").value = data.reg_month || "";
|
||||
document.getElementById("item_is_estimate").value = data.is_estimate;
|
||||
|
||||
document.getElementById("budgetRecapModal").style.display = "flex";
|
||||
document.body.classList.add('no-scroll');
|
||||
}
|
||||
|
||||
// --- 2. INTERCEPTION ASYNCHRONE DU FORMULAIRE ---
|
||||
const recapForm = document.getElementById('recapForm');
|
||||
if (recapForm) {
|
||||
recapForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerText;
|
||||
submitBtn.innerText = window.I18N['bud_saving'] || '⏳ ...';
|
||||
submitBtn.disabled = true;
|
||||
document.getElementById('recapForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const btn = this.querySelector('button[type="submit"]');
|
||||
btn.disabled = true; btn.innerText = '⏳ ...';
|
||||
|
||||
const formData = new FormData(this);
|
||||
formData.append('ajax', '1');
|
||||
|
||||
// 💡 Utilisation sécurisée de getAttribute
|
||||
const actionUrl = this.getAttribute('action');
|
||||
const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl;
|
||||
|
||||
try {
|
||||
const response = await fetch(finalUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
// 💡 Lecture robuste (anti-Warnings PHP)
|
||||
const textResult = await response.text();
|
||||
try {
|
||||
const result = JSON.parse(textResult);
|
||||
if (result.success) {
|
||||
closeRecapModal();
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
||||
}
|
||||
} catch (jsonError) {
|
||||
console.error("Réponse non-JSON :", textResult);
|
||||
alert("Le serveur a renvoyé une erreur PHP. Regarde la console (F12).");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur Fetch:", error);
|
||||
alert(window.I18N['bud_err_tech'] || 'Erreur technique');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = originalText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. SUPPRESSION ASYNCHRONE ---
|
||||
async function deleteRecapItem(id) {
|
||||
if (!confirm(window.I18N['bud_recap_confirm_delete'] || "Confirmer la suppression ?")) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("action", "delete");
|
||||
formData.append("id", id);
|
||||
formData.append("ajax", "1"); // Signale à l'API qu'on attend du JSON
|
||||
const formData = new FormData(this);
|
||||
formData.append('ajax', '1');
|
||||
const url = this.getAttribute('action');
|
||||
|
||||
try {
|
||||
const response = await fetch("modules/budget/includes/api/manage-item.php", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const textResult = await response.text();
|
||||
try {
|
||||
const result = JSON.parse(textResult);
|
||||
// On s'assure que si l'API ne renvoie pas success, on le signale
|
||||
if (result.success !== false) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert((window.I18N['bud_err_delete'] || 'Erreur de suppression') + " : " + (result.error || ""));
|
||||
}
|
||||
} catch (jsonErr) {
|
||||
console.error("Réponse non-JSON lors de la suppression :", textResult);
|
||||
window.location.reload(); // Fallback si l'API redirige au lieu de répondre en JSON
|
||||
}
|
||||
const res = await fetch(url.startsWith('/') ? url.substring(1) : url, { method: 'POST', body: formData });
|
||||
const data = await res.json();
|
||||
if (data.success) window.location.reload();
|
||||
else alert("Erreur : " + data.error);
|
||||
} catch (err) {
|
||||
console.error("Erreur réseau Suppression:", err);
|
||||
alert(window.I18N['bud_err_delete'] || 'Erreur réseau');
|
||||
alert("Erreur technique lors de la sauvegarde.");
|
||||
} finally {
|
||||
btn.disabled = false; btn.innerText = 'Enregistrer';
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteRecapItem(id) {
|
||||
if (!confirm("Confirmer la suppression ?")) return;
|
||||
const formData = new FormData();
|
||||
formData.append("action", "delete"); formData.append("id", id); formData.append("ajax", "1");
|
||||
try {
|
||||
const res = await fetch("modules/budget/includes/api/manage-item.php", { method: "POST", body: formData });
|
||||
const data = await res.json();
|
||||
if (data.success !== false) window.location.reload();
|
||||
else alert("Erreur : " + data.error);
|
||||
} catch (err) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+145
-107
@@ -60,7 +60,6 @@ if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') {
|
||||
if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
||||
$count = 0;
|
||||
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
// Mémorisation du mapping étendu (incluant budget_item_id)
|
||||
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category, budget_item_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category), budget_item_id = VALUES(budget_item_id)");
|
||||
|
||||
if (isset($_POST['lines']) && is_array($_POST['lines'])) {
|
||||
@@ -152,9 +151,43 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 3. RECUPERATION DES DONNEES ET CALCULS
|
||||
// 3. RECUPERATION DES DONNEES ET CALCULS (100% DYNAMIQUE)
|
||||
// ============================================================================
|
||||
|
||||
// --- LECTURE DYNAMIQUE DES CATEGORIES ---
|
||||
$stmtCats = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type DESC, label ASC");
|
||||
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$categoriesConfig = [];
|
||||
foreach ($dbCategories as $c) {
|
||||
$catType = ($c['type'] === 'Income') ? 'credit' : 'debit';
|
||||
$categoriesConfig[$c['code']] = [
|
||||
'type' => $catType,
|
||||
'db_type' => $c['type'],
|
||||
'label' => ($c['icon'] ? $c['icon'] . ' ' : '') . $c['label'],
|
||||
'budget' => 0, // Sera rempli par les règles
|
||||
'color' => $c['color'] ?: '#64748b',
|
||||
'suggestions' => []
|
||||
];
|
||||
}
|
||||
|
||||
// Fallback "Autres" au cas où la BDD serait vide ou pour les dépenses non classées
|
||||
if (!isset($categoriesConfig['AUTRES'])) {
|
||||
$categoriesConfig['AUTRES'] = [
|
||||
'type'=>'debit', 'db_type'=>'Expense', 'label'=>'📁 Autres / Divers',
|
||||
'budget'=>0, 'color'=>'#94a3b8', 'suggestions'=>[]
|
||||
];
|
||||
}
|
||||
|
||||
// Peuplement dynamique des suggestions via les règles existantes
|
||||
$stmtRules = $pdo->query("SELECT keyword, category FROM pf_import_rules");
|
||||
while ($rule = $stmtRules->fetch(PDO::FETCH_ASSOC)) {
|
||||
if (isset($categoriesConfig[$rule['category']])) {
|
||||
$categoriesConfig[$rule['category']]['suggestions'][] = $rule['keyword'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
|
||||
$stmtCheckClose->execute([$viewMonthDate]);
|
||||
$closureJson = $stmtCheckClose->fetchColumn();
|
||||
@@ -184,27 +217,42 @@ $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$paidItemIds = array_column(array_filter($allExpenses, fn($e) => !empty($e['budget_item_id'])), 'budget_item_id');
|
||||
$realExpensesLabels = array_column(array_filter($allExpenses, fn($e) => $e['amount'] < 0), 'label');
|
||||
|
||||
$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_income_prevu = 0;
|
||||
$budget_income_prevu = 0;
|
||||
$total_income = 0; $total_expenses_prevues = 0;
|
||||
$reste_a_venir_calc = 0;
|
||||
$fixedChargesList = []; $incomeList = []; $pending_charges = [];
|
||||
|
||||
// ============================================================================
|
||||
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
|
||||
// ============================================================================
|
||||
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items ORDER BY name ASC");
|
||||
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$absAmount = abs((float)$item['amount']);
|
||||
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
||||
$name = trim($item['name']);
|
||||
$catCode = $item['category']; // Ex: FIXED, FMCG, INCOME...
|
||||
$isIncome = ((float)$item['amount'] > 0); // La norme est désormais définie par le signe du montant
|
||||
|
||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||
}
|
||||
if ($item['category'] === 'income') {
|
||||
if ($isIncome) {
|
||||
$incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||
$total_income += $amt;
|
||||
$budget_income_prevu += $amt;
|
||||
$budget_income_prevu += $amt;
|
||||
|
||||
// Attribution au compte de revenu défini, sinon au premier disponible
|
||||
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
|
||||
$categoriesConfig[$catCode]['budget'] += $amt;
|
||||
} else {
|
||||
$incomeCatKey = array_key_first(array_filter($categoriesConfig, fn($c) => $c['db_type'] === 'Income'));
|
||||
if ($incomeCatKey) $categoriesConfig[$incomeCatKey]['budget'] += $amt;
|
||||
}
|
||||
|
||||
} else {
|
||||
$total_expenses_prevues += $amt;
|
||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||
|
||||
// C'est une charge fixe (Mensuel + Is_Estimate = 0)
|
||||
if ($item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||
|
||||
$isPaid = false;
|
||||
if (in_array($item['id'], $paidItemIds)) $isPaid = true;
|
||||
elseif (!empty($item['mapping_keywords'])) {
|
||||
@@ -221,90 +269,68 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount];
|
||||
}
|
||||
}
|
||||
if (!empty($item['mapping_keywords'])) {
|
||||
if (stripos($item['mapping_keywords'], 'FMCG') !== false) $budget_fmcg += $amt;
|
||||
if (stripos($item['mapping_keywords'], 'School') !== false) $budget_school += $amt;
|
||||
if (stripos($item['mapping_keywords'], 'Essence') !== false) $budget_essence += $amt;
|
||||
}
|
||||
|
||||
if ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') {
|
||||
$budget_frais += $absAmount;
|
||||
// NOUVEAU : Attribution du budget prévisionnel (le Plafond) via la catégorie dynamique
|
||||
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
|
||||
$categoriesConfig[$catCode]['budget'] += $amt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$budget_autres = max(0, $total_income - $total_expenses_prevues);
|
||||
|
||||
// TRADUCTION DES CATÉGORIES
|
||||
$categoriesConfig = [
|
||||
'Income' => ['type'=>'credit', 'label'=>tr('cat_income'), 'budget'=>$budget_income_prevu, 'color'=>'#10b981', 'suggestions'=>[]],
|
||||
'FMCG' => ['type'=>'debit', 'label'=>tr('cat_fmcg'), 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']],
|
||||
'Essence' => ['type'=>'debit', 'label'=>tr('cat_fuel'), 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']],
|
||||
'School' => ['type'=>'debit', 'label'=>tr('cat_school'), 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
|
||||
'Frais' => ['type'=>'debit', 'label'=>tr('cat_fixed'), 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]],
|
||||
'Autres' => ['type'=>'debit', 'label'=>tr('cat_others'), 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']],
|
||||
'Apports' => ['type'=>'debit', 'label'=>tr('cat_contributions') ?? 'Apports & Projets', 'budget'=>0, 'color'=>'#0ea5e9', 'suggestions'=>['Alex', 'Laia', 'Remboursement']],
|
||||
'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
|
||||
];
|
||||
// L'enveloppe "Autres" prend tout le reste du budget non alloué
|
||||
$categoriesConfig['AUTRES']['budget'] = max(0, $total_income - $total_expenses_prevues);
|
||||
|
||||
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
|
||||
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
|
||||
$total_rentrees = 0;
|
||||
$depenses_reelles = 0;
|
||||
|
||||
// VENTILATION DYNAMIQUE DES DÉPENSES
|
||||
foreach ($allExpenses as $exp) {
|
||||
$cat = $exp['category'];
|
||||
if (!isset($totals[$cat])) $cat = 'Autres';
|
||||
if (!isset($categoriesConfig[$cat])) $cat = 'AUTRES';
|
||||
|
||||
$val = (float)$exp['amount'];
|
||||
|
||||
if ($cat === 'Income') { $totals[$cat] += $val; }
|
||||
else {
|
||||
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val;
|
||||
if ($categoriesConfig[$cat]['db_type'] === 'Income') {
|
||||
$totals[$cat] += $val;
|
||||
} else {
|
||||
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val; // Remboursement
|
||||
else $totals[$cat] += abs($val);
|
||||
}
|
||||
|
||||
$expensesByCategory[$cat][] = $exp;
|
||||
|
||||
if ($val > 0) {
|
||||
if ($cat === 'Income' || $cat !== 'Frais') $total_rentrees += $val;
|
||||
else $depenses_reelles -= $val;
|
||||
if ($categoriesConfig[$cat]['db_type'] === 'Income') { $total_rentrees += $val; }
|
||||
else { $depenses_reelles -= $val; }
|
||||
} else {
|
||||
$depenses_reelles += abs($val);
|
||||
}
|
||||
}
|
||||
|
||||
// Reste à venir dynamiques
|
||||
$ecole_depense = isset($totals['School']) ? $totals['School'] : 0;
|
||||
$reste_ecole = max(0, $budget_school - $ecole_depense);
|
||||
if ($reste_ecole > 0) {
|
||||
$reste_a_venir_calc += $reste_ecole;
|
||||
$pending_charges[] = ['name' => tr('bud_rem_school'), 'amount' => $reste_ecole];
|
||||
}
|
||||
|
||||
$fmcg_depense = isset($totals['FMCG']) ? $totals['FMCG'] : 0;
|
||||
$reste_fmcg = max(0, $budget_fmcg - $fmcg_depense);
|
||||
if ($reste_fmcg > 0) {
|
||||
$reste_a_venir_calc += $reste_fmcg;
|
||||
$pending_charges[] = ['name' => tr('bud_rem_fmcg'), 'amount' => $reste_fmcg];
|
||||
}
|
||||
|
||||
$essence_depense = isset($totals['Essence']) ? $totals['Essence'] : 0;
|
||||
$reste_essence = max(0, $budget_essence - $essence_depense);
|
||||
if ($reste_essence > 0) {
|
||||
$reste_a_venir_calc += $reste_essence;
|
||||
$pending_charges[] = ['name' => tr('bud_rem_fuel'), 'amount' => $reste_essence];
|
||||
// RESTE A VENIR PAR CATEGORIE (Remplace les variables codées en dur)
|
||||
foreach ($categoriesConfig as $code => $conf) {
|
||||
if ($conf['db_type'] === 'Expense' && $conf['budget'] > 0) {
|
||||
$spent = $totals[$code] ?? 0;
|
||||
$rem = max(0, $conf['budget'] - $spent);
|
||||
if ($rem > 0) {
|
||||
$reste_a_venir_calc += $rem;
|
||||
$pending_charges[] = ['name' => 'Reste ' . strip_tags($conf['label']), 'amount' => $rem];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// F. Calculs des KPIs finaux
|
||||
$rentrees_salaires_reels = $totals['Income'] ?? 0;
|
||||
$rentrees_salaires_reels = 0;
|
||||
foreach($categoriesConfig as $code => $conf) { if($conf['db_type'] === 'Income') $rentrees_salaires_reels += ($totals[$code] ?? 0); }
|
||||
|
||||
$rentrees_autres = $total_rentrees - $rentrees_salaires_reels;
|
||||
$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu);
|
||||
|
||||
$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres;
|
||||
|
||||
// 1. On calcule s'il reste des salaires/revenus prévus à encaisser
|
||||
$revenus_a_venir = max(0, $budget_income_prevu - $rentrees_salaires_reels);
|
||||
|
||||
// 2. Le solde théorique part de ta vraie saisie bancaire actuelle
|
||||
$solde_theorique_calc = $snapshot['amount'] + $revenus_a_venir - $reste_a_venir_calc;
|
||||
|
||||
if ($isClosed) {
|
||||
@@ -335,7 +361,6 @@ function getDisplayLogic($spent, $bg, $type) {
|
||||
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
|
||||
}
|
||||
|
||||
// Noms des mois traduits
|
||||
$monthNames = [
|
||||
1 => tr('month_01'), 2 => tr('month_02'), 3 => tr('month_03'), 4 => tr('month_04'),
|
||||
5 => tr('month_05'), 6 => tr('month_06'), 7 => tr('month_07'), 8 => tr('month_08'),
|
||||
@@ -505,11 +530,11 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$isClosed): ?>
|
||||
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes($conf['label']) ?>')">+</button>
|
||||
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes(strip_tags($conf['label'])) ?>')">+</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php $barCol = ($key === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>
|
||||
<?php $barCol = ($conf['db_type'] === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>
|
||||
<div style="background:#f1f5f9; height:4px; width:100%;">
|
||||
<div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
|
||||
</div>
|
||||
@@ -568,7 +593,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<label class="pf-label"><?= tr('bud_category') ?></label>
|
||||
<select name="category" id="modalCatSelect" class="pf-input" onchange="handleModalCatChange(this)">
|
||||
<?php foreach($categoriesConfig as $key => $conf): ?>
|
||||
<option value="<?= $key ?>"><?= $conf['label'] ?></option>
|
||||
<option value="<?= $key ?>"><?= strip_tags($conf['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@@ -587,23 +612,10 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<datalist id="modalSuggestions"></datalist>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="blockInputSelect" style="margin-bottom:15px; display:none;">
|
||||
<label class="pf-label"><?= tr('bud_beneficiary') ?></label>
|
||||
<select name="label_select" id="schoolSelect" class="pf-input">
|
||||
<?php
|
||||
$stmtSchoolLabel = $pdo->query("SELECT name FROM pf_people WHERE role IN ('enfant', 'nounou') OR role IS NULL ORDER BY id ASC");
|
||||
while($k = $stmtSchoolLabel->fetch(PDO::FETCH_ASSOC)) {
|
||||
$pName = htmlspecialchars($k['name']);
|
||||
echo "<option value='{$pName}'>{$pName}</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;">
|
||||
<label class="pf-label"><?= tr('bud_fixed_charge') ?></label>
|
||||
<label class="pf-label">Lier à une Charge Fixe (Optionnel)</label>
|
||||
<select name="budget_item_id" id="fraisSelect" class="pf-input" disabled>
|
||||
<option value=""><?= tr('bud_select_beneficiary') ?></option>
|
||||
<option value="">-- Aucune --</option>
|
||||
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@@ -720,11 +732,11 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="padding:4px; font-size:0.85rem; flex:1;">
|
||||
<option value="">-- <?= $isCrd ? tr('bud_ignore') : tr('bud_to_define') ?> --</option>
|
||||
<?php foreach ($categoriesConfig as $k => $c): ?>
|
||||
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= $c['label'] ?></option>
|
||||
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= strip_tags($c['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
|
||||
<option value="">-- <?= tr('bud_is_charge') ?> --</option>
|
||||
<option value="">-- Lier à une Charge Fixe --</option>
|
||||
<?php foreach ($fixedChargesList as $fc): ?>
|
||||
<option value="<?= $fc['id'] ?>" <?= ($row['suggested_item_id'] == $fc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($fc['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
@@ -757,7 +769,6 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
||||
document.body.classList.add('no-scroll');
|
||||
<?php endif; ?>
|
||||
|
||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
@@ -769,34 +780,42 @@ window.I18N = {
|
||||
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
||||
};
|
||||
|
||||
// --- DICTIONNAIRE JS DYNAMIQUE ---
|
||||
const catConfigs = <?= json_encode($categoriesConfig) ?>;
|
||||
|
||||
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
|
||||
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
|
||||
function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('no-scroll'); }
|
||||
function closeSuiviModal(id) { document.getElementById(id).classList.remove('open'); document.body.classList.remove('no-scroll');}
|
||||
|
||||
const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>;
|
||||
|
||||
// Gestion dynamique du formulaire modal
|
||||
function handleModalCatChange(select) {
|
||||
const catKey = select.value;
|
||||
const conf = catConfigs[catKey] || {db_type: 'Expense', suggestions: []};
|
||||
|
||||
document.getElementById('blockInputText').style.display = 'none';
|
||||
document.getElementById('blockInputSelect').style.display = 'none';
|
||||
document.getElementById('blockInputText').style.display = 'block';
|
||||
document.getElementById('blockInputFrais').style.display = 'none';
|
||||
document.getElementById('blockInputIncome').style.display = 'none';
|
||||
|
||||
document.getElementById('modalLabelInput').required = false;
|
||||
document.getElementById('modalLabelInput').required = true;
|
||||
document.getElementById('fraisSelect').required = false;
|
||||
document.getElementById('incomeSelect').required = false;
|
||||
document.getElementById('fraisSelect').disabled = true;
|
||||
document.getElementById('incomeSelect').disabled = true;
|
||||
|
||||
if (catKey === 'School') { document.getElementById('blockInputSelect').style.display = 'block'; }
|
||||
else if (catKey === 'Frais') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputFrais').style.display = 'block'; document.getElementById('fraisSelect').required = true; document.getElementById('fraisSelect').disabled = false; }
|
||||
else if (catKey === 'Income') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputIncome').style.display = 'block'; document.getElementById('incomeSelect').required = true; document.getElementById('incomeSelect').disabled = false; }
|
||||
else {
|
||||
document.getElementById('blockInputText').style.display = 'block'; document.getElementById('modalLabelInput').required = true;
|
||||
const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
|
||||
if (suggestions[catKey]) { suggestions[catKey].forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); }); }
|
||||
if (conf.db_type === 'Income') {
|
||||
document.getElementById('blockInputIncome').style.display = 'block';
|
||||
document.getElementById('incomeSelect').disabled = false;
|
||||
document.getElementById('incomeSelect').required = true;
|
||||
} else {
|
||||
// Optionnel : lier l'Expense à une charge fixe
|
||||
document.getElementById('blockInputFrais').style.display = 'block';
|
||||
document.getElementById('fraisSelect').disabled = false;
|
||||
}
|
||||
|
||||
const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
|
||||
if (conf.suggestions) {
|
||||
conf.suggestions.forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,8 +827,10 @@ function openAddModal(catKey, catLabel) {
|
||||
document.getElementById('modalGestionMonth').value = activeViewMonth;
|
||||
document.getElementById('modalLabelInput').value = "";
|
||||
document.getElementById('modalAmount').value = "";
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey; handleModalCatChange(catSelect);
|
||||
document.getElementById('modalIsCredit').value = (catKey === 'Income') ? "1" : "0";
|
||||
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey;
|
||||
handleModalCatChange(catSelect);
|
||||
document.getElementById('modalIsCredit').value = (catConfigs[catKey]?.db_type === 'Income') ? "1" : "0";
|
||||
}
|
||||
|
||||
function openEditModal(e) {
|
||||
@@ -821,21 +842,30 @@ function openEditModal(e) {
|
||||
document.getElementById('modalLabelInput').value = e.label;
|
||||
document.getElementById('modalIsCredit').value = parseFloat(e.amount) > 0 ? "1" : "0";
|
||||
document.getElementById('modalAmount').value = Math.abs(parseFloat(e.amount));
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category; handleModalCatChange(catSelect);
|
||||
if (e.category === 'Frais') document.getElementById('fraisSelect').value = e.budget_item_id;
|
||||
else if (e.category === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
|
||||
|
||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category;
|
||||
handleModalCatChange(catSelect);
|
||||
|
||||
if (catConfigs[e.category]?.db_type === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
|
||||
else document.getElementById('fraisSelect').value = e.budget_item_id;
|
||||
}
|
||||
|
||||
// Gestion dynamique du CSV Import
|
||||
function handleLineCatChange(select, isInit = false) {
|
||||
const row = select.closest('tr');
|
||||
const fSel = row.querySelector('.select-frais'); const iSel = row.querySelector('.select-income');
|
||||
const fSel = row.querySelector('.select-frais');
|
||||
const iSel = row.querySelector('.select-income');
|
||||
const conf = catConfigs[select.value] || null;
|
||||
|
||||
fSel.style.display = 'none'; iSel.style.display = 'none';
|
||||
if (!isInit) { fSel.value = ''; iSel.value = ''; }
|
||||
fSel.disabled = true; iSel.disabled = true;
|
||||
|
||||
if (select.value === 'Frais') { fSel.style.display = 'block'; fSel.disabled = false; }
|
||||
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; }
|
||||
if (conf && conf.db_type === 'Income') {
|
||||
iSel.style.display = 'block'; iSel.disabled = false;
|
||||
} else if (conf) {
|
||||
fSel.style.display = 'block'; fSel.disabled = false;
|
||||
}
|
||||
checkValidation();
|
||||
}
|
||||
|
||||
@@ -844,16 +874,25 @@ function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disable
|
||||
function checkValidation() {
|
||||
let miss = 0;
|
||||
document.querySelectorAll('.line-checkbox:checked').forEach(cb => {
|
||||
const row = cb.closest('tr'); const isCrd = row.querySelector('.is-credit-flag').value === '1'; const cat = row.querySelector('.line-select').value;
|
||||
let v = true; if (cat==="") { if(!isCrd) v = false; } else if (cat==='Frais' && row.querySelector('.select-frais').value==="") v=false; else if (cat==='Income' && row.querySelector('.select-income').value==="") v=false;
|
||||
const row = cb.closest('tr');
|
||||
const isCrd = row.querySelector('.is-credit-flag').value === '1';
|
||||
const cat = row.querySelector('.line-select').value;
|
||||
const conf = catConfigs[cat] || null;
|
||||
|
||||
let v = true;
|
||||
if (cat === "") {
|
||||
if (!isCrd) v = false;
|
||||
} else if (conf && conf.db_type === 'Income' && row.querySelector('.select-income').value === "") {
|
||||
v = false;
|
||||
}
|
||||
if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = '';
|
||||
});
|
||||
const btn = document.getElementById('btnImport'); const msg = document.getElementById('missingCount');
|
||||
if(miss>0) { btn.disabled = true; btn.style.opacity=0.5; msg.style.display='inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || ''); }
|
||||
else { btn.disabled = false; btn.style.opacity=1; msg.style.display='none'; }
|
||||
if(miss > 0) { btn.disabled = true; btn.style.opacity = 0.5; msg.style.display = 'inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || ''); }
|
||||
else { btn.disabled = false; btn.style.opacity = 1; msg.style.display = 'none'; }
|
||||
}
|
||||
|
||||
if(document.getElementById('formMapping')) {
|
||||
if (document.getElementById('formMapping')) {
|
||||
document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s, true));
|
||||
checkValidation();
|
||||
}
|
||||
@@ -897,7 +936,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
submitBtn.innerText = '⏳ ...';
|
||||
|
||||
const formData = new FormData(formExpense);
|
||||
// Force l'action ici pour être sûr
|
||||
formData.set('action', 'save_expense_manual');
|
||||
|
||||
const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php';
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
/* En-tête & Boutons */
|
||||
/* =========================================================
|
||||
EN-TÊTE & BOUTONS
|
||||
========================================================= */
|
||||
.fc-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -62,7 +64,9 @@
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* Calendrier Mensuel */
|
||||
/* =========================================================
|
||||
CALENDRIER MENSUEL (Vue Grille Mois)
|
||||
========================================================= */
|
||||
.fc-month-calendar-wrapper {
|
||||
background: white;
|
||||
border: 1px solid var(--pf-border);
|
||||
@@ -114,6 +118,7 @@
|
||||
|
||||
.fc-view-controls {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
width: 100% !important;
|
||||
background: #f1f5f9 !important;
|
||||
padding: 4px !important;
|
||||
@@ -128,7 +133,7 @@
|
||||
padding: 6px 16px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--pf-text-muted);
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
}
|
||||
@@ -206,7 +211,9 @@
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* Décompte Congés (Pills) */
|
||||
/* =========================================================
|
||||
DÉCOMPTE CONGÉS MENSUEL (PILLS)
|
||||
========================================================= */
|
||||
.fc-month-balances {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -226,12 +233,6 @@
|
||||
border: 1px solid var(--pf-border);
|
||||
box-shadow: var(--pf-shadow-sm);
|
||||
}
|
||||
.fc-minimal-balance-card strong.alex {
|
||||
color: var(--text-alex);
|
||||
}
|
||||
.fc-minimal-balance-card strong.laia {
|
||||
color: var(--text-laia);
|
||||
}
|
||||
.fc-minimal-chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -249,17 +250,17 @@
|
||||
}
|
||||
.fc-min-chip .type {
|
||||
font-weight: 700;
|
||||
color: var(--pf-text-muted);
|
||||
color: #64748b;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.fc-min-chip .val {
|
||||
font-weight: 800;
|
||||
color: var(--pf-text-main);
|
||||
color: #0f172a;
|
||||
}
|
||||
.fc-used-badge {
|
||||
margin-left: 6px;
|
||||
background: #fee2e2;
|
||||
color: var(--pf-danger);
|
||||
color: #ef4444;
|
||||
padding: 1px 5px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.65rem;
|
||||
@@ -272,7 +273,7 @@
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--pf-danger);
|
||||
color: #ef4444;
|
||||
font-weight: bold;
|
||||
}
|
||||
@keyframes pulseBurn {
|
||||
@@ -290,7 +291,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Couleurs des jours */
|
||||
/* =========================================================
|
||||
COULEURS DES JOURS (GRILLES)
|
||||
========================================================= */
|
||||
.fc-day--school-holiday {
|
||||
background: var(--c-school-holiday) !important;
|
||||
}
|
||||
@@ -304,35 +307,13 @@
|
||||
.fc-day--extra-off-carole {
|
||||
background: var(--c-extra-off) !important;
|
||||
}
|
||||
|
||||
.fc-day--selected {
|
||||
background: var(--c-selected) !important;
|
||||
outline: 2px solid var(--pf-primary);
|
||||
z-index: 5;
|
||||
}
|
||||
.fc-day--centre::after {
|
||||
content: "🏫";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.fc-day--avis::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: url("/modules/family-calendar/assets/img/avis.svg") no-repeat
|
||||
center;
|
||||
background-size: contain;
|
||||
}
|
||||
.fc-pep-sick-emoji {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fc-day--off-carole.fc-day--school-holiday,
|
||||
.fc-day--extra-off-carole.fc-day--school-holiday,
|
||||
.fc-day--centre.fc-day--school-holiday,
|
||||
@@ -340,7 +321,26 @@
|
||||
box-shadow: inset 0 -8px 0 0 var(--c-school-holiday) !important;
|
||||
}
|
||||
|
||||
/* Planning Hebo (Desktop) */
|
||||
.fc-day--today {
|
||||
background: #eff6ff !important;
|
||||
box-shadow: inset 0 0 0 1px #bfdbfe !important;
|
||||
}
|
||||
.fc-day--today .fc-day-number {
|
||||
display: inline;
|
||||
background: none;
|
||||
width: auto;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
color: var(--pf-primary);
|
||||
font-weight: 900;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
PLANNING GLOBAL (Matrice Annuelle)
|
||||
========================================================= */
|
||||
.fc-week-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -379,22 +379,17 @@
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
HARMONISATION DES BORDURES ET HEADERS STICKY (1px strict)
|
||||
========================================================= */
|
||||
/* HARMONISATION DES BORDURES ET HEADERS STICKY */
|
||||
#planningTable thead tr {
|
||||
height: 30px; /* On fixe la hauteur pour un calcul parfait */
|
||||
height: 30px;
|
||||
}
|
||||
#planningTable thead th {
|
||||
position: sticky;
|
||||
background: #f8fafc !important;
|
||||
border: none !important; /* On supprime les bordures natives */
|
||||
|
||||
/* On utilise UNIQUEMENT des ombres internes pour simuler 1px de bordure sans doublement */
|
||||
border: none !important;
|
||||
box-shadow:
|
||||
inset 0 -1px 0 var(--pf-border),
|
||||
inset -1px 0 0 var(--pf-border) !important;
|
||||
|
||||
padding: 6px;
|
||||
font-size: 0.75rem;
|
||||
z-index: 20;
|
||||
@@ -403,14 +398,14 @@
|
||||
}
|
||||
#planningTable thead tr:nth-child(1) th {
|
||||
top: 0;
|
||||
z-index: 22; /* Au premier plan */
|
||||
z-index: 22;
|
||||
}
|
||||
#planningTable thead tr:nth-child(2) th {
|
||||
top: 30px; /* Exactement la hauteur de la ligne 1 */
|
||||
top: 30px;
|
||||
z-index: 21;
|
||||
}
|
||||
#planningTable thead tr:nth-child(3) th {
|
||||
top: 60px; /* Ligne 1 + Ligne 2 */
|
||||
top: 60px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
@@ -425,10 +420,9 @@
|
||||
color: var(--text-laia) !important;
|
||||
}
|
||||
|
||||
/* Cellules standards du corps */
|
||||
#planningTable tbody td {
|
||||
position: relative;
|
||||
border: none !important; /* Reset de sécurité */
|
||||
border: none !important;
|
||||
border-right: 1px solid var(--pf-border) !important;
|
||||
border-bottom: 1px solid var(--pf-border) !important;
|
||||
height: 36px;
|
||||
@@ -467,13 +461,10 @@ td.col-laia-sub {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
font-size: 0.7rem;
|
||||
color: var(--pf-text-muted);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
GESTION DES COLONNES STICKY (Mois & Semaine)
|
||||
========================================================= */
|
||||
|
||||
/* GESTION DES COLONNES STICKY (Mois & Semaine) */
|
||||
#planningTable tbody td.col-sticky-mois {
|
||||
position: sticky !important;
|
||||
left: 0 !important;
|
||||
@@ -481,7 +472,6 @@ td.col-laia-sub {
|
||||
background: white !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#planningTable tbody td.col-sticky-mois .fc-sticky-mois-label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -491,42 +481,31 @@ td.col-laia-sub {
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 2. Sticky Semaine (Body) */
|
||||
#planningTable tbody td.col-sticky-sem {
|
||||
position: sticky !important;
|
||||
left: 54px !important;
|
||||
z-index: 14 !important;
|
||||
background: white !important;
|
||||
/* Ombre portée pour délimiter la zone sticky du reste du tableau */
|
||||
box-shadow: 4px 0 6px -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 3. Sticky Mois (Header) */
|
||||
#planningTable thead tr th.col-sticky-mois {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
z-index: 30 !important;
|
||||
background: var(--pf-bg-lighter) !important;
|
||||
/* Les bordures sont gérées par le box-shadow général du thead th */
|
||||
}
|
||||
|
||||
/* 4. Sticky Semaine (Header) */
|
||||
#planningTable thead tr th.col-sticky-sem {
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
left: 54px !important;
|
||||
z-index: 29 !important;
|
||||
background: var(--pf-bg-lighter) !important;
|
||||
/* Ombre interne (bordures) + Ombre portée (effet sticky) */
|
||||
box-shadow:
|
||||
inset 0 -1px 0 var(--pf-border),
|
||||
inset -1px 0 0 var(--pf-border),
|
||||
4px 0 6px -2px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Rétablissement horizontal pour les headers mois et semaine */
|
||||
#planningTable thead th.col-month,
|
||||
#planningTable tbody td.col-sticky-sem {
|
||||
writing-mode: horizontal-tb;
|
||||
@@ -535,15 +514,15 @@ td.col-laia-sub {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Panneaux bas & Légendes */
|
||||
/* =========================================================
|
||||
PANNEAUX BAS, LÉGENDES & RÉSUMÉS
|
||||
========================================================= */
|
||||
.fc-bottom-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Nouvelles bordures style "Budget" pour les conteneurs */
|
||||
.pf-card {
|
||||
background: white;
|
||||
border: 1px solid var(--pf-border);
|
||||
@@ -560,6 +539,7 @@ td.col-laia-sub {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Légendes statiques */
|
||||
.pf-legend-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -607,53 +587,62 @@ td.col-laia-sub {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fc-summary-header {
|
||||
/* Résumé Mensuel Injecté */
|
||||
.fc-month-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--pf-border);
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.fc-summ-select {
|
||||
.fc-month-summary-inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
background: var(--pf-bg-lighter, #f8fafc);
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--pf-border);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
height: auto;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.fc-summary-item {
|
||||
.fc-summ-pill {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 4px;
|
||||
border-bottom: 1px solid var(--pf-border); /* Ligne de séparation visible */
|
||||
}
|
||||
.fc-summary-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.fc-summary-label {
|
||||
font-size: 0.85rem;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
.fc-summary-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
.fc-summ-pill strong {
|
||||
font-size: 0.95rem;
|
||||
color: #0f172a;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.pf-presence-pill strong {
|
||||
color: var(--pf-primary);
|
||||
}
|
||||
|
||||
/* Menu Contextuel Tactile */
|
||||
[data-theme="dark"] .fc-month-summary-inline {
|
||||
background: #1c2128;
|
||||
border-color: #30363d;
|
||||
}
|
||||
[data-theme="dark"] .fc-summ-pill strong {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
MENU CONTEXTUEL (COMPACT & PROPRE)
|
||||
========================================================= */
|
||||
#selectionMenu,
|
||||
#fc-month-selectionMenu,
|
||||
.fc-selection-menu {
|
||||
position: absolute;
|
||||
z-index: 9000;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||
box-shadow: var(--pf-shadow-lg);
|
||||
border-radius: 16px;
|
||||
padding: 10px 12px;
|
||||
min-width: 220px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
min-width: 210px;
|
||||
display: none;
|
||||
animation: menuPopIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
transform-origin: top center;
|
||||
@@ -668,88 +657,70 @@ td.col-laia-sub {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fc-menu-section {
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dashed #cbd5e1;
|
||||
}
|
||||
.fc-menu-section:last-child {
|
||||
border: none;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.fc-menu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.fc-menu-section strong {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--pf-text-muted);
|
||||
}
|
||||
.fc-menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
}
|
||||
.fc-menu-btn {
|
||||
background: white;
|
||||
border: 1px solid var(--pf-border);
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
transition: 0.2s;
|
||||
box-shadow: var(--pf-shadow-sm);
|
||||
text-align: left;
|
||||
transition: all 0.2s ease;
|
||||
color: #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.fc-menu-btn:hover {
|
||||
background: var(--pf-bg-page);
|
||||
border-color: var(--pf-primary);
|
||||
color: var(--pf-primary);
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.fc-menu-clear-icon {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
|
||||
/* --- LE BOUTON ACTIF (SANS COCHE !) --- */
|
||||
.fc-menu-btn--active {
|
||||
border: 1px solid var(--pf-primary) !important;
|
||||
background: #eff6ff !important;
|
||||
color: var(--pf-primary) !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
.fc-menu-clear-icon svg {
|
||||
/* Sécurité absolue pour tuer les coches fantômes */
|
||||
.fc-menu-btn--active::before,
|
||||
.fc-menu-btn--active::after {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
/* Icônes intégrées dans le JS */
|
||||
.fc-icon-centre {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.fc-icon-avis {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.fc-menu-clear-icon:hover {
|
||||
background: #fef2f2;
|
||||
color: var(--pf-danger);
|
||||
}
|
||||
.fc-menu-leaves-table table {
|
||||
width: 100%;
|
||||
border-spacing: 2px;
|
||||
}
|
||||
.fc-menu-leaves-table th {
|
||||
font-size: 0.7rem;
|
||||
color: var(--pf-text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
.fc-th-inline {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
object-fit: contain;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* --- BOUTON DE MENU ACTIF (Élément déjà enregistré) --- */
|
||||
.fc-menu-btn--active {
|
||||
background: #eff6ff;
|
||||
border-color: var(--pf-primary);
|
||||
color: var(--pf-primary);
|
||||
/* Un léger inset shadow pour rendre la bordure un peu plus épaisse sans casser la taille */
|
||||
box-shadow: inset 0 0 0 1px var(--pf-primary);
|
||||
}
|
||||
|
||||
/* Table specifique modale vacances */
|
||||
/* =========================================================
|
||||
MODALES (Paramètres & Vacances)
|
||||
========================================================= */
|
||||
.fc-holidays-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -772,7 +743,6 @@ td.col-laia-sub {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* === MODALES : Alignement du Header et de la Croix === */
|
||||
.pf-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -781,14 +751,12 @@ td.col-laia-sub {
|
||||
border-bottom: 1px solid var(--pf-border);
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.pf-modal-header .pf-modal-title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-bottom: none; /* On annule la bordure par défaut du global.css pour la déléguer au header */
|
||||
border-bottom: none;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.pf-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -799,9 +767,37 @@ td.col-laia-sub {
|
||||
padding: 0 0 0 15px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.pf-modal-close:hover {
|
||||
color: #ef4444; /* Devient rouge au survol */
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.fc-sidebar-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 1.2rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
color: #0f172a;
|
||||
transition: all 0.2s;
|
||||
width: calc(100% - 16px);
|
||||
margin: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.fc-sidebar-tab.active {
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
}
|
||||
.fc-sidebar-tab:hover:not(.active) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
[data-theme="dark"] .fc-sidebar-tab.active {
|
||||
background: var(--pf-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
@@ -811,36 +807,25 @@ td.col-laia-sub {
|
||||
background: #ffffff;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
|
||||
font-family: inherit;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
|
||||
/* Padding standard */
|
||||
color: #0f172a;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-transform: capitalize;
|
||||
box-shadow: var(--pf-shadow-sm);
|
||||
}
|
||||
|
||||
.fc-smart-select:hover {
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.fc-smart-select:focus {
|
||||
background: #ffffff;
|
||||
border-color: var(--primary);
|
||||
border-color: var(--pf-primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
|
||||
/* --- BOUTON AUJOURD'HUI & CONTROLES --- */
|
||||
.fc-view-controls {
|
||||
align-items: center; /* Pour bien centrer le nouveau bouton */
|
||||
}
|
||||
|
||||
.fc-today-button {
|
||||
background: white;
|
||||
border: 1px solid var(--pf-border);
|
||||
@@ -853,12 +838,10 @@ td.col-laia-sub {
|
||||
transition: all 0.2s;
|
||||
box-shadow: var(--pf-shadow-sm);
|
||||
}
|
||||
|
||||
.fc-today-button:hover {
|
||||
background: #eff6ff;
|
||||
border-color: var(--pf-primary);
|
||||
}
|
||||
|
||||
.fc-view-divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
@@ -866,34 +849,14 @@ td.col-laia-sub {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* --- MISE EN ÉVIDENCE DU JOUR EN COURS --- */
|
||||
.fc-day--today {
|
||||
background: #eff6ff !important;
|
||||
box-shadow: inset 0 0 0 1px #bfdbfe !important;
|
||||
}
|
||||
|
||||
.fc-day--today .fc-day-number {
|
||||
display: inline;
|
||||
background: none;
|
||||
width: auto;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
color: var(--pf-primary);
|
||||
font-weight: 900;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
MOBILE RESPONSIVE
|
||||
========================================================= */
|
||||
@media (max-width: 768px) {
|
||||
.fc-smart-select {
|
||||
font-size: 1rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.pf-family-calendar .pf-container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
@@ -927,6 +890,9 @@ td.col-laia-sub {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#selectionMenu,
|
||||
#fc-month-selectionMenu,
|
||||
.fc-selection-menu {
|
||||
position: fixed !important;
|
||||
top: auto !important;
|
||||
@@ -939,7 +905,8 @@ td.col-laia-sub {
|
||||
animation: slideUpSheet 0.3s forwards;
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
.fc-selection-menu::before {
|
||||
#selectionMenu::before,
|
||||
#fc-month-selectionMenu::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 40px;
|
||||
@@ -955,39 +922,8 @@ td.col-laia-sub {
|
||||
gap: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
MODALE PARAMÈTRES (SIDEBAR STYLE)
|
||||
========================================================================== */
|
||||
.fc-sidebar-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 1.2rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
color: var(--text-main);
|
||||
transition: all 0.2s;
|
||||
width: calc(100% - 16px);
|
||||
margin: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.fc-sidebar-tab.active {
|
||||
background: #111827; /* Gris très foncé typique des sidebars */
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.fc-sidebar-tab:hover:not(.active) {
|
||||
background: var(--bg-soft, #f1f5f9);
|
||||
}
|
||||
|
||||
/* Variante Dark Mode native pour l'onglet actif */
|
||||
[data-theme="dark"] .fc-sidebar-tab.active {
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
@keyframes slideUpSheet {
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,3 +52,10 @@
|
||||
[2026-05-06T20:43:43+02:00] RAW INPUT: [{"date":"2026-05-06","type":"AVIS","duration":1,"person":"Carole"}]
|
||||
[2026-05-06T20:44:36+02:00] RAW INPUT: [{"date":"2026-05-06","type":"PEP_SICK","duration":1,"person":"Carole"}]
|
||||
[2026-05-20T16:40:17+02:00] RAW INPUT: [{"date":"2026-05-19","type":"OFF_CAROLE","duration":1,"person":"Carole"}]
|
||||
[2026-06-16T14:20:35+02:00] RAW INPUT: [{"date":"2026-07-29","type":"CARE_MODE","duration":1,"person":"Centre"}]
|
||||
[2026-06-16T14:20:43+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CARE_MODE","duration":1,"person":"Centre"}]
|
||||
[2026-06-16T14:22:51+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CHILD_SICK","duration":1,"person":"5"}]
|
||||
[2026-06-16T14:23:10+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CHILD_SICK","duration":1,"person":"5"}]
|
||||
[2026-06-16T14:23:33+02:00] RAW INPUT: [{"date":"2026-06-18","type":"CARE_MODE","duration":1,"person":"Nounou"}]
|
||||
[2026-06-16T14:23:54+02:00] RAW INPUT: [{"date":"2026-06-11","type":"HELPER_OFF","duration":1,"person":"1"}]
|
||||
[2026-06-16T14:23:57+02:00] RAW INPUT: [{"date":"2026-06-25","type":"CHILD_SICK","duration":1,"person":"4"}]
|
||||
|
||||
@@ -8,77 +8,98 @@ $action = $input['action'] ?? '';
|
||||
|
||||
if (!$action) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Action manquante.']);
|
||||
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action manquante.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// --- SUPPRESSION UNITAIRE ---
|
||||
if ($action === 'delete') {
|
||||
$eventId = (int)($input['event_id'] ?? 0);
|
||||
if ($eventId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'ID manquant.']);
|
||||
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'ID manquant.']);
|
||||
exit;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
|
||||
$stmt->execute([$eventId]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- MISE À JOUR UNITAIRE ---
|
||||
if ($action === 'update') {
|
||||
$eventId = (int)($input['event_id'] ?? 0);
|
||||
$newType = $input['new_type'] ?? '';
|
||||
if ($eventId <= 0 || !$newType) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Données manquantes.']);
|
||||
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Données manquantes.']);
|
||||
exit;
|
||||
}
|
||||
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
|
||||
$stmt->execute([$newType, $eventId]);
|
||||
echo json_encode(['status' => 'success']);
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'bulk_delete_day_types_person') {
|
||||
$dates = $input['dates'] ?? [];
|
||||
$types = $input['types'] ?? [];
|
||||
$person_id = $input['person_id'] ?? null;
|
||||
|
||||
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||
|
||||
if (empty($dates) || empty($types)) {
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
|
||||
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
|
||||
|
||||
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
|
||||
$params = array_merge($dates, $types);
|
||||
|
||||
// 🔥 LE CORRECTIF : On cherche le 0 en base de données
|
||||
if ($person_id === null || $person_id === '' || (int)$person_id === 0) {
|
||||
$sql .= " AND person_id = 0";
|
||||
} else {
|
||||
$sql .= " AND person_id = ?";
|
||||
$params[] = (int)$person_id;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- SUPPRESSION DE MASSE (Par date et type) ---
|
||||
// Utilisé quand on ajoute un événement pour nettoyer les doublons potentiels (ex: Off vs Extra)
|
||||
if ($action === 'bulk_delete_day_types') {
|
||||
$dates = $input['dates'] ?? [];
|
||||
$types = $input['types'] ?? [];
|
||||
$dates = array_filter($dates, function($d) {
|
||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
|
||||
});
|
||||
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||
|
||||
if (empty($dates) || empty($types)) {
|
||||
echo json_encode(['status' => 'success']);
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Création des placeholders IN (?,?,?)
|
||||
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
|
||||
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
|
||||
|
||||
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
// Fusion des tableaux pour l'exécution
|
||||
$stmt->execute(array_merge($dates, $types));
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- SUPPRESSION TOTALE SUR DES DATES ---
|
||||
if ($action === 'bulk_delete_all') {
|
||||
$dates = $input['dates'] ?? [];
|
||||
$dates = array_filter($dates, function($d) {
|
||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
|
||||
});
|
||||
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||
|
||||
if (empty($dates) || empty($types)) {
|
||||
echo json_encode(['status' => 'success']);
|
||||
if (empty($dates)) {
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -87,15 +108,15 @@ try {
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($dates);
|
||||
|
||||
echo json_encode(['status' => 'success']);
|
||||
echo json_encode(['success' => true, 'status' => 'success']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Action inconnue
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Action non reconnue : ' . $action]);
|
||||
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action non reconnue : ' . $action]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
echo json_encode(['success' => false, 'status' => 'error', 'message' => $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
@@ -1,33 +1,14 @@
|
||||
<?php
|
||||
// includes/api/save-events.php
|
||||
// modules/family-calendar/includes/api/save-events.php
|
||||
header('Content-Type: application/json');
|
||||
require __DIR__ . '/../../../../includes/db.php';
|
||||
|
||||
$rawInput = file_get_contents('php://input');
|
||||
$eventsToSave = json_decode($rawInput, true);
|
||||
|
||||
// Optionnel : tu peux commenter/supprimer ces logs en production pour économiser du disque
|
||||
file_put_contents(
|
||||
__DIR__ . '/events-debug.log',
|
||||
"[" . date('c') . "] RAW INPUT: " . $rawInput . PHP_EOL,
|
||||
FILE_APPEND
|
||||
);
|
||||
|
||||
if (empty($eventsToSave) || !is_array($eventsToSave)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Aucune donnée d\'événement reçue.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$inserted = [];
|
||||
require_once __DIR__ . '/../../../../includes/db.php';
|
||||
|
||||
try {
|
||||
// 1. OPTIMISATION : On récupère toutes les personnes d'un coup (Mapping)
|
||||
$stmtPeople = $pdo->query("SELECT id, name FROM pf_people");
|
||||
$peopleMap = [];
|
||||
while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
||||
// On crée un tableau associatif : ['Carole' => 1, 'Alex' => 2, etc.]
|
||||
$peopleMap[$row['name']] = $row['id'];
|
||||
$rawInput = file_get_contents('php://input');
|
||||
$eventsToSave = json_decode($rawInput, true);
|
||||
|
||||
if (empty($eventsToSave) || !is_array($eventsToSave)) {
|
||||
throw new Exception("Aucune donnée d'événement reçue.");
|
||||
}
|
||||
|
||||
$pdo->beginTransaction();
|
||||
@@ -36,26 +17,32 @@ try {
|
||||
VALUES (:event_date, :event_type, :person_id, :duration)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
|
||||
foreach ($eventsToSave as $event) {
|
||||
$person_id = null;
|
||||
$inserted = [];
|
||||
|
||||
// 2. On vérifie simplement dans notre tableau (plus de requête SQL ici !)
|
||||
if (!empty($event['person']) && isset($peopleMap[$event['person']])) {
|
||||
$person_id = $peopleMap[$event['person']];
|
||||
foreach ($eventsToSave as $event) {
|
||||
// 🔥 LE CORRECTIF : On initialise à 0 (car NOT NULL en base)
|
||||
$person_id = 0;
|
||||
|
||||
if (!empty($event['person_id']) && is_numeric($event['person_id'])) {
|
||||
$person_id = (int)$event['person_id'];
|
||||
} elseif (!empty($event['person']) && is_numeric($event['person'])) {
|
||||
$person_id = (int)$event['person'];
|
||||
}
|
||||
|
||||
$duration = isset($event['duration']) ? (float)$event['duration'] : 1.0;
|
||||
|
||||
$stmt->execute([
|
||||
':event_date' => $event['date'],
|
||||
':event_type' => $event['type'],
|
||||
':person_id' => $person_id,
|
||||
':duration' => $event['duration'] ?? 1.0,
|
||||
':duration' => $duration,
|
||||
]);
|
||||
|
||||
$inserted[] = [
|
||||
'id' => $pdo->lastInsertId(),
|
||||
'date' => $event['date'],
|
||||
'type' => $event['type'],
|
||||
'duration' => $event['duration'] ?? 1.0,
|
||||
'duration' => $duration,
|
||||
'person_id' => $person_id,
|
||||
];
|
||||
}
|
||||
@@ -63,12 +50,20 @@ try {
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'success',
|
||||
'inserted' => $inserted,
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'error', 'message' => 'Erreur lors de la sauvegarde : ' . $e->getMessage()]);
|
||||
}
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur SQL : ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
?>
|
||||
@@ -11,17 +11,18 @@ try {
|
||||
if ($action === 'get_all') {
|
||||
// A. Options globales du foyer
|
||||
$stmtFoyer = $pdo->query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1");
|
||||
$foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '["Nounou","Centre"]'];
|
||||
// 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
|
||||
$stmtPeople = $pdo->query("SELECT id, name, role FROM pf_people WHERE is_active = 1 ORDER BY role DESC, name ASC");
|
||||
// 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 FROM pf_person_leave_meta");
|
||||
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance FROM pf_person_leave_meta");
|
||||
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// On organise les congés par ID de personne pour faciliter le traitement côté JS
|
||||
$leavesMap = [];
|
||||
foreach ($leavesRaw as $leave) {
|
||||
$leavesMap[$leave['person_id']][] = [
|
||||
@@ -32,6 +33,21 @@ try {
|
||||
];
|
||||
}
|
||||
|
||||
// D. Paramètres dynamiques du calendrier (pf_settings)
|
||||
$stmtSettings = $pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'calendar'");
|
||||
$calendarSettingsRaw = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR); // Crée un tableau [key => value]
|
||||
|
||||
// Paramètres par défaut si la table est vide
|
||||
$calendarSettings = [
|
||||
'calendar_default_view' => $calendarSettingsRaw['calendar_default_view'] ?? 'month',
|
||||
'calendar_first_day' => $calendarSettingsRaw['calendar_first_day'] ?? '1',
|
||||
'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00'
|
||||
];
|
||||
|
||||
// E. NOUVEAU : Récupération du catalogue des types de congés de la famille
|
||||
$stmtLeaveTypes = $pdo->query("SELECT code, label, default_allowance, reset_month, allow_carry_over FROM pf_leave_types ORDER BY label ASC");
|
||||
$leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
@@ -40,7 +56,9 @@ try {
|
||||
'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true)
|
||||
],
|
||||
'people' => $people,
|
||||
'leaves' => $leavesMap
|
||||
'leaves' => $leavesMap,
|
||||
'calendar_settings' => $calendarSettings,
|
||||
'leave_types' => $leaveTypes // On envoie le catalogue au JS !
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
@@ -48,7 +66,7 @@ try {
|
||||
|
||||
// ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ───
|
||||
if ($action === 'save_foyer') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("Méthode non autorisée");
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||
|
||||
$zone = trim($_POST['zone_scolaire'] ?? 'C');
|
||||
$modesRaw = $_POST['care_modes'] ?? '[]';
|
||||
@@ -64,7 +82,7 @@ try {
|
||||
|
||||
// ─── 3. SAUVEGARDE DES CONGÉS D'UN MEMBRE (ONGLET 2) ───
|
||||
if ($action === 'save_member_leaves') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("Méthode non autorisée");
|
||||
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);
|
||||
@@ -73,19 +91,22 @@ try {
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// On nettoie les anciennes configurations de congés de cette personne
|
||||
$stmtDelete = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE person_id = ?");
|
||||
$stmtDelete->execute([$personId]);
|
||||
|
||||
// On réinsère la nouvelle matrice propre
|
||||
if (!empty($leavesData)) {
|
||||
$stmtInsert = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, method, allowance, anniversary_date) VALUES (?, ?, ?, ?, ?)");
|
||||
foreach ($leavesData as $leave) {
|
||||
$type = strtoupper(trim($leave['type']));
|
||||
$method = in_array($leave['method'], ['FIXED', 'ACCUMULATED']) ? $leave['method'] : 'FIXED';
|
||||
$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]);
|
||||
}
|
||||
@@ -97,6 +118,52 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// ─── 4. SAUVEGARDE DES PARAMÈTRES D'AFFICHAGE DU CALENDRIER (ONGLET 3) ───
|
||||
if ($action === 'save_calendar_settings') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||
|
||||
$allowed_keys = ['calendar_default_view', 'calendar_first_day', 'calendar_working_hours'];
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO pf_settings (setting_key, setting_value, module)
|
||||
VALUES (:key, :val, 'calendar')
|
||||
ON DUPLICATE KEY UPDATE setting_value = :val2
|
||||
");
|
||||
|
||||
$pdo->beginTransaction();
|
||||
foreach ($allowed_keys as $key) {
|
||||
if (isset($_POST[$key])) {
|
||||
$value = trim($_POST[$key]);
|
||||
$stmt->execute([
|
||||
'key' => $key,
|
||||
'val' => $value,
|
||||
'val2' => $value
|
||||
]);
|
||||
}
|
||||
}
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ─── 5. SAUVEGARDE DES MODES DE GARDE D'UN ENFANT ───
|
||||
if ($action === 'save_child_care_modes') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||
|
||||
$person_id = (int)($_POST['person_id'] ?? 0);
|
||||
$care_modes = $_POST['care_modes'] ?? '[]';
|
||||
|
||||
if ($person_id > 0) {
|
||||
$stmt = $pdo->prepare("UPDATE pf_people SET care_modes = ? WHERE id = ?");
|
||||
$stmt->execute([$care_modes, $person_id]);
|
||||
echo json_encode(['success' => true]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'ID de l\'enfant manquant.']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
throw new Exception("Action inconnue");
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
Reference in New Issue
Block a user