This commit is contained in:
2026-04-13 11:06:08 +02:00
parent c4b80c3650
commit feb2f158dd
19 changed files with 1738 additions and 917 deletions
+109 -104
View File
@@ -36,7 +36,6 @@ $nextMonthLink = date('Y-m-01', strtotime("+1 month", $focusTs));
$cycleConfigs = [];
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
// Convertit "03-2026" en "2026-03-01" (Notre Tiroir)
$parts = explode('-', $row['reference_id']);
if (count($parts) == 2) {
$mKey = $parts[1] . '-' . $parts[0] . '-01';
@@ -81,25 +80,32 @@ $currentNote = $stmtNote->fetchColumn();
// 7. Récupération des vacances actives
$activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN ('draft', 'planned', 'booked') ORDER BY start_date ASC")->fetchAll(PDO::FETCH_ASSOC);
// Helper function pour les mois
function getTranslatedMonthName($dateString) {
$m = date('m', strtotime($dateString));
$y = date('Y', strtotime($dateString));
return tr('month_' . $m) . ' ' . $y;
}
?>
<div class="prev-container">
<div>
<div class="prev-section-header">
<h2>Revenus <?= $currentYear ?></h2>
<h2><?= tr('bud_prev_incomes') ?> <?= $currentYear ?></h2>
</div>
<table class="prev-salary-table">
<thead>
<tr>
<th>Personne</th>
<th>Salaire</th>
<th>Mensualité<br><small style="font-weight:normal; text-transform:none;">(Cpt Commun)</small></th>
<th>Frais Func.</th>
<th>Eco Perso</th>
<th style="background:#f0f9ff; color:#0284c7;">Eco Family<br><small style="font-weight:normal; text-transform:none;">(Dispo)</small></th>
<th>Restant Perso</th>
<th><?= tr('bud_prev_person') ?></th>
<th><?= tr('bud_prev_salary') ?></th>
<th><?= tr('bud_prev_monthly_payment') ?><br><small style="font-weight:normal; text-transform:none;">(<?= tr('bud_prev_joint_account') ?>)</small></th>
<th><?= tr('bud_prev_func_expenses') ?></th>
<th><?= tr('bud_prev_perso_savings') ?></th>
<th style="background:#f0f9ff; color:#0284c7;"><?= tr('bud_prev_family_savings') ?><br><small style="font-weight:normal; text-transform:none;">(<?= tr('bud_prev_available') ?>)</small></th>
<th><?= tr('bud_prev_perso_remaining') ?></th>
</tr>
</thead>
<tbody>
@@ -124,8 +130,6 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
@@ -133,20 +137,19 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<div>
<div class="prev-section-header">
<div style="display:flex; gap:10px; align-items:center;">
<h2>Répartition Budgétaire</h2>
<h2><?= tr('bud_prev_budget_alloc') ?></h2>
<div class="nav-group">
<a href="?tab=budget_prev&focus_date=<?= $prevMonthLink ?>" class="btn-nav">◀</a>
<a href="?tab=budget_prev&focus_date=<?= date('Y-m-01') ?>" class="btn-nav">Auj.</a>
<a href="?tab=budget_prev&focus_date=<?= date('Y-m-01') ?>" class="btn-nav"><?= tr('bud_prev_today') ?></a>
<a href="?tab=budget_prev&focus_date=<?= $nextMonthLink ?>" class="btn-nav">▶</a>
</div>
</div>
<div style="display:flex; gap:10px;">
<button class="pf-btn btn-secondary" onclick="duplicateMonth()">
🔁 +1 Mois
🔁 <?= tr('bud_sav_add_one_month') ?>
</button>
<button class="pf-btn" onclick="document.getElementById('addCatModal').style.display='flex'">
Nouvelle Ligne
<button class="pf-btn" onclick="document.getElementById('addCatModal').style.display='flex'; document.body.classList.add('no-scroll');">
<?= tr('bud_prev_new_line') ?>
</button>
</div>
</div>
@@ -161,21 +164,18 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
$cls = $isCurrent ? 'current' : '';
?>
<th colspan="3" class="th-month <?= $cls ?>">
<?= date('F Y', strtotime($month)) ?>
<span style="text-transform:capitalize;"><?= getTranslatedMonthName($month) ?></span>
<?php
// AFFICHAGE DU CYCLE CONFIGURÉ
if (isset($cycleConfigs[$month]) && !empty($cycleConfigs[$month]['start_date'])) {
$cStart = date('d/m', strtotime($cycleConfigs[$month]['start_date']));
echo "<div style='font-size:0.75rem; font-weight:normal; color:#64748b; margin-top:2px;'>Dès le $cStart</div>";
echo "<div style='font-size:0.75rem; font-weight:normal; color:#64748b; margin-top:2px;'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</div>";
}
?>
</th>
<?php endforeach; ?>
</tr>
<tr>
<th class="col-sticky header-cell">
</th>
<th class="col-sticky header-cell"></th>
<?php foreach ($months as $month): ?>
<th class="th-sub">Global</th>
<th class="th-sub txt-alex">Alex</th>
@@ -194,7 +194,7 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</tr>
<tr class="row-restant">
<td class="col-sticky" style="text-align:right !important;">Restant (Eco Family)</td>
<td class="col-sticky" style="text-align:right !important;"><?= tr('bud_prev_remaining') ?> (Eco Family)</td>
<?php foreach ($months as $m): ?>
<td style="border-left:2px solid #cbd5e1; background:#e2e8f0;">-</td>
<td class="val-ok sum-target" id="restant_alloc_alex_<?= $m ?>">0</td>
@@ -222,7 +222,7 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<div class="row-actions">
<button type="button"
class="btn-icon-action edit"
title="Modifier"
title="<?= tr('edit') ?>"
data-id="<?= $cat['id'] ?>"
data-name="<?= htmlspecialchars($cat['name']) ?>"
data-target="<?= htmlspecialchars($cat['target']) ?>"
@@ -232,8 +232,8 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</button>
<a href="?tab=budget_prev&id=<?= $cat['id'] ?>&action=delete_category"
onclick="return confirm('Supprimer cette ligne et tout son historique ?')"
class="btn-icon-action delete" title="Supprimer">
onclick="return confirm('<?= tr('bud_prev_confirm_del_line') ?>')"
class="btn-icon-action delete" title="<?= tr('delete') ?>">
&times;
</a>
</div>
@@ -269,34 +269,32 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</div>
<div style="margin: 20px 0; background: white; padding: 20px; border-radius: 12px; box-shadow: var(--shadow-sm); border: 1px solid #e2e8f0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3 style="margin: 0; font-size: 1.1rem; color: #1e293b;">
📝 Notes pour <?= date('F Y', strtotime($focusDate)) ?>
</h3>
<span id="note-save-indicator" style="font-size:0.85rem; color:#10b981; font-weight:bold; opacity:0; transition:opacity 0.3s;">
✓ Enregistré
</span>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3 style="margin: 0; font-size: 1.1rem; color: #1e293b;">
📝 <?= tr('bud_prev_notes_for') ?> <span style="text-transform:capitalize;"><?= getTranslatedMonthName($focusDate) ?></span>
</h3>
<span id="note-save-indicator" style="font-size:0.85rem; color:#10b981; font-weight:bold; opacity:0; transition:opacity 0.3s;">
✓ <?= tr('bud_prev_saved') ?>
</span>
</div>
<textarea
id="monthNoteArea"
class="pf-input"
rows="3"
placeholder="<?= tr('bud_prev_notes_ph') ?>"
style="width: 100%; resize: vertical; border-color: #cbd5e1; background: #f8fafc; margin-bottom: 10px;"
><?= htmlspecialchars((string)$currentNote) ?></textarea>
<div style="text-align: right;">
<button type="button" class="pf-btn" style="width: auto; display: inline-flex;"
onclick="saveGenericNote('budget_prev', '<?= $focusDate ?>', document.getElementById('monthNoteArea').value)">
<?= tr('bud_prev_save_note') ?>
</button>
</div>
</div>
<textarea
id="monthNoteArea"
class="pf-input"
rows="3"
placeholder="Écrivez vos remarques, rappels ou commentaires pour ce mois ici..."
style="width: 100%; resize: vertical; border-color: #cbd5e1; background: #f8fafc; margin-bottom: 10px;"
><?= htmlspecialchars((string)$currentNote) ?></textarea>
<div style="text-align: right;">
<button type="button" class="pf-btn" style="width: auto; display: inline-flex;"
onclick="saveGenericNote('budget_prev', '<?= $focusDate ?>', document.getElementById('monthNoteArea').value)">
Enregistrer la note
</button>
</div>
</div>
<?php
<?php
$focusMonth = $months[0];
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
@@ -335,24 +333,24 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<div class="recap-wrapper">
<div class="recap-card">
<div class="recap-header">
Virements à effectuer - <?= date('F Y', strtotime($focusMonth)) ?>
<?= tr('bud_prev_transfers_to_make') ?> - <span style="text-transform:capitalize;"><?= getTranslatedMonthName($focusMonth) ?></span>
</div>
<table class="recap-table">
<thead>
<tr>
<th style="text-align:left;">Destination</th>
<th style="text-align:left;"><?= tr('bud_prev_destination') ?></th>
<th class="col-alex">
<div style="display:flex; flex-direction:column; align-items:center; gap:5px;">
<span>ALEX</span>
<?php if($isValidatedAlex): ?>
<div style="background:#10b981; color:white; padding:4px 8px; border-radius:4px; font-size:0.7rem; display:flex; align-items:center; gap:4px;">
FAIT
<?= tr('bud_prev_done') ?>
</div>
<?php else: ?>
<button onclick="validateTransfers('Alex', '<?= $focusMonth ?>')" class="pf-btn btn-small" style="background:white; color:#0891b2; border:1px solid #0891b2; font-size:0.7rem; padding:2px 8px; height:auto;">
Valider
<?= tr('bud_prev_validate') ?>
</button>
<?php endif; ?>
</div>
@@ -363,11 +361,11 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<span>LAIA</span>
<?php if($isValidatedLaia): ?>
<div style="background:#10b981; color:white; padding:4px 8px; border-radius:4px; font-size:0.7rem; display:flex; align-items:center; gap:4px;">
FAIT
<?= tr('bud_prev_done') ?>
</div>
<?php else: ?>
<button onclick="validateTransfers('Laia', '<?= $focusMonth ?>')" class="pf-btn btn-small" style="background:white; color:#d97706; border:1px solid #d97706; font-size:0.7rem; padding:2px 8px; height:auto;">
Valider
<?= tr('bud_prev_validate') ?>
</button>
<?php endif; ?>
</div>
@@ -390,7 +388,7 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</tbody>
<tfoot>
<tr class="row-grand-total">
<td>GRAND TOTAL</td>
<td><?= tr('bud_prev_grand_total') ?></td>
<td class="col-alex sum-target" id="grand_total_alex">0 €</td>
<td class="col-laia sum-target" id="grand_total_laia">0 €</td>
<td class="col-global sum-target" id="grand_total_global">0 €</td>
@@ -399,25 +397,24 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</table>
</div>
</div>
</div>
<div id="addCatModal" 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:400px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;">Nouvelle Ligne de Budget</h3>
<button onclick="document.getElementById('addCatModal').style.display='none'" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">&times;</button>
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;"><?= tr('bud_prev_new_line_title') ?></h3>
<button onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">&times;</button>
</div>
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
<input type="hidden" name="action" value="add_category">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Nom (ex: Vacances)</label>
<label class="pf-label"><?= tr('bud_prev_label_name') ?></label>
<input type="text" name="name" class="pf-input" required>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Cible (Destination)</label>
<label class="pf-label"><?= tr('bud_prev_label_target') ?></label>
<select name="target" class="pf-input" required>
<option value="" disabled selected>-- Choisir --</option>
<option value="" disabled selected>-- <?= tr('bud_prev_choose') ?> --</option>
<option value="vers L.Pol">vers L.Pol</option>
<option value="vers L.Pep">vers L.Pep</option>
<option value="vers L.Perso">vers L.Perso</option>
@@ -425,17 +422,17 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</select>
</div>
<div class="form-group" style="margin-bottom:20px;">
<label class="pf-label" style="color:#8b5cf6;">🌴 Lier à un voyage (Optionnel)</label>
<label class="pf-label" style="color:#8b5cf6;">🌴 <?= tr('bud_prev_link_holiday') ?></label>
<select name="holiday_id" class="pf-input" style="border-color:#8b5cf6; background:#f5f3ff;">
<option value="">-- Ne pas associer --</option>
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
<?php foreach ($activeHolidays as $hol): ?>
<option value="<?= $hol['id'] ?>"><?= htmlspecialchars($hol['title']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="modal-footer">
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'" class="pf-btn btn-secondary">Annuler</button>
<button type="submit" class="pf-btn">Ajouter</button>
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('bud_add_title') ?></button>
</div>
</form>
</div>
@@ -444,19 +441,19 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<div id="editCatModal" class="pf-modal">
<div class="pf-modal-content" style="max-width:400px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;">Modifier la ligne</h3>
<button onclick="document.getElementById('editCatModal').style.display='none'" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">&times;</button>
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;"><?= tr('bud_prev_edit_line_title') ?></h3>
<button onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">&times;</button>
</div>
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
<input type="hidden" name="action" value="update_category">
<input type="hidden" name="cat_id" id="edit_cat_id">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Nom</label>
<label class="pf-label"><?= tr('bud_label_name') ?></label>
<input type="text" name="name" id="edit_cat_name" class="pf-input" required>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Cible (Destination)</label>
<label class="pf-label"><?= tr('bud_prev_label_target') ?></label>
<select name="target" id="edit_cat_target" class="pf-input" required>
<option value="vers L.Pol">vers L.Pol</option>
<option value="vers L.Pep">vers L.Pep</option>
@@ -465,17 +462,17 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</select>
</div>
<div class="form-group" style="margin-bottom:20px;">
<label class="pf-label" style="color:#8b5cf6;">🌴 Lier à un voyage (Optionnel)</label>
<label class="pf-label" style="color:#8b5cf6;">🌴 <?= tr('bud_prev_link_holiday') ?></label>
<select name="holiday_id" id="edit_cat_holiday" class="pf-input" style="border-color:#8b5cf6; background:#f5f3ff;">
<option value="">-- Ne pas associer --</option>
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
<?php foreach ($activeHolidays as $hol): ?>
<option value="<?= $hol['id'] ?>"><?= htmlspecialchars($hol['title']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="modal-footer">
<button type="button" onclick="document.getElementById('editCatModal').style.display='none'" class="pf-btn btn-secondary">Annuler</button>
<button type="submit" class="pf-btn">Enregistrer</button>
<button type="button" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
@@ -564,15 +561,28 @@ body.sum-mode-active .sum-target {
}
</style>
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()" title="Activer l'addition rapide">
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()" title="<?= tr('bud_sav_sum_mode_title') ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="8" y1="12" x2="16" y2="12"></line><line x1="12" y1="8" x2="12" y2="16"></line></svg>
</button>
<div id="sumResultBar" class="pf-sum-bar">
<span class="pf-sum-label">lection :</span>
<span class="pf-sum-label"><?= tr('bud_sav_selection') ?></span>
<span id="sumResultValue" class="pf-sum-value">0 €</span>
<button onclick="toggleSumMode()" class="pf-sum-close" title="Fermer">&times;</button>
<button onclick="toggleSumMode()" class="pf-sum-close" title="<?= tr('btn_close') ?>">&times;</button>
</div>
<script>
window.I18N = {
...window.I18N,
'bud_prev_err_no_history': "<?= tr('bud_prev_err_no_history') ?>",
'bud_prev_confirm_copy': "<?= tr('bud_prev_confirm_copy') ?>",
'bud_prev_confirm_transfers': "<?= tr('bud_prev_confirm_transfers') ?>",
'bud_err_tech': "<?= tr('bud_err_tech') ?>"
};
const currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
</script>
<script>
function openEditModal(btn) {
const id = btn.getAttribute('data-id');
@@ -586,6 +596,7 @@ function openEditModal(btn) {
document.getElementById('edit_cat_holiday').value = holiday;
document.getElementById('editCatModal').style.display = 'flex';
document.body.classList.add('no-scroll');
}
const currentYear = <?= $currentYear ?>;
@@ -600,7 +611,7 @@ function updateSalary(person, input) {
const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0;
const restant = salary - (mens + frais + ecoP + ecoF);
document.getElementById('restant_' + person).innerText = Math.round(restant).toLocaleString('fr-FR') + ' €';
document.getElementById('restant_' + person).innerText = Math.round(restant).toLocaleString(currentLang) + ' €';
saveData('update_salary_config', { year: currentYear, person: person, field: input.dataset.field, value: input.value });
recalcAllAllocations();
@@ -611,29 +622,24 @@ function updateAlloc(month, catId, personField, input) {
recalcAllAllocations();
}
function getMonthName(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('fr-FR', { month: 'long' }).toUpperCase();
}
function duplicateMonth() {
const targetDateStr = months[0];
const sourceDateStr = months[1];
if (!sourceDateStr) {
alert("Impossible : pas d'historique disponible pour copier.");
alert(tr("bud_prev_err_no_history"));
return;
}
const formatMonth = (d) => {
let str = new Date(d).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
let str = new Date(d).toLocaleDateString(currentLang, { month: 'long', year: 'numeric' });
return str.charAt(0).toUpperCase() + str.slice(1);
};
const sourceName = formatMonth(sourceDateStr);
const targetName = formatMonth(targetDateStr);
const message = `Voulez-vous copier les données de ${sourceName} vers ${targetName} ?\n\n⚠️ Cela écrasera toutes les valeurs déjà présentes pour ${targetName}.`;
const message = tr("bud_prev_confirm_copy").replace('%s', sourceName).replace('%t', targetName);
if(!confirm(message)) return;
@@ -699,13 +705,13 @@ function recalcAllAllocations() {
const elRestLaia = document.getElementById('restant_alloc_laia_' + m);
elRestAlex.innerText = Math.round(restAlex) + ' €';
elRestAlex.className = 'val-' + (restAlex >= 0 ? 'ok' : 'ko') + ' sum-target'; // On maintient la classe sum-target !
elRestAlex.className = 'val-' + (restAlex >= 0 ? 'ok' : 'ko') + ' sum-target';
elRestLaia.innerText = Math.round(restLaia) + ' €';
elRestLaia.className = 'val-' + (restLaia >= 0 ? 'ok' : 'ko') + ' sum-target';
});
updateSummaryTable();
if(isSumModeActive) updateSumResult(); // Force recalcul si la calculatrice est ouverte
if(isSumModeActive) updateSumResult();
}
function updateSummaryTable() {
@@ -739,18 +745,18 @@ function updateSummaryTable() {
const laiaSum = dataByTarget[targetName] ? dataByTarget[targetName].laia : 0;
const globalSum = alexSum + laiaSum;
row.cells[1].innerText = Math.round(alexSum).toLocaleString('fr-FR') + ' €';
row.cells[2].innerText = Math.round(laiaSum).toLocaleString('fr-FR') + ' €';
row.cells[3].innerText = Math.round(globalSum).toLocaleString('fr-FR') + ' €';
row.cells[1].innerText = Math.round(alexSum).toLocaleString(currentLang) + ' €';
row.cells[2].innerText = Math.round(laiaSum).toLocaleString(currentLang) + ' €';
row.cells[3].innerText = Math.round(globalSum).toLocaleString(currentLang) + ' €';
grandTotalAlex += alexSum;
grandTotalLaia += laiaSum;
});
}
document.getElementById('grand_total_alex').innerText = Math.round(grandTotalAlex).toLocaleString('fr-FR') + ' €';
document.getElementById('grand_total_laia').innerText = Math.round(grandTotalLaia).toLocaleString('fr-FR') + ' €';
document.getElementById('grand_total_global').innerText = Math.round(grandTotalAlex + grandTotalLaia).toLocaleString('fr-FR') + ' €';
document.getElementById('grand_total_alex').innerText = Math.round(grandTotalAlex).toLocaleString(currentLang) + ' €';
document.getElementById('grand_total_laia').innerText = Math.round(grandTotalLaia).toLocaleString(currentLang) + ' €';
document.getElementById('grand_total_global').innerText = Math.round(grandTotalAlex + grandTotalLaia).toLocaleString(currentLang) + ' €';
}
function saveData(action, data) {
@@ -761,7 +767,8 @@ function saveData(action, data) {
}
function validateTransfers(person, month) {
if (!confirm(`Confirmer que ${person} a bien effectué tous ses virements pour ${month} ?\n\nCela mettra à jour l'Épargne automatiquement.`)) return;
const msg = tr("bud_prev_confirm_transfers").replace('%p', person).replace('%m', month);
if (!confirm(msg)) return;
const formData = new FormData();
formData.append('action', 'validate_transfers');
@@ -777,7 +784,7 @@ function validateTransfers(person, month) {
if(data.success) window.location.reload();
else alert("Erreur: " + data.error);
})
.catch(e => alert("Erreur technique"));
.catch(e => alert(tr("bud_err_tech")));
}
function saveGenericNote(noteType, refId, content) {
@@ -845,7 +852,6 @@ function toggleSumMode() {
function extractNumberFromText(text) {
if (!text) return 0;
// Remplace les virgules par des points et supprime tout ce qui n'est pas chiffre, point ou signe moins
const cleanText = text.replace(',', '.').replace(/[^\d.-]/g, '');
return parseFloat(cleanText) || 0;
}
@@ -862,13 +868,12 @@ function updateSumResult() {
total += val;
});
document.getElementById('sumResultValue').innerText = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total);
document.getElementById('sumResultValue').innerText = new Intl.NumberFormat(currentLang, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total);
}
document.addEventListener('click', function(e) {
if (!isSumModeActive) return;
// Cible les inputs type number OU les cellules (td/th/div) ayant la classe .sum-target
const targetElement = e.target.closest('input[type="number"], .sum-target');
if (targetElement) {
@@ -884,7 +889,7 @@ document.addEventListener('click', function(e) {
updateSumResult();
}
}, true); // Le 'true' intercepte le clic avant le focus normal
}, true);
document.addEventListener('DOMContentLoaded', recalcAllAllocations);
</script>
+79 -47
View File
@@ -13,6 +13,13 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
$cycleConfigs[$mKey] = json_decode($row['content'], true);
}
}
// Récupération sécurisée du nom des mois (utilise les clés globales existantes)
function getMonthName($dateString) {
$m = date('m', strtotime($dateString));
$y = date('Y', strtotime($dateString));
return tr('month_' . $m) . ' ' . $y;
}
?>
<div class="budget-view">
@@ -48,7 +55,7 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
$ownerTextClass = '';
if ($currentOwner === 'Alex') $ownerTextClass = 'txt-alex';
elseif ($currentOwner === 'Laia') $ownerTextClass = 'txt-laia';
else $ownerTextClass = 'txt-global'; // Pour Pol et Pep (ou autre)
else $ownerTextClass = 'txt-global'; // Pour Pol et Pep
?>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
@@ -65,11 +72,11 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<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
🔁 <?= tr('bud_sav_add_one_month') ?>
</button>
<?php endif; ?>
<button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn">
Saisir un mois
<?= tr('bud_sav_add_month') ?>
</button>
</div>
</div>
@@ -77,32 +84,32 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<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>
<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;">
<thead>
<tr>
<th class="sticky-col" style="background:#f8fafc;">Poste / Mois</th>
<th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></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>
<span class="month-name" style="text-transform:capitalize;"><?= getMonthName($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>";
echo "<span style='font-size:0.75rem; font-weight:normal; color:#64748b;'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</span>";
}
?>
</div>
<div class="month-actions" style="justify-content: center; width: 100%;">
<button class="btn-icon-small" title="Modifier avec Modale"
<button class="btn-icon-small" 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")))'>
✏️
</button>
<button class="btn-icon-small" title="Supprimer tout le mois"
<button class="btn-icon-small" title="<?= tr('bud_sav_delete_month') ?>"
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
🗑️
@@ -115,7 +122,7 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
</thead>
<tbody>
<tr class="row-total">
<td class="sticky-col"><strong>Total Banque</strong></td>
<td class="sticky-col"><strong><?= tr('bud_sav_total_bank') ?></strong></td>
<?php foreach ($months as $month):
$val = $data[$month]['TOTAL_BANQUE'] ?? 0;
?>
@@ -154,7 +161,7 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<?php endforeach; ?>
<tr class="row-extres">
<td class="sticky-col"><strong>Extra</strong></td>
<td class="sticky-col"><strong><?= tr('bud_sav_extra') ?></strong></td>
<?php foreach ($months as $month):
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
$sum = 0;
@@ -176,8 +183,8 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<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>
<h3 id="savingsModalTitle" class="pf-modal-title" style="margin:0;"><?= tr('bud_sav_modal_title_add') ?></h3>
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'; document.body.classList.remove('no-scroll');" 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">
@@ -187,12 +194,12 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<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>
<label class="pf-label"><?= tr('bud_sav_month_concerned') ?></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>
<label class="pf-label"><?= tr('bud_sav_total_bank_eur') ?></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>
@@ -201,17 +208,17 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<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>
<h4 style="margin:0; font-size:1rem; color:#1e293b;"><?= tr('bud_sav_ventilation') ?></h4>
<span style="font-size:0.8rem; color:#64748b;"><?= tr('bud_sav_adj_help') ?></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>
<button type="button" class="pf-btn btn-secondary" onclick="addCustomEpargneLine()" style="padding:4px 10px; height:auto; width:auto; font-size:0.9rem;"> <?= tr('bud_sav_add_line') ?></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="flex:2;"><?= tr('bud_category') ?></div>
<div style="width:100px;"><?= tr('bud_sav_current') ?></div>
<div style="width:90px;"><?= tr('bud_sav_adjust') ?></div>
<div style="width:100px;"><?= tr('bud_sav_new') ?></div>
<div style="width:28px;"></div>
</div>
@@ -219,22 +226,41 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
</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>
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary" style="width:auto; margin:0;"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn" style="width:auto; margin:0;"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
</div>
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()" title="Activer l'addition rapide">
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()" title="<?= tr('bud_sav_sum_mode_title') ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="8" y1="12" x2="16" y2="12"></line><line x1="12" y1="8" x2="12" y2="16"></line></svg>
</button>
<div id="sumResultBar" class="pf-sum-bar">
<span class="pf-sum-label">lection :</span>
<span class="pf-sum-label"><?= tr('bud_sav_selection') ?></span>
<span id="sumResultValue" class="pf-sum-value">0 €</span>
<button onclick="toggleSumMode()" class="pf-sum-close" title="Fermer">&times;</button>
<button onclick="toggleSumMode()" class="pf-sum-close" title="<?= tr('btn_close') ?>">&times;</button>
</div>
<script>
window.I18N = {
...window.I18N,
'bud_sav_ph_name' : "<?= tr('bud_sav_ph_name') ?>",
'bud_sav_modal_title_edit' : "<?= tr('bud_sav_modal_title_edit') ?>",
'bud_sav_modal_title_add' : "<?= tr('bud_sav_modal_title_add') ?>",
'bud_sav_saving' : "<?= tr('bud_sav_saving') ?>",
'bud_err_tech' : "<?= tr('bud_err_tech') ?>",
'bud_sav_confirm_delete_month' : "<?= tr('bud_sav_confirm_delete_month') ?>",
'bud_sav_prompt_duplicate' : "<?= tr('bud_sav_prompt_duplicate') ?>",
'bud_err_server' : "<?= tr('bud_err_server') ?>",
'bud_err_network_dup' : "<?= tr('bud_err_network_dup') ?>"
};
// Détection de la langue pour le JS
const currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
</script>
<script>
// ============================================================================
// 1. GESTION DE L'ÉDITION INVISIBLE EN DIRECT (Input Classique)
@@ -243,7 +269,6 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
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);
@@ -254,9 +279,8 @@ function updateEpargneCell(month, category, owner, inputEl) {
fetch('/modules/budget/includes/api/save-savings.php', {
method: 'POST',
body: formData
}).catch(err => alert("Erreur lors de la sauvegarde"));
}).catch(err => alert(tr("bud_err_tech")));
// 2. Recalcul visuel de l'Extra instantané
const totalInput = document.querySelector(`.total-input-${owner}-${month}`);
const totalVal = parseFloat(totalInput.value) || 0;
@@ -269,11 +293,10 @@ function updateEpargneCell(month, category, owner, inputEl) {
const extraCell = document.getElementById(`extra_${owner}_${month}`);
if (extraCell) {
extraCell.innerText = Math.round(extra).toLocaleString('fr-FR') + ' €';
extraCell.innerText = Math.round(extra).toLocaleString(currentLang) + ' €';
extraCell.style.color = extra >= 0 ? '#10b981' : '#ef4444';
}
// Si la calculatrice est active, on met à jour son résultat aussi
if(isSumModeActive) updateSumResult();
}
@@ -290,7 +313,7 @@ function addCustomEpargneLine(catName = '', amount = '') {
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>
<input type="text" class="pf-input cat-name-input" value="${catName}" placeholder="${tr("bud_sav_ph_name")}" oninput="updateCustomFieldName(this)" style="padding:6px; font-size:0.9rem;" required>
</div>
<div style="width:100px;">
@@ -337,8 +360,8 @@ function editCustomSavingsMonth(monthDate, owner, rowData) {
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 + ")";
const monthName = dateObj.toLocaleDateString(currentLang, { month: 'long', year: 'numeric' });
document.getElementById('savingsModalTitle').innerText = tr("bud_sav_modal_title_edit") + " " + monthName + " (" + owner + ")";
document.getElementById('sav_total').value = rowData['TOTAL_BANQUE'] || '';
@@ -352,6 +375,7 @@ function editCustomSavingsMonth(monthDate, owner, rowData) {
if (container.children.length === 0) addCustomEpargneLine();
document.getElementById('savingsModal').style.display = 'flex';
document.body.classList.add('no-scroll');
}
function openCustomSavingsModal(owner) {
@@ -359,18 +383,22 @@ function openCustomSavingsModal(owner) {
document.getElementById('sav_month').value = '';
document.getElementById('sav_total').value = '';
document.getElementById('savingsModalTitle').innerText = "Saisir un mois (" + owner + ")";
document.getElementById('savingsModalTitle').innerText = tr("bud_sav_modal_title_add") + " (" + owner + ")";
const container = document.getElementById('linesContainer');
container.innerHTML = '';
addCustomEpargneLine();
document.getElementById('savingsModal').style.display = 'flex';
document.body.classList.add('no-scroll');
}
window.onclick = function(event) {
const modal = document.getElementById('savingsModal');
if (event.target == modal) modal.style.display = 'none';
if (event.target == modal) {
modal.style.display = 'none';
document.body.classList.remove('no-scroll');
}
}
const savingsForm = document.getElementById('savingsForm');
@@ -385,7 +413,7 @@ if (savingsForm) {
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.innerText;
submitBtn.innerText = "Enregistrement...";
submitBtn.innerText = tr('bud_sav_saving');
submitBtn.disabled = true;
const formData = new FormData(this);
@@ -395,7 +423,7 @@ if (savingsForm) {
.then(text => { window.location.reload(); })
.catch(error => {
console.error("Erreur:", error);
alert("Une erreur technique est survenue.");
alert(tr("bud_err_tech"));
submitBtn.innerText = originalText;
submitBtn.disabled = false;
});
@@ -403,14 +431,16 @@ if (savingsForm) {
}
function deleteEntireMonth(monthDate, owner) {
if (!confirm(`Supprimer TOUT le mois de ${monthDate} pour ${owner} ?`)) return;
const msg = tr('bud_sav_confirm_delete_month').replace('%m', monthDate).replace('%o', owner);
if (!confirm(msg)) 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."));
.catch(err => alert("<?= tr('bud_err_delete') ?>"));
}
function duplicateLastMonth(lastMonthDate, owner) {
@@ -421,7 +451,7 @@ function duplicateLastMonth(lastMonthDate, owner) {
let nextMonthStr = `${year}-${month}-01`;
const formatMonth = (d) => {
let str = new Date(d).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
let str = new Date(d).toLocaleDateString(currentLang, { month: 'long', year: 'numeric' });
return str.charAt(0).toUpperCase() + str.slice(1);
};
@@ -433,7 +463,10 @@ function duplicateLastMonth(lastMonthDate, owner) {
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} :`;
const message = tr('bud_sav_prompt_duplicate')
.replace('%s', sourceName)
.replace('%t1', targetName)
.replace('%t2', targetName);
let newTotal = prompt(message, defaultTotal);
@@ -451,15 +484,14 @@ function duplicateLastMonth(lastMonthDate, owner) {
try {
const d = JSON.parse(text);
if (d.success) window.location.reload();
else alert("Erreur serveur : " + (d.error || "Inconnue"));
else alert(tr('bud_err_server') + (d.error || "Inconnue"));
} catch(e) {
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.");
alert(tr("bud_err_network_dup"));
});
}
}
@@ -509,7 +541,7 @@ function updateSumResult() {
total += val;
});
document.getElementById('sumResultValue').innerText = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total);
document.getElementById('sumResultValue').innerText = new Intl.NumberFormat(currentLang, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total);
}
document.addEventListener('click', function(e) {
+64 -54
View File
@@ -17,8 +17,12 @@ $currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEF
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth));
$viewMonthDate = "$currentYear-$currentMonth-01";
// Conservation du tableau d'origine pour la correspondance en base de données
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
$currentMonthName = $moisFr[(int)$currentMonth] . ' ' . $currentYear;
// Affichage dynamique traduit
$monthTranslationKey = 'month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT);
$currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear;
// 4. Récupération des Dépenses Réelles du mois (Basé sur le Mois de Gestion !)
$stmtExp = $pdo->prepare("SELECT amount, label, category, budget_item_id FROM pf_expenses WHERE gestion_month = ?");
@@ -31,20 +35,20 @@ $totalRevenus = 0;
<div class="budget-view">
<div class="view-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2 style="margin:0;">Récapitulatif Mensuel</h2>
<button onclick="openRecapModal('add')" class="pf-btn"> Ajouter un frais / revenu</button>
<h2 style="margin:0;"><?= tr('bud_recap_monthly_title') ?></h2>
<button onclick="openRecapModal('add')" class="pf-btn"> <?= tr('bud_recap_btn_add') ?></button>
</div>
<div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0; overflow:hidden;">
<table class="pf-table" style="margin:0; box-shadow:none;">
<thead style="background:#f8fafc;">
<tr>
<th>Nom</th>
<th>Montant Prévu</th>
<th>Type</th>
<th>Jour</th>
<th>État du mois (<?= $currentMonthName ?>)</th>
<th style="text-align:right;">Actions</th>
<th><?= tr('bud_label_name') ?></th>
<th><?= tr('bud_planned_amount') ?></th>
<th><?= tr('bud_type') ?></th>
<th><?= tr('bud_day') ?></th>
<th><?= tr('bud_month_state') ?> (<?= $currentMonthName ?>)</th>
<th style="text-align:right;"><?= tr('actions') ?></th>
</tr>
</thead>
<tbody>
@@ -109,7 +113,7 @@ $totalRevenus = 0;
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid #f1f5f9;">
<td style="padding:15px;">
<strong><?= htmlspecialchars($item['name']) ?></strong>
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">(Est.)</small>' : '' ?>
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">('.tr('bud_est_short').')</small>' : '' ?>
<?php if(!empty($item['mapping_keywords'])): ?>
<span title="<?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
@@ -117,7 +121,7 @@ $totalRevenus = 0;
<?php if(!empty($item['reg_month'])): ?>
<div style="font-size:0.75rem; color:#94a3b8; font-style:italic; margin-top:2px;">
📅 Régul. prévue en <?= htmlspecialchars($item['reg_month']) ?>
📅 <?= sprintf(tr('bud_reg_planned_in'), tr('month_'.str_pad(array_search($item['reg_month'], $moisFr), 2, '0', STR_PAD_LEFT))) ?>
</div>
<?php endif; ?>
</td>
@@ -128,30 +132,29 @@ $totalRevenus = 0;
<?php if ($hasMatchingExpense): ?>
<?php
// Le "Gap" est la différence visuelle.
// S'il y a plus de $realAbs que prévu, c'est un dépassement ou un bonus.
$gap = $realAbs - $targetAbs;
if ($gap > 0.05): ?>
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">
<?= $item['category'] === 'income' ? 'Bonus :' : 'Dépassement :' ?> +<?= number_format($gap, 2, ',', ' ') ?> €
<?= $item['category'] === 'income' ? tr('bud_bonus') : tr('bud_overrun') ?> : +<?= number_format($gap, 2, ',', ' ') ?> €
</div>
<?php elseif ($gap < -0.05): ?>
<div style="font-size:0.75rem; color:#f59e0b; font-weight:normal;">
Reste : <?= number_format(abs($gap), 2, ',', ' ') ?> €
<?= tr('bud_remaining') ?> : <?= number_format(abs($gap), 2, ',', ' ') ?> €
</div>
<?php else: ?>
<div style="font-size:0.75rem; color:#10b981; font-weight:normal;">
Montant exact
<?= tr('bud_exact_amount') ?>
</div>
<?php endif; ?>
<?php elseif ($item['type'] === 'Annuel'): ?>
<div style="font-size:0.75rem; color:#94a3b8; font-weight:normal;">Soit <?= number_format($amountToAdd, 2, ',', ' ') ?>/mois</div>
<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>
<?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;">
<?= $item['type'] ?>
<?= tr('bud_freq_'.strtolower($item['type'])) ?>
</span>
</td>
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td>
@@ -159,23 +162,23 @@ $totalRevenus = 0;
<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> Validé
<span>✓</span> <?= tr('bud_state_validated') ?>
</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> Partiel
<span>⏳</span> <?= tr('bud_state_partial') ?>
</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> En attente
<span>○</span> <?= tr('bud_state_waiting') ?>
</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" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">✏️</button>
<button class="btn-icon" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="Supprimer" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">🗑️</button>
<button class="btn-icon" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">✏️</button>
<button class="btn-icon" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">🗑️</button>
</div>
</td>
</tr>
@@ -183,18 +186,18 @@ $totalRevenus = 0;
</tbody>
<tfoot style="background:#f8fafc;">
<tr>
<td colspan="1" style="padding:15px;"><strong>Total Revenus (Lissés)</strong></td>
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_income_smoothed') ?></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>Totalpenses (Lissées)</strong></td>
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_expenses_smoothed') ?></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>Équilibre théorique</strong></td>
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong><?= tr('bud_theoretical_balance_recap') ?></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, ',', ' ') ?> € / mois</strong>
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / <?= tr('bud_month_short') ?></strong>
</td>
</tr>
</tfoot>
@@ -202,7 +205,7 @@ $totalRevenus = 0;
</div>
<div class="budget-note" style="margin-top:15px; font-size:0.85rem; color:#64748b;">
<p>* L'état de paiement se met à jour automatiquement en fonction des opérations importées dans l'onglet "Suivi Mensuel".</p>
<p>* <?= tr('bud_recap_footer_note') ?></p>
</div>
</div>
@@ -210,7 +213,7 @@ $totalRevenus = 0;
<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;">Ajouter un élément</h3>
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;"><?= tr('bud_recap_modal_add') ?></h3>
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
</div>
@@ -219,67 +222,70 @@ $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;">Nom</label>
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="Ex: Loyer, Salaire..." 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;"><?= 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;">Mots-clés (Détection Auto)</label>
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="Ex: NETFLIX, PRIME VIDEO (séparés par virgule)" 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;">Si une dépense du mois contient ce mot, la ligne passera en "Validé".</small>
<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>
</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;">Montant (€)</label>
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_amount_eur') ?></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;">Jour (1-31)</label>
<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;">
</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;">Catégorie</label>
<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">Dépense (Frais)</option>
<option value="income">Revenu (Salaire)</option>
<option value="expense"><?= tr('bud_cat_expense') ?></option>
<option value="income"><?= tr('bud_cat_income') ?></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;">Fréquence</label>
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_frequency') ?></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">Mensuel</option>
<option value="Annuel">Annuel</option>
<option value="Mensuel"><?= tr('bud_freq_mensuel') ?></option>
<option value="Annuel"><?= tr('bud_freq_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;">Type de montant</label>
<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">Fixe (Facture)</option>
<option value="1">Variable (Estimation)</option>
<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;">gularisation</label>
<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="">Aucune</option>
<option value=""><?= tr('bud_reg_none') ?></option>
<?php
$mois = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
foreach($mois as $m) echo "<option value='$m'>$m</option>";
foreach($moisFr as $index => $m) {
if($index == 0) continue;
// On injecte la valeur en français (pour la DB) mais on affiche la version traduite
echo "<option value='$m'>" . tr('month_'.str_pad($index, 2, '0', STR_PAD_LEFT)) . "</option>";
}
?>
</select>
</div>
</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;">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>
<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>
</form>
</div>
@@ -290,26 +296,29 @@ window.onclick = function(event) {
const modal = document.getElementById('budgetRecapModal');
if (event.target == modal) {
modal.style.display = 'none';
document.body.classList.remove('no-scroll');
}
}
function openRecapModal(mode) {
if (mode === "add") {
document.getElementById("recapModalTitle").innerText = "Ajouter un élément";
document.getElementById("recapModalTitle").innerText = "<?= tr('bud_recap_modal_add') ?>";
document.getElementById("item_id").value = "";
document.getElementById("recapForm").reset();
}
document.getElementById("budgetRecapModal").style.display = "flex";
document.body.classList.add('no-scroll');
}
function closeRecapModal() {
document.getElementById("budgetRecapModal").style.display = "none";
document.body.classList.remove('no-scroll');
}
function editRecapItem(item) {
const data = typeof item === "string" ? JSON.parse(item) : item;
document.getElementById("recapModalTitle").innerText = "Modifier : " + data.name;
document.getElementById("recapModalTitle").innerText = "<?= tr('bud_recap_modal_edit') ?> : " + data.name;
document.getElementById("item_id").value = data.id;
document.getElementById("item_name").value = data.name;
document.getElementById("item_keywords").value = data.mapping_keywords || '';
@@ -322,10 +331,11 @@ function editRecapItem(item) {
document.getElementById("item_is_estimate").value = data.is_estimate;
document.getElementById("budgetRecapModal").style.display = "flex";
document.body.classList.add('no-scroll');
}
function deleteRecapItem(id) {
if (confirm("Voulez-vous vraiment supprimer cet élément ?")) {
if (confirm("<?= tr('bud_recap_confirm_delete') ?>")) {
const formData = new FormData();
formData.append("action", "delete");
formData.append("id", id);
@@ -336,7 +346,7 @@ function deleteRecapItem(id) {
}).then(() => {
window.location.reload();
}).catch(err => {
alert("Erreur lors de la suppression.");
alert("<?= tr('bud_err_delete') ?>");
console.error(err);
});
}
+78 -83
View File
@@ -5,14 +5,12 @@
// 1. GESTION DU MOIS ACTIF ET DE LA NAVIGATION
// ============================================================================
// A. Déterminer quel est le mois "ouvert" par défaut
$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');
}
// B. Mois affiché à l'écran
$viewM = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth));
$viewY = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth));
$viewMonthDate = "$viewY-$viewM-01";
@@ -168,21 +166,18 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
// 3. RECUPERATION DES DONNEES ET CALCULS
// ============================================================================
// A. Vérifier si le mois affiché est clôturé
$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
$stmtCheckClose->execute([$viewMonthDate]);
$closureJson = $stmtCheckClose->fetchColumn();
$monthState = $closureJson ? json_decode($closureJson, true) : null;
$isClosed = ($monthState && isset($monthState['is_closed']) && $monthState['is_closed'] === true);
// B. Récupérer le solde bancaire (Snapshot actuel)
$snapshot = ['date' => date('Y-m-d'), 'amount' => 0];
$snapStmt = $pdo->query("SELECT * FROM pf_bank_snapshots ORDER BY id DESC LIMIT 1");
if ($s = $snapStmt->fetch(PDO::FETCH_ASSOC)) {
$snapshot = ['date' => $s['snapshot_date'], 'amount' => (float)$s['amount']];
}
// C. Récupérer le solde final du mois PRÉCÉDENT pour avoir la base de ce mois-ci
$stmtPrevClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
$stmtPrevClose->execute([$prevDate]);
$prevClosureJson = $stmtPrevClose->fetchColumn();
@@ -193,7 +188,6 @@ if ($solde_initial === 0 && !$prevMonthState) {
$solde_initial = $snapshot['amount'];
}
// D. Les Dépenses assignées à ce MOIS DE GESTION
$stmt = $pdo->prepare("SELECT * FROM pf_expenses WHERE gestion_month = ? ORDER BY date_exp DESC");
$stmt->execute([$viewMonthDate]);
$allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -201,13 +195,11 @@ $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');
// E. Lecture du Budget
$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_income_prevu = 0;
$total_income = 0; $total_expenses_prevues = 0;
$reste_a_venir_calc = 0;
$fixedChargesList = []; $incomeList = []; $pending_charges = [];
// CORRECTION BUG N°2 : On supprime la vérification "is_checked" de la base, on ne se fie qu'aux dépenses réelles du mois.
$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']);
@@ -249,26 +241,25 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
$budget_autres = max(0, $total_income - $total_expenses_prevues);
// TRADUCTION DES CATÉGORIES
$categoriesConfig = [
'Income' => ['type'=>'credit', 'label'=>'Revenus', 'budget'=>$budget_income_prevu, 'color'=>'#10b981', 'suggestions'=>[]],
'FMCG' => ['type'=>'debit', 'label'=>'Courses (FMCG)', 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']],
'Essence' => ['type'=>'debit', 'label'=>'Essence', 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']],
'School' => ['type'=>'debit', 'label'=>'École / Garde', 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
'Frais' => ['type'=>'debit', 'label'=>'Charges Fixes', 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]],
'Autres' => ['type'=>'debit', 'label'=>'Autres / Imprévus', 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']],
'LivretA' => ['type'=>'debit', 'label'=>'Epargne', 'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
'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']],
'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
];
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
$total_rentrees = 0;
$depenses_reelles = 0;
foreach ($allExpenses as $exp) {
$cat = $exp['category'];
if (!isset($totals[$cat])) $cat = 'Autres';
$val = (float)$exp['amount'];
if ($cat === 'Income') { $totals[$cat] += $val; }
@@ -286,28 +277,26 @@ foreach ($allExpenses as $exp) {
}
}
// 1. École
// 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' => 'École (Reste estimé)', 'amount' => $reste_ecole];
$pending_charges[] = ['name' => tr('bud_rem_school'), 'amount' => $reste_ecole];
}
// 2. Courses (FMCG)
$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' => 'Courses (Reste estimé)', 'amount' => $reste_fmcg];
$pending_charges[] = ['name' => tr('bud_rem_fmcg'), 'amount' => $reste_fmcg];
}
// 3. Essence
$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' => 'Essence (Reste estimé)', 'amount' => $reste_essence];
$pending_charges[] = ['name' => tr('bud_rem_fuel'), 'amount' => $reste_essence];
}
// F. Calculs des KPIs finaux
@@ -315,7 +304,6 @@ $rentrees_salaires_reels = $totals['Income'] ?? 0;
$rentrees_autres = $total_rentrees - $rentrees_salaires_reels;
$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu);
// Capacité Max : Solde fin de mois précédent + Salaire retenu + Autres rentrées
$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres;
$solde_theorique_calc = ($solde_initial + $total_rentrees) - $depenses_reelles - $reste_a_venir_calc;
@@ -347,8 +335,13 @@ function getDisplayLogic($spent, $bg, $type) {
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
}
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
$monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
// 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'),
9 => tr('month_09'), 10 => tr('month_10'), 11 => tr('month_11'), 12 => tr('month_12')
];
$monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
?>
<div class="budget-view">
@@ -357,37 +350,37 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<?php if ($isClosed): ?>
<div style="position:absolute; top:0; left:0; right:0; background:#f1f5f9; padding:5px; text-align:center; font-size:0.8rem; font-weight:bold; color:#64748b; border-radius:16px 16px 0 0; border-bottom:1px solid #e2e8f0;">
🔒 Ce mois est clôturé et archivé.
🔒 <?= tr('bud_closed_archived') ?>
</div>
<div style="height:20px;"></div>
<?php endif; ?>
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; margin-bottom: 20px;">
<div style="display:flex; align-items:center; gap:15px;">
<h2 style="margin:0; font-size:1.3rem; color:#0f172a; text-transform:capitalize;">Budget : <?= $monthName ?></h2>
<h2 style="margin:0; font-size:1.3rem; color:#0f172a; text-transform:capitalize;"><?= tr('bud_budget_of') ?> <?= $monthName ?></h2>
</div>
<div style="display:flex; gap:10px;">
<?php if (!$isClosed): ?>
<form method="POST" onsubmit="return confirm('Clôturer ce mois ? Cela figera les soldes actuels et basculera le suivi par défaut sur le mois prochain.');">
<form method="POST" onsubmit="return confirm('<?= tr('bud_confirm_close') ?>');">
<input type="hidden" name="action" value="close_month">
<input type="hidden" name="close_month_date" value="<?= $viewMonthDate ?>">
<input type="hidden" name="freeze_solde_actuel" value="<?= $solde_actuel ?>">
<input type="hidden" name="freeze_capacite_max" value="<?= $capacite_max ?>">
<input type="hidden" name="freeze_solde_theorique" value="<?= $solde_theorique ?>">
<input type="hidden" name="freeze_reste_a_venir" value="<?= $reste_a_venir ?>">
<button type="submit" class="pf-btn" style="background:linear-gradient(135deg, #10b981, #059669); padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔒 Clôturer</button>
<button type="submit" class="pf-btn" style="background:linear-gradient(135deg, #10b981, #059669); padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔒 <?= tr('bud_close_btn') ?></button>
</form>
<?php else: ?>
<form method="POST" onsubmit="return confirm('Rouvrir ce mois ? Ses soldes seront recalculés en direct.');">
<form method="POST" onsubmit="return confirm('<?= tr('bud_confirm_reopen') ?>');">
<input type="hidden" name="action" value="reopen_month">
<button type="submit" class="pf-btn btn-secondary" style="padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔓 Rouvrir</button>
<button type="submit" class="pf-btn btn-secondary" style="padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔓 <?= tr('bud_reopen_btn') ?></button>
</form>
<?php endif; ?>
<div class="suivi-nav-group">
<a href="<?= $prevLink ?>" class="suivi-btn-nav">◀</a>
<a href="<?= $defaultLink ?>" class="suivi-btn-nav">Mois Actif</a>
<a href="<?= $defaultLink ?>" class="suivi-btn-nav"><?= tr('bud_active_month_btn') ?></a>
<a href="<?= $nextLink ?>" class="suivi-btn-nav">▶</a>
</div>
</div>
@@ -395,17 +388,17 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:15px; margin-bottom:25px;">
<div style="padding:15px; background:#f8fafc; border-radius:12px; position:relative; border:1px solid #e2e8f0;">
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;">Solde bancaire <?= $isClosed ? "(Figé)" : "(Actuel)" ?></div>
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;"><?= tr('bud_bank_balance') ?> <?= $isClosed ? "(".tr('bud_frozen').")" : "(".tr('bud_current').")" ?></div>
<div style="font-size:1.4rem; font-weight:700; color:#0f172a;">
<?= number_format($solde_actuel, 2, ',', ' ') ?> €
</div>
<?php if (!$isClosed): ?>
<button onclick="openSuiviModal('snapshotModal')" style="position:absolute; top:12px; right:12px; background:white; border:1px solid #cbd5e1; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.8rem; color:#475569; transition:0.2s;" onmouseover="this.style.background='#f1f5f9'" onmouseout="this.style.background='white'">✏️ MàJ</button>
<button onclick="openSuiviModal('snapshotModal')" style="position:absolute; top:12px; right:12px; background:white; border:1px solid #cbd5e1; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.8rem; color:#475569; transition:0.2s;" onmouseover="this.style.background='#f1f5f9'" onmouseout="this.style.background='white'">✏️ <?= tr('bud_update_btn') ?></button>
<?php endif; ?>
</div>
<div style="padding:15px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0;">
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;">Solde théorique fin de mois</div>
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;"><?= tr('bud_theoretical_balance') ?></div>
<div style="font-size:1.4rem; font-weight:700; color:<?= $solde_theorique < 0 ? '#ef4444' : '#334155' ?>;">
<?= number_format($solde_theorique, 2, ',', ' ') ?> €
</div>
@@ -413,7 +406,7 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div style="padding:15px; background:#fef2f2; border-radius:12px; border:1px solid #fecaca; position: relative;">
<div style="font-size:0.85rem; color:#991b1b; margin-bottom:4px; display:flex; justify-content:space-between; align-items:center;">
<span>Charges à venir</span>
<span><?= tr('bud_upcoming_charges') ?></span>
<button onclick="toggleDiv('pendingDetailsList')" style="background:none; border:none; cursor:pointer; font-size:1rem; padding:0; filter: grayscale(1); opacity: 0.7; transition: 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7">👁️</button>
</div>
<div style="font-size:1.4rem; font-weight:700; color:#b91c1c;">
@@ -421,7 +414,7 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div id="pendingDetailsList" style="display:none; margin-top: 12px; padding-top: 12px; border-top: 1px dashed #fca5a5; font-size: 0.8rem; color: #7f1d1d;">
<?php if (empty($pending_charges)): ?>
<div style="text-align:center; font-style:italic; opacity:0.8;">Tout est payé !</div>
<div style="text-align:center; font-style:italic; opacity:0.8;"><?= tr('bud_all_paid') ?></div>
<?php else: ?>
<?php foreach($pending_charges as $pc): ?>
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
@@ -438,12 +431,12 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div style="position: absolute; top: 0px; left: 10px; font-size: 0.8rem; color: #64748b; font-weight: 700;">0 €</div>
<div style="position: absolute; top: 0px; right: 10px; font-size: 0.8rem; color: #64748b; font-weight: 700; text-align:right; cursor:help;"
title="Calcul : Report Mois Précédent (<?= number_format($solde_initial, 0) ?>€) + Salaires (<?= number_format($salaires_retenus, 0) ?>€) + Autres rentrées (<?= number_format($rentrees_autres, 0) ?>€)">
Capacité Max : <?= number_format($capacite_max, 0, ',', ' ') ?> €
title="<?= sprintf(tr('bud_capacity_tooltip'), number_format($solde_initial, 0), number_format($salaires_retenus, 0), number_format($rentrees_autres, 0)) ?>">
<?= tr('bud_max_capacity') ?> : <?= number_format($capacite_max, 0, ',', ' ') ?> €
</div>
<div style="position: absolute; top: 12px; left: <?= $pct_actuel ?>%; transform: translateX(-50%); text-align: center; z-index: 10;">
<div style="color: #334155; font-size: 0.75rem; font-weight: 800; white-space: nowrap; margin-bottom: 2px;">Actuel</div>
<div style="color: #334155; font-size: 0.75rem; font-weight: 800; white-space: nowrap; margin-bottom: 2px;"><?= tr('bud_actual') ?></div>
<div style="width: 3px; height: 18px; background: #334155; margin: 0 auto; border-radius: 2px;"></div>
</div>
<div style="height: 24px; background: #e2e8f0; border-radius: 12px; display: flex; overflow: hidden; position: relative; box-shadow: inset 0 2px 4px rgba(0,0,0,0.05);">
@@ -452,37 +445,37 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div style="position: absolute; top: 54px; left: <?= $pct_theorique ?>%; transform: translateX(-50%); text-align: center; z-index: 10;">
<div style="width: 3px; height: 18px; background: #8b5cf6; margin: 0 auto 2px auto; border-radius: 2px;"></div>
<div style="color: #8b5cf6; font-size: 0.75rem; font-weight: 800; white-space: nowrap;">Théorique</div>
<div style="color: #8b5cf6; font-size: 0.75rem; font-weight: 800; white-space: nowrap;"><?= tr('bud_theoretical') ?></div>
</div>
</div>
</div>
<?php if (!$isClosed): ?>
<div style="display:flex; gap:10px; margin-bottom:20px;">
<button class="pf-btn btn-secondary" onclick="toggleDiv('importCsvForm')">📂 Importer CSV</button>
<button class="pf-btn btn-secondary" onclick="toggleDiv('importCsvForm')">📂 <?= tr('bud_import_csv') ?></button>
</div>
<div id="importCsvForm" style="display:<?= $showPreview ? 'block' : 'none' ?>; background:#f8fafc; padding:20px; border-radius:12px; margin-bottom:20px;">
<?php if (!$showPreview): ?>
<form method="POST" enctype="multipart/form-data" style="display:flex; gap:10px; align-items:end; flex-wrap:wrap;">
<div style="flex:1; min-width:250px;">
<label class="pf-label">Fichier CSV (Point-virgule)</label>
<label class="pf-label"><?= tr('bud_csv_file') ?></label>
<input type="file" name="csv_file" accept=".csv" class="pf-input">
</div>
<button type="submit" class="pf-btn" style="width:auto;">Prévisualiser</button>
<button type="submit" class="pf-btn" style="width:auto;"><?= tr('bud_preview') ?></button>
</form>
<?php else: ?>
<form method="POST" id="formMapping">
<input type="hidden" name="action" value="save_import">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; flex-wrap:wrap; gap:10px;">
<h3 style="margin:0;">Valider l'importation</h3>
<h3 style="margin:0;"><?= tr('bud_validate_import') ?></h3>
<span id="missingCount" style="color:#ef4444; background:#fee2e2; padding:4px 10px; border-radius:12px; font-weight:bold; font-size:0.85rem; display:none;"></span>
</div>
<div style="max-height:350px; overflow-y:auto; background:white; border:1px solid #eee; border-radius:8px;">
<table class="pf-table" style="margin:0;">
<thead style="position:sticky; top:0; z-index:10;">
<tr><th><input type="checkbox" onclick="toggleAll(this)" checked></th><th>Imputer au mois</th><th>Date Opération</th><th>Libellé</th><th>Montant</th><th>Catégorie</th></tr>
<tr><th><input type="checkbox" onclick="toggleAll(this)" checked></th><th><?= tr('bud_assign_month') ?></th><th><?= tr('bud_op_date') ?></th><th><?= tr('bud_label') ?></th><th><?= tr('bud_amount') ?></th><th><?= tr('bud_category') ?></th></tr>
</thead>
<tbody>
<?php foreach ($csvData as $idx => $row):
@@ -507,17 +500,17 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<td>
<div style="display:flex; gap:5px;">
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="flex:1;">
<option value="">-- <?= $isCrd ? 'Ignorer' : 'À définir' ?> --</option>
<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>
<?php endforeach; ?>
</select>
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; flex:1;" disabled>
<option value="">-- Charge ? --</option>
<option value="">-- <?= tr('bud_is_charge') ?> --</option>
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
</select>
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-income" onchange="checkValidation()" style="display:none; flex:1;" disabled>
<option value="">-- Revenu ? --</option>
<option value="">-- <?= tr('bud_is_income') ?> --</option>
<?php foreach ($incomeList as $inc): ?><option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option><?php endforeach; ?>
</select>
</div>
@@ -528,8 +521,8 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</table>
</div>
<div style="margin-top:15px; display:flex; justify-content:flex-end; gap:10px;">
<a href="?tab=suivi" class="pf-btn btn-secondary" style="width:auto;">Annuler</a>
<button type="submit" id="btnImport" class="pf-btn" style="width:auto;">Importer</button>
<a href="?tab=suivi" class="pf-btn btn-secondary" style="width:auto;"><?= tr('btn_cancel') ?></a>
<button type="submit" id="btnImport" class="pf-btn" style="width:auto;"><?= tr('btn_import') ?></button>
</div>
</form>
<?php endif; ?>
@@ -558,7 +551,7 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div style="flex:1; max-height:300px; overflow-y:auto; padding:0;">
<?php if (empty($expensesByCategory[$key])): ?>
<div style="padding:20px; text-align:center; color:#cbd5e1; font-style:italic; font-size:0.85rem;">Aucune ligne.</div>
<div style="padding:20px; text-align:center; color:#cbd5e1; font-style:italic; font-size:0.85rem;"><?= tr('bud_no_lines') ?></div>
<?php else: ?>
<table style="width:100%; border-collapse:collapse; font-size:0.85rem;">
<?php foreach ($expensesByCategory[$key] as $exp): ?>
@@ -572,8 +565,8 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</td>
<?php if (!$isClosed): ?>
<td style="width:60px; padding-right:10px; text-align:right; white-space:nowrap;">
<button onclick='openEditModal(<?= json_encode($exp) ?>)' style="background:none; border:none; cursor:pointer; font-size:1rem; margin-right:8px;">✏️</button>
<a href="?tab=suivi&delete_expense=<?= $exp['id'] ?>" onclick="return confirm('Supprimer ?')" style="color:#ef4444; text-decoration:none; font-size:1.4rem;">&times;</a>
<button onclick='openEditModal(<?= json_encode($exp, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)' style="background:none; border:none; cursor:pointer; font-size:1rem; margin-right:8px;">✏️</button>
<a href="?tab=suivi&delete_expense=<?= $exp['id'] ?>" onclick="return confirm('<?= tr('bud_confirm_delete') ?>')" style="color:#ef4444; text-decoration:none; font-size:1.4rem;">&times;</a>
</td>
<?php endif; ?>
</tr>
@@ -589,25 +582,25 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div id="manualExpenseModal" class="pf-modal">
<div class="pf-modal-content" style="max-width:400px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 style="margin:0;" id="modalTitle">Nouvelle transaction</h3>
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" style="border:none; background:none; font-size:1.8rem; cursor:pointer;">&times;</button>
<h3 style="margin:0;" id="modalTitle"><?= tr('bud_new_transaction') ?></h3>
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" class="pf-modal-close">&times;</button>
</div>
<form method="POST">
<input type="hidden" name="action" value="save_expense_manual">
<input type="hidden" name="expense_id" id="modalExpenseId">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Imputer au mois de gestion :</label>
<label class="pf-label"><?= tr('bud_assign_to_month') ?> :</label>
<input type="month" name="gestion_month" id="modalGestionMonth" class="pf-input" style="font-weight:bold; color:#2563eb;" required>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Date réelle de l'opération</label>
<label class="pf-label"><?= tr('bud_real_date') ?></label>
<input type="date" name="date" id="modalDate" class="pf-input" required>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Catégorie</label>
<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>
@@ -616,21 +609,21 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Type</label>
<label class="pf-label"><?= tr('bud_type') ?></label>
<select name="is_credit" id="modalIsCredit" class="pf-input">
<option value="0">Dépense (-)</option>
<option value="1">Revenu / Remboursement (+)</option>
<option value="0"><?= tr('bud_type_expense') ?> (-)</option>
<option value="1"><?= tr('bud_type_income') ?> (+)</option>
</select>
</div>
<div class="form-group" id="blockInputText" style="margin-bottom:15px;">
<label class="pf-label">Libellé</label>
<label class="pf-label"><?= tr('bud_label') ?></label>
<input type="text" name="label" class="pf-input" id="modalLabelInput" list="modalSuggestions" autocomplete="off">
<datalist id="modalSuggestions"></datalist>
</div>
<div class="form-group" id="blockInputSelect" style="margin-bottom:15px; display:none;">
<label class="pf-label">Bénéficiaire</label>
<label class="pf-label"><?= tr('bud_beneficiary') ?></label>
<select name="label_select" id="schoolSelect" class="pf-input">
<option value="Ecole Pol">Ecole Pol</option>
<option value="Carole">Carole</option>
@@ -638,7 +631,7 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;">
<label class="pf-label">Charge Fixe</label>
<label class="pf-label"><?= tr('bud_fixed_charge') ?></label>
<select name="budget_item_id" id="fraisSelect" class="pf-input" disabled>
<option value="">-- Sélectionner --</option>
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
@@ -646,7 +639,7 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div class="form-group" id="blockInputIncome" style="margin-bottom:15px; display:none;">
<label class="pf-label">Revenu Attendu</label>
<label class="pf-label"><?= tr('bud_expected_income') ?></label>
<select name="budget_item_id" id="incomeSelect" class="pf-input" disabled>
<option value="">-- Sélectionner --</option>
<?php foreach ($incomeList as $inc): ?><option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option><?php endforeach; ?>
@@ -654,13 +647,13 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Montant (€)</label>
<label class="pf-label"><?= tr('bud_amount_eur') ?></label>
<input type="number" step="0.01" name="amount" id="modalAmount" class="pf-input" placeholder="0.00" required>
</div>
<div style="margin-top:20px; display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" class="pf-btn btn-secondary" style="width:auto;">Annuler</button>
<button type="submit" class="pf-btn" style="width:auto;">Enregistrer</button>
<div class="modal-footer">
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
@@ -668,14 +661,14 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div id="snapshotModal" class="pf-modal">
<div class="pf-modal-content" style="max-width:350px;">
<h3 style="margin-top:0;">MàJ Solde Bancaire</h3>
<h3 style="margin-top:0;"><?= tr('bud_update_balance') ?></h3>
<form method="POST">
<input type="hidden" name="action" value="save_snapshot">
<input type="date" name="snapshot_date" class="pf-input" value="<?= date('Y-m-d') ?>" style="margin-bottom:10px;" required>
<input type="number" step="0.01" name="snapshot_amount" class="pf-input" placeholder="Solde (€)" style="margin-bottom:15px;" required>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn btn-secondary" style="width:auto;">Annuler</button>
<button type="submit" class="pf-btn" style="width:auto;">Sauvegarder</button>
<input type="number" step="0.01" name="snapshot_amount" class="pf-input" placeholder="<?= tr('bud_balance_eur') ?>" style="margin-bottom:15px;" required>
<div class="modal-footer">
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
@@ -684,14 +677,13 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<script>
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).style.display = 'flex'; }
function closeSuiviModal(id) { document.getElementById(id).style.display = 'none'; }
function openSuiviModal(id) { document.getElementById(id).style.display = 'flex'; document.body.classList.add('no-scroll'); }
function closeSuiviModal(id) { document.getElementById(id).style.display = 'none'; document.body.classList.remove('no-scroll');}
const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>;
function handleModalCatChange(select) {
const catKey = select.value;
const catText = select.options[select.selectedIndex].text.toLowerCase();
document.getElementById('blockInputText').style.display = 'none';
document.getElementById('blockInputSelect').style.display = 'none';
@@ -716,10 +708,10 @@ function handleModalCatChange(select) {
function openAddModal(catKey, catLabel) {
openSuiviModal('manualExpenseModal');
document.getElementById('modalTitle').innerText = "Ajouter : " + catLabel;
document.getElementById('modalTitle').innerText = "<?= tr('bud_add_title') ?> : " + catLabel;
document.getElementById('modalExpenseId').value = "";
document.getElementById('modalDate').value = new Date().toISOString().split('T')[0];
document.getElementById('modalGestionMonth').value = activeViewMonth; // Force le mois de gestion actuel
document.getElementById('modalGestionMonth').value = activeViewMonth;
document.getElementById('modalLabelInput').value = "";
document.getElementById('modalAmount').value = "";
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey; handleModalCatChange(catSelect);
@@ -728,7 +720,7 @@ function openAddModal(catKey, catLabel) {
function openEditModal(e) {
openSuiviModal('manualExpenseModal');
document.getElementById('modalTitle').innerText = "Modifier la transaction";
document.getElementById('modalTitle').innerText = "<?= tr('bud_edit_title') ?>";
document.getElementById('modalExpenseId').value = e.id;
document.getElementById('modalDate').value = e.date_exp;
document.getElementById('modalGestionMonth').value = e.gestion_month ? e.gestion_month.substring(0, 7) : activeViewMonth;
@@ -748,7 +740,9 @@ function handleLineCatChange(select) {
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; }
checkValidation();
}
function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disabled])').forEach(c => c.checked = src.checked); checkValidation(); }
function checkValidation() {
let miss = 0;
document.querySelectorAll('.line-checkbox:checked').forEach(cb => {
@@ -757,8 +751,9 @@ function checkValidation() {
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+' à définir'; }
if(miss>0) { btn.disabled = true; btn.style.opacity=0.5; msg.style.display='inline'; msg.innerText = miss + ' <?= tr('bud_to_define_js') ?>'; }
else { btn.disabled = false; btn.style.opacity=1; msg.style.display='none'; }
}
if(document.getElementById('formMapping')) { document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s)); checkValidation(); }
</script>