Files
HouseHub/modules/budget/views/epargne.php
T
2026-03-28 17:41:46 +01:00

455 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// modules/budget/views/epargne.php
$requestedOwner = $_GET['owner'] ?? 'Nens';
$ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOwner];
$cycleConfigs = [];
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
$parts = explode('-', $row['reference_id']);
if (count($parts) == 2) {
$mKey = $parts[1] . '-' . $parts[0] . '-01';
$cycleConfigs[$mKey] = json_decode($row['content'], true);
}
}
?>
<div class="budget-view">
<div class="view-header">
<div class="owner-tabs">
<a href="?tab=epargne&owner=Alex" class="owner-tab <?= $requestedOwner === 'Alex' ? 'active' : '' ?>">Alex</a>
<a href="?tab=epargne&owner=Laia" class="owner-tab <?= $requestedOwner === 'Laia' ? 'active' : '' ?>">Laia</a>
<a href="?tab=epargne&owner=Nens" class="owner-tab <?= $requestedOwner === 'Nens' ? 'active' : '' ?>">Nens 👶</a>
</div>
</div>
<?php foreach ($ownersToDisplay as $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);
$data = [];
$months = [];
$allCategories = [];
foreach ($rows as $row) {
$m = $row['month_date'];
$cat = $row['category'];
$val = $row['amount'];
$data[$m][$cat] = $val;
if (!in_array($m, $months)) $months[] = $m;
if ($cat !== 'TOTAL_BANQUE' && !in_array($cat, $allCategories)) $allCategories[] = $cat;
}
$months = array_slice($months, 0, 7);
sort($allCategories);
// Définition de la classe couleur selon le propriétaire
$ownerTextClass = '';
if ($currentOwner === 'Alex') $ownerTextClass = 'txt-alex';
elseif ($currentOwner === 'Laia') $ownerTextClass = 'txt-laia';
else $ownerTextClass = 'txt-global'; // Pour Pol et Pep (ou autre)
?>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
<div style="flex-grow: 1;">
<?php if ($requestedOwner === 'Nens'):
$themeClass = 'theme-' . strtolower($currentOwner);
?>
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
<?= $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">
🔁 +1 Mois
</button>
<?php endif; ?>
<button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn">
Saisir un mois
</button>
</div>
</div>
<div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0;">
<?php if (empty($months)): ?>
<div style="padding: 30px; text-align: center; color: #64748b;">
<p>Aucune donnée pour <?= 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;">
<thead>
<tr>
<th class="sticky-col" style="background:#f8fafc;">Poste / Mois</th>
<?php foreach ($months as $month): ?>
<th>
<div class="month-header-container" style="display: flex; flex-direction: column; align-items: center; gap: 6px;">
<div style="display:flex; flex-direction:column; text-align:center;">
<span class="month-name"><?= date('M Y', strtotime($month)) ?></span>
<?php
if (isset($cycleConfigs[$month]) && !empty($cycleConfigs[$month]['start_date'])) {
$cStart = date('d/m', strtotime($cycleConfigs[$month]['start_date']));
echo "<span style='font-size:0.75rem; font-weight:normal; color:#64748b;'>Dès le $cStart</span>";
}
?>
</div>
<div class="month-actions" style="justify-content: center; width: 100%;">
<button class="btn-icon-small" title="Modifier avec Modale"
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", JSON.parse(this.getAttribute("data-json")))'>
✏️
</button>
<button class="btn-icon-small" title="Supprimer tout le mois"
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
🗑️
</button>
</div>
</div>
</th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<tr class="row-total">
<td class="sticky-col"><strong>Total Banque</strong></td>
<?php foreach ($months as $month):
$val = $data[$month]['TOTAL_BANQUE'] ?? 0;
?>
<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 ?>"
style="width: 70px; font-weight:bold; color:#2563eb;"
value="<?= $val != 0 ? round($val) : '' ?>"
placeholder="0"
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= $currentOwner ?>', this)">
<span style="color:#2563eb; font-weight:bold; font-size:0.9rem;">€</span>
</div>
</td>
<?php endforeach; ?>
</tr>
<?php foreach ($allCategories as $cat): ?>
<tr>
<td class="sticky-col"><?= htmlspecialchars($cat) ?></td>
<?php foreach ($months as $month):
$amount = $data[$month][$cat] ?? 0;
?>
<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 ?>"
style="width: 70px;"
value="<?= $amount != 0 ? round($amount) : '' ?>"
placeholder="-"
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>', this)">
</div>
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
<tr class="row-extres">
<td class="sticky-col"><strong>Extra</strong></td>
<?php foreach ($months as $month):
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
$sum = 0;
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
$extra = $total - $sum;
?>
<td class="text-center font-bold" id="extra_<?= $currentOwner ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
<?= number_format($extra, 0, ',', ' ') ?> €
</td>
<?php endforeach; ?>
</tr>
</tbody>
</table>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<div id="savingsModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 600px; width: 95%;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 id="savingsModalTitle" class="pf-modal-title" style="margin:0;">Saisir un mois</h3>
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
</div>
<form action="/modules/budget/includes/api/save-savings.php" method="POST" id="savingsForm">
<input type="hidden" name="owner" id="sav_owner">
<input type="hidden" name="redirect_tab" id="redirect_tab" value="<?= htmlspecialchars($requestedOwner) ?>">
<input type="hidden" name="month_date" id="sav_date_hidden">
<div style="display:flex; gap:15px; margin-bottom:20px;">
<div class="form-group" style="flex:1; margin:0;">
<label class="pf-label">Mois concerné</label>
<input type="month" id="sav_month" required class="pf-input">
</div>
<div class="form-group" style="flex:1; margin:0;">
<label class="pf-label">Total en Banque (€)</label>
<input type="number" step="0.01" name="values[TOTAL_BANQUE]" id="sav_total" required class="pf-input no-spinners" style="font-weight:bold; color:#2563eb;">
</div>
</div>
<div class="separator" style="margin: 20px 0; border-bottom: 1px solid #e2e8f0;"></div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
<div>
<h4 style="margin:0; font-size:1rem; color:#1e293b;">Ventilation</h4>
<span style="font-size:0.8rem; color:#64748b;">Utilisez l'ajustement (+/-) pour recalculer automatiquement.</span>
</div>
<button type="button" class="pf-btn btn-secondary" onclick="addCustomEpargneLine()" style="padding:4px 10px; height:auto; width:auto; font-size:0.9rem;"> Ligne</button>
</div>
<div style="display:flex; gap:10px; padding:0 5px 5px 5px; font-size:0.8rem; color:#64748b; font-weight:600;">
<div style="flex:2;">Catégorie</div>
<div style="width:100px;">Actuel</div>
<div style="width:90px;">Ajust (+/-)</div>
<div style="width:100px;">Nouveau</div>
<div style="width:28px;"></div>
</div>
<div id="linesContainer" style="max-height: 350px; overflow-y: auto; padding-right:5px; display:flex; flex-direction:column; gap:10px;">
</div>
<div style="margin-top:25px; display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'" class="pf-btn btn-secondary" style="width:auto; margin:0;">Annuler</button>
<button type="submit" class="pf-btn" style="width:auto; margin:0;">Enregistrer</button>
</div>
</form>
</div>
</div>
<script>
// ============================================================================
// 1. GESTION DE L'ÉDITION INVISIBLE EN DIRECT (Input Classique)
// ============================================================================
function updateEpargneCell(month, category, owner, inputEl) {
const val = parseFloat(inputEl.value) || 0;
// 1. Sauvegarde silencieuse en Ajax
const formData = new FormData();
formData.append('action', 'update_single_entry');
formData.append('month_date', month);
formData.append('category', category);
formData.append('owner', owner);
formData.append('amount', val);
fetch('/modules/budget/includes/api/save-savings.php', {
method: 'POST',
body: formData
}).catch(err => alert("Erreur lors de la sauvegarde"));
// 2. Recalcul visuel de l'Extra instantané
const totalInput = document.querySelector(`.total-input-${owner}-${month}`);
const totalVal = parseFloat(totalInput.value) || 0;
let sumCats = 0;
document.querySelectorAll(`.cat-input-${owner}-${month}`).forEach(inp => {
sumCats += parseFloat(inp.value) || 0;
});
const extra = totalVal - sumCats;
const extraCell = document.getElementById(`extra_${owner}_${month}`);
if (extraCell) {
extraCell.innerText = Math.round(extra).toLocaleString('fr-FR') + ' €';
extraCell.style.color = extra >= 0 ? '#10b981' : '#ef4444';
}
}
// ============================================================================
// 2. MODALE D'ÉDITION CLASSIQUE / AJOUT
// ============================================================================
const cycleConfigs = <?= json_encode($cycleConfigs) ?>;
function addCustomEpargneLine(catName = '', amount = '') {
const container = document.getElementById('linesContainer');
const baseAmount = (amount !== '' && amount !== null) ? parseFloat(amount).toFixed(2) : '0.00';
const inputName = catName ? `values[${catName}]` : '';
const html = `
<div class="ventilation-line" style="display:flex; gap:10px; align-items:center; background:#f8fafc; padding:8px; border-radius:8px; border:1px solid #e2e8f0;">
<div style="flex:2;">
<input type="text" class="pf-input cat-name-input" value="${catName}" placeholder="Nom (ex: Vacances)" oninput="updateCustomFieldName(this)" style="padding:6px; font-size:0.9rem;" required>
</div>
<div style="width:100px;">
<input type="number" step="0.01" class="pf-input base-amount no-spinners" value="${baseAmount}" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; background:#fff;">
</div>
<div style="width:90px;">
<input type="number" step="0.01" class="pf-input adjustment-amount no-spinners" placeholder="+ / -" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; color:#f59e0b; font-weight:bold;">
</div>
<div style="width:100px;">
<input type="number" step="0.01" name="${inputName}" class="pf-input final-amount no-spinners" value="${baseAmount}" style="padding:6px; font-size:0.9rem; font-weight:bold; background:#e0f2fe; border-color:#bae6fd; color:#0369a1;" readonly>
</div>
<button type="button" onclick="this.parentElement.remove()" style="width:28px; height:28px; border:none; background:#fee2e2; color:#ef4444; border-radius:4px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-weight:bold;">&times;</button>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function updateCustomFieldName(inputElement) {
const line = inputElement.closest('.ventilation-line');
const finalInput = line.querySelector('.final-amount');
const newName = inputElement.value.trim();
if (newName) finalInput.name = `values[${newName}]`;
else finalInput.name = '';
}
function recalculateCustomLine(inputElement) {
const line = inputElement.closest('.ventilation-line');
const baseInput = line.querySelector('.base-amount');
const adjInput = line.querySelector('.adjustment-amount');
const finalInput = line.querySelector('.final-amount');
const base = parseFloat(baseInput.value) || 0;
const adj = parseFloat(adjInput.value) || 0;
finalInput.value = (base + adj).toFixed(2);
}
function editCustomSavingsMonth(monthDate, owner, rowData) {
document.getElementById('sav_owner').value = owner;
const ym = monthDate.substring(0, 7);
document.getElementById('sav_month').value = ym;
const dateObj = new Date(monthDate);
const monthName = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
document.getElementById('savingsModalTitle').innerText = "Modifier : " + monthName + " (" + owner + ")";
document.getElementById('sav_total').value = rowData['TOTAL_BANQUE'] || '';
const container = document.getElementById('linesContainer');
container.innerHTML = '';
for (const [cat, val] of Object.entries(rowData)) {
if (cat !== 'TOTAL_BANQUE') addCustomEpargneLine(cat, val);
}
if (container.children.length === 0) addCustomEpargneLine();
document.getElementById('savingsModal').style.display = 'flex';
}
function openCustomSavingsModal(owner) {
document.getElementById('sav_owner').value = owner;
document.getElementById('sav_month').value = '';
document.getElementById('sav_total').value = '';
document.getElementById('savingsModalTitle').innerText = "Saisir un mois (" + owner + ")";
const container = document.getElementById('linesContainer');
container.innerHTML = '';
addCustomEpargneLine();
document.getElementById('savingsModal').style.display = 'flex';
}
window.onclick = function(event) {
const modal = document.getElementById('savingsModal');
if (event.target == modal) modal.style.display = 'none';
}
const savingsForm = document.getElementById('savingsForm');
if (savingsForm) {
savingsForm.addEventListener('submit', function(e) {
e.preventDefault();
const ym = document.getElementById('sav_month').value;
if(ym) {
document.getElementById('sav_date_hidden').value = ym + '-01';
}
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.innerText;
submitBtn.innerText = "Enregistrement...";
submitBtn.disabled = true;
const formData = new FormData(this);
fetch(this.action, { method: 'POST', body: formData })
.then(response => response.text())
.then(text => { window.location.reload(); })
.catch(error => {
console.error("Erreur:", error);
alert("Une erreur technique est survenue.");
submitBtn.innerText = originalText;
submitBtn.disabled = false;
});
});
}
function deleteEntireMonth(monthDate, owner) {
if (!confirm(`Supprimer TOUT le mois de ${monthDate} pour ${owner} ?`)) return;
const formData = new FormData();
formData.append("action", "delete_month_global");
formData.append("month_date", monthDate);
formData.append("owner", owner);
fetch("/modules/budget/includes/api/save-savings.php", { method: "POST", body: formData })
.then(() => window.location.reload())
.catch(err => alert("Erreur lors de la suppression."));
}
function duplicateLastMonth(lastMonthDate, owner) {
let dateObj = new Date(lastMonthDate);
dateObj.setMonth(dateObj.getMonth() + 1);
let year = dateObj.getFullYear();
let month = String(dateObj.getMonth() + 1).padStart(2, '0');
let nextMonthStr = `${year}-${month}-01`;
const formatMonth = (d) => {
let str = new Date(d).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
return str.charAt(0).toUpperCase() + str.slice(1);
};
const sourceName = formatMonth(lastMonthDate);
const targetName = formatMonth(nextMonthStr);
let defaultTotal = "";
if (cycleConfigs[nextMonthStr] && cycleConfigs[nextMonthStr].start_balance !== undefined) {
defaultTotal = cycleConfigs[nextMonthStr].start_balance;
}
const message = `Voulez-vous copier les données de ${sourceName} vers ${targetName} ?\n\nSaisissez le nouveau TOTAL en banque (€) pour ${targetName} :`;
let newTotal = prompt(message, defaultTotal);
if (newTotal !== null && newTotal.trim() !== "") {
const formData = new FormData();
formData.append("action", "duplicate_month");
formData.append("source_date", lastMonthDate);
formData.append("target_date", nextMonthStr);
formData.append("new_total", newTotal);
formData.append("owner", owner);
fetch("/modules/budget/includes/api/save-savings.php", { method: "POST", body: formData })
.then(async r => {
const text = await r.text(); // On lit en texte brut d'abord
try {
const d = JSON.parse(text); // On essaie de parser en JSON
if (d.success) window.location.reload();
else alert("Erreur serveur : " + (d.error || "Inconnue"));
} catch(e) {
// Si ça plante (HTML, redirection, erreur PHP masquée), on force le rechargement
console.log("Avertissement : La réponse serveur n'était pas du JSON. Rechargement...");
window.location.reload();
}
})
.catch(err => {
console.error(err);
alert("Erreur réseau lors de la duplication.");
});
}
}
</script>