input
This commit is contained in:
@@ -213,7 +213,7 @@
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-icon:hover:hover {
|
.btn-icon:hover {
|
||||||
background: #e2e8f0;
|
background: #e2e8f0;
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
@@ -558,7 +558,7 @@ input[type="number"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- 10. BUDGET PREVISIONNEL (TIMELINE 2025) --- */
|
/* --- 10. BUDGET PREVISIONNEL (TIMELINE) --- */
|
||||||
|
|
||||||
.prev-container {
|
.prev-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1111,3 +1111,34 @@ input[type="month"].pf-input {
|
|||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
14. EPARGNE (epargne.php)
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
/* Style compact pour les inputs intégrés au tableau d'épargne */
|
||||||
|
.epargne-inline-input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 60px;
|
||||||
|
max-width: 85px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: 0.2s;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epargne-inline-input:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epargne-inline-input:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: #fff;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,33 @@ require __DIR__ . '/../../../../includes/auth.php';
|
|||||||
require __DIR__ . '/../../../../includes/db.php';
|
require __DIR__ . '/../../../../includes/db.php';
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// MISE À JOUR D'UNE CELLULE EN DIRECT (AJAX)
|
||||||
|
// =================================================================
|
||||||
|
if ($action === 'update_single_entry') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$month = $_POST['month_date'];
|
||||||
|
$cat = $_POST['category'];
|
||||||
|
$owner = $_POST['owner'];
|
||||||
|
$amount = (float)$_POST['amount'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($amount == 0 && $cat !== 'TOTAL_BANQUE') {
|
||||||
|
// Si on met à 0 une ligne (autre que le total), on supprime l'entrée pour garder la base propre
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_savings WHERE month_date=? AND owner=? AND category=?");
|
||||||
|
$stmt->execute([$month, $owner, $cat]);
|
||||||
|
} else {
|
||||||
|
// Sinon on insère ou on met à jour
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_savings (month_date, owner, category, amount) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE amount = VALUES(amount)");
|
||||||
|
$stmt->execute([$month, $owner, $cat, $amount]);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// --- ACTION : SUPPRESSION D'UNE ENTRÉE UNIQUE ---
|
// --- ACTION : SUPPRESSION D'UNE ENTRÉE UNIQUE ---
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_entry') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_entry') {
|
||||||
$owner = $_POST['owner'];
|
$owner = $_POST['owner'];
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
$salaryConfig[$row['person']] = $row;
|
$salaryConfig[$row['person']] = $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init si vide
|
|
||||||
foreach (['Alex', 'Laia'] as $p) {
|
foreach (['Alex', 'Laia'] as $p) {
|
||||||
if (!isset($salaryConfig[$p])) {
|
if (!isset($salaryConfig[$p])) {
|
||||||
$salaryConfig[$p] = ['salary'=>0, 'mensualite'=>0, 'frais_func'=>0, 'eco_perso'=>0, 'eco_family'=>0];
|
$salaryConfig[$p] = ['salary'=>0, 'mensualite'=>0, 'frais_func'=>0, 'eco_perso'=>0, 'eco_family'=>0];
|
||||||
@@ -25,16 +24,26 @@ $cats = $pdo->query("SELECT * FROM pf_alloc_categories ORDER BY sort_order ASC,
|
|||||||
$focusDate = isset($_GET['focus_date']) ? $_GET['focus_date'] : date('Y-m-01');
|
$focusDate = isset($_GET['focus_date']) ? $_GET['focus_date'] : date('Y-m-01');
|
||||||
$focusTs = strtotime($focusDate);
|
$focusTs = strtotime($focusDate);
|
||||||
|
|
||||||
// On affiche 6 mois (Timeline inversée)
|
|
||||||
$months = [];
|
$months = [];
|
||||||
for ($i = 0; $i < 6; $i++) {
|
for ($i = 0; $i < 6; $i++) {
|
||||||
$months[] = date('Y-m-01', strtotime("-$i months", $focusTs));
|
$months[] = date('Y-m-01', strtotime("-$i months", $focusTs));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Liens navigation
|
|
||||||
$prevMonthLink = date('Y-m-01', strtotime("-1 month", $focusTs));
|
$prevMonthLink = date('Y-m-01', strtotime("-1 month", $focusTs));
|
||||||
$nextMonthLink = date('Y-m-01', strtotime("+1 month", $focusTs));
|
$nextMonthLink = date('Y-m-01', strtotime("+1 month", $focusTs));
|
||||||
|
|
||||||
|
// NOUVEAU : Récupération des Cycles configurés dans pf_notes
|
||||||
|
$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';
|
||||||
|
$cycleConfigs[$mKey] = json_decode($row['content'], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Récupération Valeurs Répartition
|
// 4. Récupération Valeurs Répartition
|
||||||
$inQuery = implode(',', array_fill(0, count($months), '?'));
|
$inQuery = implode(',', array_fill(0, count($months), '?'));
|
||||||
$stmt = $pdo->prepare("SELECT * FROM pf_alloc_values WHERE month_date IN ($inQuery)");
|
$stmt = $pdo->prepare("SELECT * FROM pf_alloc_values WHERE month_date IN ($inQuery)");
|
||||||
@@ -46,19 +55,16 @@ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Récupération de l'ID de la catégorie système
|
// 5. Récupération de l'ID de la catégorie système
|
||||||
// On cherche l'ID de la catégorie 'SYSTEM_VALIDATION'
|
|
||||||
$sysCatId = null;
|
$sysCatId = null;
|
||||||
foreach ($cats as $key => $c) {
|
foreach ($cats as $key => $c) {
|
||||||
if ($c['name'] === 'SYSTEM_VALIDATION') {
|
if ($c['name'] === 'SYSTEM_VALIDATION') {
|
||||||
$sysCatId = $c['id'];
|
$sysCatId = $c['id'];
|
||||||
// IMPORTANT : On retire cette catégorie de la liste affichable ($cats)
|
|
||||||
// pour qu'elle n'apparaisse pas dans le tableau HTML !
|
|
||||||
unset($cats[$key]);
|
unset($cats[$key]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Lecture des statuts de validation (1 = Validé, 0 = Non)
|
// 6. Lecture des statuts de validation
|
||||||
$focusDate = $months[0];
|
$focusDate = $months[0];
|
||||||
$isValidatedAlex = false;
|
$isValidatedAlex = false;
|
||||||
$isValidatedLaia = false;
|
$isValidatedLaia = false;
|
||||||
@@ -73,7 +79,7 @@ $stmtNote = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'budge
|
|||||||
$stmtNote->execute([$focusDate]);
|
$stmtNote->execute([$focusDate]);
|
||||||
$currentNote = $stmtNote->fetchColumn();
|
$currentNote = $stmtNote->fetchColumn();
|
||||||
|
|
||||||
// 7. Récupération des vacances actives pour les lier au budget
|
// 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);
|
$activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN ('draft', 'planned', 'booked') ORDER BY start_date ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -154,6 +160,13 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
|
|||||||
?>
|
?>
|
||||||
<th colspan="3" class="th-month <?= $cls ?>">
|
<th colspan="3" class="th-month <?= $cls ?>">
|
||||||
<?= date('F Y', strtotime($month)) ?>
|
<?= date('F Y', strtotime($month)) ?>
|
||||||
|
<?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>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
</th>
|
</th>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -188,10 +201,9 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
|
|||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<?php foreach ($cats as $cat):
|
<?php foreach ($cats as $cat):
|
||||||
// Détection des lignes indicatives
|
|
||||||
$isIndicative = ($cat['name'] === 'Eco Alex' || $cat['name'] === 'Eco Laia');
|
$isIndicative = ($cat['name'] === 'Eco Alex' || $cat['name'] === 'Eco Laia');
|
||||||
$rowClass = $isIndicative ? 'row-indicative' : '';
|
$rowClass = $isIndicative ? 'row-indicative' : '';
|
||||||
$inputClass = $isIndicative ? 'ignore-calc' : ''; // Classe pour le JS
|
$inputClass = $isIndicative ? 'ignore-calc' : '';
|
||||||
$rowStyle = $isIndicative ? 'background:#f8fafc; color:#94a3b8;' : '';
|
$rowStyle = $isIndicative ? 'background:#f8fafc; color:#94a3b8;' : '';
|
||||||
?>
|
?>
|
||||||
<tr class="<?= $rowClass ?>" style="<?= $rowStyle ?>">
|
<tr class="<?= $rowClass ?>" style="<?= $rowStyle ?>">
|
||||||
@@ -283,16 +295,11 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
|
|||||||
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$focusMonth = $months[0]; // Mois affiché à gauche
|
$focusMonth = $months[0];
|
||||||
|
|
||||||
// 1. Initialisation des cibles par défaut
|
|
||||||
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
|
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
|
||||||
|
|
||||||
// 2. On reconstruit la liste complète $allTargets
|
|
||||||
// On part des cibles par défaut
|
|
||||||
$allTargets = $targetsOrder;
|
$allTargets = $targetsOrder;
|
||||||
|
|
||||||
// On ajoute les cibles personnalisées trouvées dans les catégories actives
|
|
||||||
foreach($cats as $c) {
|
foreach($cats as $c) {
|
||||||
$t = trim($c['target']);
|
$t = trim($c['target']);
|
||||||
if(!empty($t) && !in_array($t, $allTargets)) {
|
if(!empty($t) && !in_array($t, $allTargets)) {
|
||||||
@@ -301,7 +308,6 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
|
|||||||
}
|
}
|
||||||
$allTargets = array_unique($allTargets);
|
$allTargets = array_unique($allTargets);
|
||||||
|
|
||||||
// 3. Calcul des sommes (Logiciel d'affichage)
|
|
||||||
$summaryData = [];
|
$summaryData = [];
|
||||||
foreach($allTargets as $t) $summaryData[$t] = ['Alex' => 0, 'Laia' => 0];
|
foreach($allTargets as $t) $summaryData[$t] = ['Alex' => 0, 'Laia' => 0];
|
||||||
|
|
||||||
@@ -319,7 +325,6 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
|
|||||||
$summaryData[$t]['Laia'] += $val['amount_laia'];
|
$summaryData[$t]['Laia'] += $val['amount_laia'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupération des totaux pour le footer
|
|
||||||
$grandTotalAlex = 0;
|
$grandTotalAlex = 0;
|
||||||
$grandTotalLaia = 0;
|
$grandTotalLaia = 0;
|
||||||
$grandTotalGlobal = 0;
|
$grandTotalGlobal = 0;
|
||||||
@@ -371,7 +376,7 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach($allTargets as $target):
|
<?php foreach($allTargets as $target):
|
||||||
$tId = md5($target); // ID unique pour le JS
|
$tId = md5($target);
|
||||||
?>
|
?>
|
||||||
<tr id="row_summary_<?= $tId ?>">
|
<tr id="row_summary_<?= $tId ?>">
|
||||||
<td><?= htmlspecialchars($target) ?></td>
|
<td><?= htmlspecialchars($target) ?></td>
|
||||||
@@ -475,17 +480,16 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Fonction pour ouvrir la modale et pré-remplir les valeurs
|
|
||||||
function openEditModal(btn) {
|
function openEditModal(btn) {
|
||||||
const id = btn.getAttribute('data-id');
|
const id = btn.getAttribute('data-id');
|
||||||
const name = btn.getAttribute('data-name');
|
const name = btn.getAttribute('data-name');
|
||||||
const target = btn.getAttribute('data-target');
|
const target = btn.getAttribute('data-target');
|
||||||
const holiday = btn.getAttribute('data-holiday'); // NOUVEAU
|
const holiday = btn.getAttribute('data-holiday');
|
||||||
|
|
||||||
document.getElementById('edit_cat_id').value = id;
|
document.getElementById('edit_cat_id').value = id;
|
||||||
document.getElementById('edit_cat_name').value = name;
|
document.getElementById('edit_cat_name').value = name;
|
||||||
document.getElementById('edit_cat_target').value = target;
|
document.getElementById('edit_cat_target').value = target;
|
||||||
document.getElementById('edit_cat_holiday').value = holiday; // NOUVEAU
|
document.getElementById('edit_cat_holiday').value = holiday;
|
||||||
|
|
||||||
document.getElementById('editCatModal').style.display = 'flex';
|
document.getElementById('editCatModal').style.display = 'flex';
|
||||||
}
|
}
|
||||||
@@ -496,7 +500,6 @@ function openEditModal(btn) {
|
|||||||
const currentYear = <?= $currentYear ?>;
|
const currentYear = <?= $currentYear ?>;
|
||||||
const months = <?= json_encode($months) ?>;
|
const months = <?= json_encode($months) ?>;
|
||||||
|
|
||||||
// --- SALAIRES ---
|
|
||||||
function updateSalary(person, input) {
|
function updateSalary(person, input) {
|
||||||
const row = input.closest('tr');
|
const row = input.closest('tr');
|
||||||
const salary = parseFloat(row.querySelector('[data-field="salary"]').value) || 0;
|
const salary = parseFloat(row.querySelector('[data-field="salary"]').value) || 0;
|
||||||
@@ -506,28 +509,23 @@ function updateSalary(person, input) {
|
|||||||
const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0;
|
const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0;
|
||||||
|
|
||||||
const restant = salary - (mens + frais + ecoP + ecoF);
|
const restant = salary - (mens + frais + ecoP + ecoF);
|
||||||
// Affichage sans décimale
|
|
||||||
document.getElementById('restant_' + person).innerText = Math.round(restant).toLocaleString('fr-FR') + ' €';
|
document.getElementById('restant_' + person).innerText = Math.round(restant).toLocaleString('fr-FR') + ' €';
|
||||||
|
|
||||||
saveData('update_salary_config', { year: currentYear, person: person, field: input.dataset.field, value: input.value });
|
saveData('update_salary_config', { year: currentYear, person: person, field: input.dataset.field, value: input.value });
|
||||||
recalcAllAllocations();
|
recalcAllAllocations();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ALLOCATIONS ---
|
|
||||||
function updateAlloc(month, catId, personField, input) {
|
function updateAlloc(month, catId, personField, input) {
|
||||||
saveData('update_allocation', { month_date: month, cat_id: catId, person: personField, value: input.value || 0 });
|
saveData('update_allocation', { month_date: month, cat_id: catId, person: personField, value: input.value || 0 });
|
||||||
recalcAllAllocations();
|
recalcAllAllocations();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction utilitaire : Retourne le mois en MAJUSCULES (ex: "FÉVRIER")
|
|
||||||
function getMonthName(dateStr) {
|
function getMonthName(dateStr) {
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
return date.toLocaleDateString('fr-FR', { month: 'long' }).toUpperCase();
|
return date.toLocaleDateString('fr-FR', { month: 'long' }).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateMonth() {
|
function duplicateMonth() {
|
||||||
// months[0] = Le mois affiché le plus à gauche (Cible)
|
|
||||||
// months[1] = Le mois juste à droite (Source)
|
|
||||||
const targetDateStr = months[0];
|
const targetDateStr = months[0];
|
||||||
const sourceDateStr = months[1];
|
const sourceDateStr = months[1];
|
||||||
|
|
||||||
@@ -536,23 +534,25 @@ function duplicateMonth() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetName = getMonthName(targetDateStr); // ex: MARS
|
// Formatage propre (ex: "Mars 2026")
|
||||||
const sourceName = getMonthName(sourceDateStr); // ex: FÉVRIER
|
const formatMonth = (d) => {
|
||||||
|
let str = new Date(d).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
};
|
||||||
|
|
||||||
// On utilise des tirets ou étoiles pour attirer l'attention à défaut de gras
|
const sourceName = formatMonth(sourceDateStr);
|
||||||
const message = `Voulez-vous copier les valeurs de ${sourceName} vers ${targetName} ?\n\n` +
|
const targetName = formatMonth(targetDateStr);
|
||||||
`Cela écrasera toutes les valeurs présentes sur ${targetName}.`;
|
|
||||||
|
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}.`;
|
||||||
|
|
||||||
if(!confirm(message)) return;
|
if(!confirm(message)) return;
|
||||||
|
|
||||||
// Le reste du code de copie reste identique...
|
|
||||||
document.querySelectorAll('.inp-alex-' + sourceDateStr).forEach(sourceInput => {
|
document.querySelectorAll('.inp-alex-' + sourceDateStr).forEach(sourceInput => {
|
||||||
const catIdMatch = sourceInput.getAttribute('onchange').match(/, (\d+),/);
|
const catIdMatch = sourceInput.getAttribute('onchange').match(/, (\d+),/);
|
||||||
if(!catIdMatch) return;
|
if(!catIdMatch) return;
|
||||||
const catId = catIdMatch[1];
|
const catId = catIdMatch[1];
|
||||||
const row = sourceInput.closest('tr');
|
const row = sourceInput.closest('tr');
|
||||||
|
|
||||||
// Copie Alex
|
|
||||||
const valAlex = sourceInput.value;
|
const valAlex = sourceInput.value;
|
||||||
const targetAlex = row.querySelector('.inp-alex-' + targetDateStr);
|
const targetAlex = row.querySelector('.inp-alex-' + targetDateStr);
|
||||||
if(targetAlex) {
|
if(targetAlex) {
|
||||||
@@ -560,7 +560,6 @@ function duplicateMonth() {
|
|||||||
updateAlloc(targetDateStr, catId, 'amount_alex', targetAlex);
|
updateAlloc(targetDateStr, catId, 'amount_alex', targetAlex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copie Laia
|
|
||||||
const valLaia = row.querySelector('.inp-laia-' + sourceDateStr).value;
|
const valLaia = row.querySelector('.inp-laia-' + sourceDateStr).value;
|
||||||
const targetLaia = row.querySelector('.inp-laia-' + targetDateStr);
|
const targetLaia = row.querySelector('.inp-laia-' + targetDateStr);
|
||||||
if(targetLaia) {
|
if(targetLaia) {
|
||||||
@@ -578,16 +577,10 @@ function recalcAllAllocations() {
|
|||||||
let sumAlex = 0;
|
let sumAlex = 0;
|
||||||
let sumLaia = 0;
|
let sumLaia = 0;
|
||||||
|
|
||||||
// Boucle sur colonne Alex
|
|
||||||
document.querySelectorAll('.inp-alex-' + m).forEach(inp => {
|
document.querySelectorAll('.inp-alex-' + m).forEach(inp => {
|
||||||
const val = parseFloat(inp.value) || 0;
|
const val = parseFloat(inp.value) || 0;
|
||||||
|
if (!inp.classList.contains('ignore-calc')) sumAlex += val;
|
||||||
|
|
||||||
// 1. On ajoute au TOTAL COLONNE seulement si ce n'est PAS une ligne indicative
|
|
||||||
if (!inp.classList.contains('ignore-calc')) {
|
|
||||||
sumAlex += val;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Calcul du GLOBAL LIGNE (On le fait toujours, même pour les indicatifs)
|
|
||||||
const row = inp.closest('tr');
|
const row = inp.closest('tr');
|
||||||
const laiaVal = parseFloat(row.querySelector('.inp-laia-' + m).value) || 0;
|
const laiaVal = parseFloat(row.querySelector('.inp-laia-' + m).value) || 0;
|
||||||
const globalSum = val + laiaVal;
|
const globalSum = val + laiaVal;
|
||||||
@@ -600,20 +593,15 @@ function recalcAllAllocations() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Boucle sur colonne Laia (Juste pour le total colonne)
|
|
||||||
document.querySelectorAll('.inp-laia-' + m).forEach(inp => {
|
document.querySelectorAll('.inp-laia-' + m).forEach(inp => {
|
||||||
const val = parseFloat(inp.value) || 0;
|
const val = parseFloat(inp.value) || 0;
|
||||||
if (!inp.classList.contains('ignore-calc')) {
|
if (!inp.classList.contains('ignore-calc')) sumLaia += val;
|
||||||
sumLaia += val;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Totaux
|
|
||||||
document.getElementById('total_alex_' + m).innerText = Math.round(sumAlex) + ' €';
|
document.getElementById('total_alex_' + m).innerText = Math.round(sumAlex) + ' €';
|
||||||
document.getElementById('total_laia_' + m).innerText = Math.round(sumLaia) + ' €';
|
document.getElementById('total_laia_' + m).innerText = Math.round(sumLaia) + ' €';
|
||||||
document.getElementById('total_global_' + m).innerText = Math.round(sumAlex + sumLaia) + ' €';
|
document.getElementById('total_global_' + m).innerText = Math.round(sumAlex + sumLaia) + ' €';
|
||||||
|
|
||||||
// Restants
|
|
||||||
const restAlex = budgetAlex - sumAlex;
|
const restAlex = budgetAlex - sumAlex;
|
||||||
const restLaia = budgetLaia - sumLaia;
|
const restLaia = budgetLaia - sumLaia;
|
||||||
|
|
||||||
@@ -630,27 +618,12 @@ function recalcAllAllocations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateSummaryTable() {
|
function updateSummaryTable() {
|
||||||
// 1. On identifie le mois focus (colonne de gauche)
|
|
||||||
const focusMonth = months[0];
|
const focusMonth = months[0];
|
||||||
|
|
||||||
// 2. Objet pour stocker les sommes par cible
|
|
||||||
// Structure : { 'md5_target': { alex: 0, laia: 0 } }
|
|
||||||
const sums = {};
|
const sums = {};
|
||||||
|
|
||||||
// 3. On parcourt les inputs ALEX du mois focus
|
|
||||||
document.querySelectorAll('.inp-alex-' + focusMonth).forEach(inp => {
|
|
||||||
const val = parseFloat(inp.value) || 0;
|
|
||||||
const target = inp.getAttribute('data-target'); // Récupéré de l'étape 1
|
|
||||||
|
|
||||||
if (target) { }
|
|
||||||
});
|
|
||||||
|
|
||||||
// On reset les totaux
|
|
||||||
let grandTotalAlex = 0;
|
let grandTotalAlex = 0;
|
||||||
let grandTotalLaia = 0;
|
let grandTotalLaia = 0;
|
||||||
const dataByTarget = {};
|
const dataByTarget = {};
|
||||||
|
|
||||||
// Calcul Alex
|
|
||||||
document.querySelectorAll('.inp-alex-' + focusMonth).forEach(inp => {
|
document.querySelectorAll('.inp-alex-' + focusMonth).forEach(inp => {
|
||||||
const target = inp.getAttribute('data-target');
|
const target = inp.getAttribute('data-target');
|
||||||
if(target) {
|
if(target) {
|
||||||
@@ -659,7 +632,6 @@ function updateSummaryTable() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calcul Laia
|
|
||||||
document.querySelectorAll('.inp-laia-' + focusMonth).forEach(inp => {
|
document.querySelectorAll('.inp-laia-' + focusMonth).forEach(inp => {
|
||||||
const target = inp.getAttribute('data-target');
|
const target = inp.getAttribute('data-target');
|
||||||
if(target) {
|
if(target) {
|
||||||
@@ -668,31 +640,23 @@ function updateSummaryTable() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mise à jour du tableau
|
|
||||||
// On parcourt toutes les lignes du tbody du récap
|
|
||||||
const tbody = document.querySelector('.recap-table tbody');
|
const tbody = document.querySelector('.recap-table tbody');
|
||||||
if(tbody) {
|
if(tbody) {
|
||||||
Array.from(tbody.rows).forEach(row => {
|
Array.from(tbody.rows).forEach(row => {
|
||||||
const targetName = row.cells[0].innerText.trim(); // "vers L.Pol"
|
const targetName = row.cells[0].innerText.trim();
|
||||||
|
|
||||||
const alexSum = dataByTarget[targetName] ? dataByTarget[targetName].alex : 0;
|
const alexSum = dataByTarget[targetName] ? dataByTarget[targetName].alex : 0;
|
||||||
const laiaSum = dataByTarget[targetName] ? dataByTarget[targetName].laia : 0;
|
const laiaSum = dataByTarget[targetName] ? dataByTarget[targetName].laia : 0;
|
||||||
const globalSum = alexSum + laiaSum;
|
const globalSum = alexSum + laiaSum;
|
||||||
|
|
||||||
// Mise à jour des cellules (Index 1=Alex, 2=Laia, 3=Global)
|
|
||||||
row.cells[1].innerText = Math.round(alexSum).toLocaleString('fr-FR') + ' €';
|
row.cells[1].innerText = Math.round(alexSum).toLocaleString('fr-FR') + ' €';
|
||||||
row.cells[2].innerText = Math.round(laiaSum).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[3].innerText = Math.round(globalSum).toLocaleString('fr-FR') + ' €';
|
||||||
|
|
||||||
// Gestion visuelle : Masquer la ligne si tout est à 0 (Optionnel)
|
|
||||||
// row.style.display = globalSum === 0 ? 'none' : '';
|
|
||||||
|
|
||||||
grandTotalAlex += alexSum;
|
grandTotalAlex += alexSum;
|
||||||
grandTotalLaia += laiaSum;
|
grandTotalLaia += laiaSum;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mise à jour du Footer Grand Total
|
|
||||||
document.getElementById('grand_total_alex').innerText = Math.round(grandTotalAlex).toLocaleString('fr-FR') + ' €';
|
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_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_global').innerText = Math.round(grandTotalAlex + grandTotalLaia).toLocaleString('fr-FR') + ' €';
|
||||||
@@ -709,41 +673,20 @@ document.addEventListener('DOMContentLoaded', recalcAllAllocations);
|
|||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Gestion du clic sur mobile pour afficher les actions
|
|
||||||
if (window.innerWidth <= 768) {
|
if (window.innerWidth <= 768) {
|
||||||
|
|
||||||
// 1. On écoute les clics sur tout le document
|
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
|
|
||||||
// On récupère la cellule "sticky" (catégorie) si elle a été cliquée
|
|
||||||
const clickedCell = e.target.closest('.col-sticky');
|
const clickedCell = e.target.closest('.col-sticky');
|
||||||
|
|
||||||
// Si on a cliqué sur une cellule de catégorie...
|
|
||||||
if (clickedCell) {
|
if (clickedCell) {
|
||||||
const actionsDiv = clickedCell.querySelector('.row-actions');
|
const actionsDiv = clickedCell.querySelector('.row-actions');
|
||||||
|
|
||||||
// Si on a cliqué DIRECTEMENT sur un bouton d'action (crayon/poubelle), on ne fait rien
|
|
||||||
// (on laisse le bouton faire son travail : ouvrir la modale ou supprimer)
|
|
||||||
if (e.target.closest('.btn-icon-action')) return;
|
if (e.target.closest('.btn-icon-action')) return;
|
||||||
|
|
||||||
// Sinon, on Toggle (Affiche/Cache) le menu de CETTE ligne
|
|
||||||
if (actionsDiv) {
|
if (actionsDiv) {
|
||||||
// Est-ce qu'il est déjà ouvert ?
|
|
||||||
const isOpen = actionsDiv.classList.contains('show-actions');
|
const isOpen = actionsDiv.classList.contains('show-actions');
|
||||||
|
|
||||||
// D'abord, on ferme TOUS les autres menus ouverts pour ne pas en avoir partout
|
|
||||||
document.querySelectorAll('.row-actions.show-actions').forEach(el => {
|
document.querySelectorAll('.row-actions.show-actions').forEach(el => {
|
||||||
el.classList.remove('show-actions');
|
el.classList.remove('show-actions');
|
||||||
});
|
});
|
||||||
|
if (!isOpen) actionsDiv.classList.add('show-actions');
|
||||||
// Si c'était fermé, on l'ouvre
|
|
||||||
if (!isOpen) {
|
|
||||||
actionsDiv.classList.add('show-actions');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// Si on a cliqué AILLEURS (pas sur une catéogrie), on ferme tout
|
|
||||||
document.querySelectorAll('.row-actions.show-actions').forEach(el => {
|
document.querySelectorAll('.row-actions.show-actions').forEach(el => {
|
||||||
el.classList.remove('show-actions');
|
el.classList.remove('show-actions');
|
||||||
});
|
});
|
||||||
@@ -759,9 +702,6 @@ function validateTransfers(person, month) {
|
|||||||
formData.append('action', 'validate_transfers');
|
formData.append('action', 'validate_transfers');
|
||||||
formData.append('person', person);
|
formData.append('person', person);
|
||||||
formData.append('month_date', month);
|
formData.append('month_date', month);
|
||||||
|
|
||||||
// On récupère aussi les données calculées du tableau pour les envoyer au backend
|
|
||||||
// Mais pour plus de sécurité, le backend recalculera tout lui-même.
|
|
||||||
|
|
||||||
fetch('/modules/budget/includes/api/save-budget.php', {
|
fetch('/modules/budget/includes/api/save-budget.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -769,22 +709,18 @@ function validateTransfers(person, month) {
|
|||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if(data.success) {
|
if(data.success) window.location.reload();
|
||||||
window.location.reload();
|
else alert("Erreur: " + data.error);
|
||||||
} else {
|
|
||||||
alert("Erreur: " + data.error);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(e => alert("Erreur technique"));
|
.catch(e => alert("Erreur technique"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SAUVEGARDE GÉNÉRIQUE DES NOTES ---
|
|
||||||
function saveGenericNote(noteType, refId, content) {
|
function saveGenericNote(noteType, refId, content) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('action', 'save_note');
|
formData.append('action', 'save_note');
|
||||||
formData.append('note_type', noteType); // ex: 'budget_prev'
|
formData.append('note_type', noteType);
|
||||||
formData.append('reference_id', refId); // ex: '2026-03-01'
|
formData.append('reference_id', refId);
|
||||||
formData.append('content', content); // Le texte
|
formData.append('content', content);
|
||||||
|
|
||||||
fetch('/modules/budget/includes/api/save-budget.php', {
|
fetch('/modules/budget/includes/api/save-budget.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
+137
-119
@@ -1,27 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
// modules/budget/views/epargne.php
|
// modules/budget/views/epargne.php
|
||||||
|
|
||||||
// 1. Gestion des propriétaires à afficher
|
|
||||||
$requestedOwner = $_GET['owner'] ?? 'Nens';
|
$requestedOwner = $_GET['owner'] ?? 'Nens';
|
||||||
|
|
||||||
// Si l'onglet est "Nens", on affiche Pol et Pep. Sinon, on affiche juste la personne demandée.
|
|
||||||
$ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOwner];
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Cacher les flèches haut/bas des champs de type number */
|
|
||||||
input[type="number"].no-spinners::-webkit-inner-spin-button,
|
|
||||||
input[type="number"].no-spinners::-webkit-outer-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
input[type="number"].no-spinners {
|
|
||||||
-moz-appearance: textfield; /* Firefox */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="budget-view">
|
<div class="budget-view">
|
||||||
|
|
||||||
<div class="view-header">
|
<div class="view-header">
|
||||||
<div class="owner-tabs">
|
<div class="owner-tabs">
|
||||||
<a href="?tab=epargne&owner=Alex" class="owner-tab <?= $requestedOwner === 'Alex' ? 'active' : '' ?>">Alex</a>
|
<a href="?tab=epargne&owner=Alex" class="owner-tab <?= $requestedOwner === 'Alex' ? 'active' : '' ?>">Alex</a>
|
||||||
@@ -31,7 +25,6 @@ input[type="number"].no-spinners {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php foreach ($ownersToDisplay as $currentOwner):
|
<?php foreach ($ownersToDisplay as $currentOwner):
|
||||||
// --- Récupération des données pour $currentOwner ---
|
|
||||||
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
|
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
|
||||||
$stmt->execute([$currentOwner]);
|
$stmt->execute([$currentOwner]);
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
@@ -48,8 +41,14 @@ input[type="number"].no-spinners {
|
|||||||
if (!in_array($m, $months)) $months[] = $m;
|
if (!in_array($m, $months)) $months[] = $m;
|
||||||
if ($cat !== 'TOTAL_BANQUE' && !in_array($cat, $allCategories)) $allCategories[] = $cat;
|
if ($cat !== 'TOTAL_BANQUE' && !in_array($cat, $allCategories)) $allCategories[] = $cat;
|
||||||
}
|
}
|
||||||
$months = array_slice($months, 0, 7); // 7 derniers mois
|
$months = array_slice($months, 0, 7);
|
||||||
sort($allCategories);
|
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="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
|
||||||
@@ -88,14 +87,22 @@ input[type="number"].no-spinners {
|
|||||||
<?php foreach ($months as $month): ?>
|
<?php foreach ($months as $month): ?>
|
||||||
<th>
|
<th>
|
||||||
<div class="month-header-container">
|
<div class="month-header-container">
|
||||||
<span class="month-name"><?= date('M Y', strtotime($month)) ?></span>
|
<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">
|
<div class="month-actions">
|
||||||
<button class="btn-icon-small" title="Modifier"
|
<button class="btn-icon-small" title="Modifier avec Modale"
|
||||||
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
|
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
|
||||||
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", JSON.parse(this.getAttribute("data-json")))'>
|
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", JSON.parse(this.getAttribute("data-json")))'>
|
||||||
✏️
|
✏️
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon-small" title="Supprimer"
|
<button class="btn-icon-small" title="Supprimer tout le mois"
|
||||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
|
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
|
||||||
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
|
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
|
||||||
🗑️
|
🗑️
|
||||||
@@ -108,10 +115,20 @@ input[type="number"].no-spinners {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="row-total">
|
<tr class="row-total">
|
||||||
<td class="sticky-col"><strong>Total</strong></td>
|
<td class="sticky-col"><strong>Total Banque</strong></td>
|
||||||
<?php foreach ($months as $month): ?>
|
<?php foreach ($months as $month):
|
||||||
<td class="text-center font-bold" style="color: #2563eb;">
|
$val = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
||||||
<?= number_format($data[$month]['TOTAL_BANQUE'] ?? 0, 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>
|
</td>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -119,17 +136,19 @@ input[type="number"].no-spinners {
|
|||||||
<?php foreach ($allCategories as $cat): ?>
|
<?php foreach ($allCategories as $cat): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="sticky-col"><?= htmlspecialchars($cat) ?></td>
|
<td class="sticky-col"><?= htmlspecialchars($cat) ?></td>
|
||||||
<?php foreach ($months as $month): $amount = $data[$month][$cat] ?? 0; ?>
|
<?php foreach ($months as $month):
|
||||||
<td class="text-center text-muted">
|
$amount = $data[$month][$cat] ?? 0;
|
||||||
<?php if ($amount != 0): ?>
|
?>
|
||||||
<div class="cell-content">
|
<td class="text-center" style="padding:4px;">
|
||||||
<span style="color:#1e293b; font-weight:500;"><?= number_format($amount, 0, ',', ' ') ?> €</span>
|
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
||||||
<button class="btn-cell-delete"
|
<input type="number" step="0.01"
|
||||||
onclick="deleteSavingsEntry('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>')">
|
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $currentOwner ?>-<?= $month ?>"
|
||||||
×
|
style="width: 70px;"
|
||||||
</button>
|
value="<?= $amount != 0 ? round($amount) : '' ?>"
|
||||||
</div>
|
placeholder="-"
|
||||||
<?php else: ?> - <?php endif; ?>
|
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>', this)">
|
||||||
|
<span style="color:var(--text-muted); font-size:0.8rem;">€</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -140,11 +159,10 @@ input[type="number"].no-spinners {
|
|||||||
<?php foreach ($months as $month):
|
<?php foreach ($months as $month):
|
||||||
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
|
||||||
$sum = 0;
|
$sum = 0;
|
||||||
// Le calcul reste identique : Total de la banque - Somme des enveloppes
|
|
||||||
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
||||||
$extra = $total - $sum;
|
$extra = $total - $sum;
|
||||||
?>
|
?>
|
||||||
<td class="text-center font-bold" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>">
|
<td class="text-center font-bold" id="extra_<?= $currentOwner ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
|
||||||
<?= number_format($extra, 0, ',', ' ') ?> €
|
<?= number_format($extra, 0, ',', ' ') ?> €
|
||||||
</td>
|
</td>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -159,18 +177,19 @@ input[type="number"].no-spinners {
|
|||||||
<div id="savingsModal" class="pf-modal">
|
<div id="savingsModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 600px; width: 95%;">
|
<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;">
|
<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 le mois</h3>
|
<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;">×</button>
|
<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;">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action="/modules/budget/includes/api/save-savings.php" method="POST" id="savingsForm">
|
<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="owner" id="sav_owner">
|
||||||
<input type="hidden" name="redirect_tab" id="redirect_tab" value="<?= htmlspecialchars($requestedOwner) ?>">
|
<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 style="display:flex; gap:15px; margin-bottom:20px;">
|
||||||
<div class="form-group" style="flex:1; margin:0;">
|
<div class="form-group" style="flex:1; margin:0;">
|
||||||
<label class="pf-label">Mois concerné</label>
|
<label class="pf-label">Mois concerné</label>
|
||||||
<input type="date" name="month_date" id="sav_date" required class="pf-input">
|
<input type="month" id="sav_month" required class="pf-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="flex:1; margin:0;">
|
<div class="form-group" style="flex:1; margin:0;">
|
||||||
@@ -209,14 +228,52 @@ input[type="number"].no-spinners {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// --- LOGIQUE MODALE EPARGNE ---
|
// ============================================================================
|
||||||
|
// 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 = '') {
|
function addCustomEpargneLine(catName = '', amount = '') {
|
||||||
const container = document.getElementById('linesContainer');
|
const container = document.getElementById('linesContainer');
|
||||||
// Si amount est vide, on met '0.00', sinon on formate
|
|
||||||
const baseAmount = (amount !== '' && amount !== null) ? parseFloat(amount).toFixed(2) : '0.00';
|
const baseAmount = (amount !== '' && amount !== null) ? parseFloat(amount).toFixed(2) : '0.00';
|
||||||
|
|
||||||
// Nom du champ input pour le serveur
|
|
||||||
const inputName = catName ? `values[${catName}]` : '';
|
const inputName = catName ? `values[${catName}]` : '';
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
@@ -247,13 +304,8 @@ function updateCustomFieldName(inputElement) {
|
|||||||
const line = inputElement.closest('.ventilation-line');
|
const line = inputElement.closest('.ventilation-line');
|
||||||
const finalInput = line.querySelector('.final-amount');
|
const finalInput = line.querySelector('.final-amount');
|
||||||
const newName = inputElement.value.trim();
|
const newName = inputElement.value.trim();
|
||||||
|
if (newName) finalInput.name = `values[${newName}]`;
|
||||||
// Mise à jour dynamique de l'attribut name pour que PHP le reçoive correctement
|
else finalInput.name = '';
|
||||||
if (newName) {
|
|
||||||
finalInput.name = `values[${newName}]`;
|
|
||||||
} else {
|
|
||||||
finalInput.name = ''; // Si vide, ne sera pas envoyé
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recalculateCustomLine(inputElement) {
|
function recalculateCustomLine(inputElement) {
|
||||||
@@ -268,42 +320,32 @@ function recalculateCustomLine(inputElement) {
|
|||||||
finalInput.value = (base + adj).toFixed(2);
|
finalInput.value = (base + adj).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction d'ouverture pour ÉDITION
|
|
||||||
function editCustomSavingsMonth(monthDate, owner, rowData) {
|
function editCustomSavingsMonth(monthDate, owner, rowData) {
|
||||||
document.getElementById('sav_owner').value = owner;
|
document.getElementById('sav_owner').value = owner;
|
||||||
document.getElementById('sav_date').value = monthDate;
|
const ym = monthDate.substring(0, 7);
|
||||||
|
document.getElementById('sav_month').value = ym;
|
||||||
|
|
||||||
// Formatage date pour le titre
|
|
||||||
const dateObj = new Date(monthDate);
|
const dateObj = new Date(monthDate);
|
||||||
const monthName = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
|
const monthName = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
|
||||||
document.getElementById('savingsModalTitle').innerText = "Modifier : " + monthName + " (" + owner + ")";
|
document.getElementById('savingsModalTitle').innerText = "Modifier : " + monthName + " (" + owner + ")";
|
||||||
|
|
||||||
// Remplissage Total Banque
|
|
||||||
document.getElementById('sav_total').value = rowData['TOTAL_BANQUE'] || '';
|
document.getElementById('sav_total').value = rowData['TOTAL_BANQUE'] || '';
|
||||||
|
|
||||||
// Remplissage des lignes
|
|
||||||
const container = document.getElementById('linesContainer');
|
const container = document.getElementById('linesContainer');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
// On parcourt les données JSON reçues
|
|
||||||
for (const [cat, val] of Object.entries(rowData)) {
|
for (const [cat, val] of Object.entries(rowData)) {
|
||||||
if (cat !== 'TOTAL_BANQUE') {
|
if (cat !== 'TOTAL_BANQUE') addCustomEpargneLine(cat, val);
|
||||||
addCustomEpargneLine(cat, val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si aucune ligne de détail, on en ajoute une vide
|
if (container.children.length === 0) addCustomEpargneLine();
|
||||||
if (container.children.length === 0) {
|
|
||||||
addCustomEpargneLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('savingsModal').style.display = 'flex';
|
document.getElementById('savingsModal').style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction d'ouverture pour AJOUT (Nouveau mois)
|
|
||||||
function openCustomSavingsModal(owner) {
|
function openCustomSavingsModal(owner) {
|
||||||
document.getElementById('sav_owner').value = owner;
|
document.getElementById('sav_owner').value = owner;
|
||||||
document.getElementById('sav_date').value = '';
|
document.getElementById('sav_month').value = '';
|
||||||
document.getElementById('sav_total').value = '';
|
document.getElementById('sav_total').value = '';
|
||||||
|
|
||||||
document.getElementById('savingsModalTitle').innerText = "Saisir un mois (" + owner + ")";
|
document.getElementById('savingsModalTitle').innerText = "Saisir un mois (" + owner + ")";
|
||||||
@@ -315,19 +357,20 @@ function openCustomSavingsModal(owner) {
|
|||||||
document.getElementById('savingsModal').style.display = 'flex';
|
document.getElementById('savingsModal').style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fermeture modale au clic extérieur
|
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
const modal = document.getElementById('savingsModal');
|
const modal = document.getElementById('savingsModal');
|
||||||
if (event.target == modal) {
|
if (event.target == modal) modal.style.display = 'none';
|
||||||
modal.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SOUMISSION DU FORMULAIRE (AJAX) ---
|
|
||||||
const savingsForm = document.getElementById('savingsForm');
|
const savingsForm = document.getElementById('savingsForm');
|
||||||
if (savingsForm) {
|
if (savingsForm) {
|
||||||
savingsForm.addEventListener('submit', function(e) {
|
savingsForm.addEventListener('submit', function(e) {
|
||||||
e.preventDefault(); // On bloque le rechargement standard
|
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 submitBtn = this.querySelector('button[type="submit"]');
|
||||||
const originalText = submitBtn.innerText;
|
const originalText = submitBtn.innerText;
|
||||||
@@ -336,15 +379,9 @@ if (savingsForm) {
|
|||||||
|
|
||||||
const formData = new FormData(this);
|
const formData = new FormData(this);
|
||||||
|
|
||||||
fetch(this.action, {
|
fetch(this.action, { method: 'POST', body: formData })
|
||||||
method: 'POST',
|
.then(response => response.text())
|
||||||
body: formData
|
.then(text => { window.location.reload(); })
|
||||||
})
|
|
||||||
.then(response => response.text()) // On lit la réponse texte (parfois PHP renvoie du HTML ou vide)
|
|
||||||
.then(text => {
|
|
||||||
// Rechargement forcé pour voir les modifications
|
|
||||||
window.location.reload();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Erreur:", error);
|
console.error("Erreur:", error);
|
||||||
alert("Une erreur technique est survenue.");
|
alert("Une erreur technique est survenue.");
|
||||||
@@ -354,56 +391,40 @@ if (savingsForm) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// ACTIONS DE SUPPRESSION / DUPLICATION
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
function deleteEntireMonth(monthDate, owner) {
|
function deleteEntireMonth(monthDate, owner) {
|
||||||
if (!confirm(`Supprimer TOUT le mois de ${monthDate} pour ${owner} ?`)) return;
|
if (!confirm(`Supprimer TOUT le mois de ${monthDate} pour ${owner} ?`)) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("action", "delete_month_global"); // Action gérée par save-savings.php
|
formData.append("action", "delete_month_global");
|
||||||
formData.append("month_date", monthDate);
|
formData.append("month_date", monthDate);
|
||||||
formData.append("owner", owner);
|
formData.append("owner", owner);
|
||||||
|
fetch("/modules/budget/includes/api/save-savings.php", { method: "POST", body: formData })
|
||||||
fetch("/modules/budget/includes/api/save-savings.php", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
})
|
|
||||||
.then(() => window.location.reload())
|
.then(() => window.location.reload())
|
||||||
.catch(err => alert("Erreur lors de la suppression."));
|
.catch(err => alert("Erreur lors de la suppression."));
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSavingsEntry(monthDate, category, owner) {
|
|
||||||
if (!confirm(`Supprimer la ligne "${category}" ?`)) return;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("action", "delete_entry");
|
|
||||||
formData.append("month_date", monthDate);
|
|
||||||
formData.append("category", category);
|
|
||||||
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 de la ligne."));
|
|
||||||
}
|
|
||||||
|
|
||||||
function duplicateLastMonth(lastMonthDate, owner) {
|
function duplicateLastMonth(lastMonthDate, owner) {
|
||||||
// Calcul du mois suivant
|
|
||||||
let dateObj = new Date(lastMonthDate);
|
let dateObj = new Date(lastMonthDate);
|
||||||
dateObj.setMonth(dateObj.getMonth() + 1);
|
dateObj.setMonth(dateObj.getMonth() + 1);
|
||||||
// Astuce pour gérer les fuseaux horaires et garder YYYY-MM-01
|
|
||||||
let year = dateObj.getFullYear();
|
let year = dateObj.getFullYear();
|
||||||
let month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
let month = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||||
let nextMonthStr = `${year}-${month}-01`;
|
let nextMonthStr = `${year}-${month}-01`;
|
||||||
|
|
||||||
let newTotal = prompt(
|
const formatMonth = (d) => {
|
||||||
`Dupliquer les données de ${lastMonthDate} vers ${nextMonthStr} ?\n\nNouveau TOTAL en banque (€) :`,
|
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() !== "") {
|
if (newTotal !== null && newTotal.trim() !== "") {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -413,10 +434,7 @@ function duplicateLastMonth(lastMonthDate, owner) {
|
|||||||
formData.append("new_total", newTotal);
|
formData.append("new_total", newTotal);
|
||||||
formData.append("owner", owner);
|
formData.append("owner", owner);
|
||||||
|
|
||||||
fetch("/modules/budget/includes/api/save-savings.php", {
|
fetch("/modules/budget/includes/api/save-savings.php", { method: "POST", body: formData })
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
})
|
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(d => {
|
.then(d => {
|
||||||
if (d.success) window.location.reload();
|
if (d.success) window.location.reload();
|
||||||
|
|||||||
Reference in New Issue
Block a user