@@ -1,15 +1,18 @@
|
||||
<?php
|
||||
// modules/budget/views/budget_prev.php
|
||||
|
||||
// 1. Chargement dynamique des parents
|
||||
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color FROM pf_people WHERE role = 'parent' ORDER BY id ASC");
|
||||
// 1. Chargement dynamique des ADULTES de la famille
|
||||
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color FROM pf_people WHERE role NOT IN ('enfant', 'nounou') AND is_active = 1 ORDER BY id ASC");
|
||||
$budgetParents = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$p1_name = $budgetParents[0]['name'] ?? 'Parent 1';
|
||||
$p2_name = $budgetParents[1]['name'] ?? 'Parent 2';
|
||||
// Sécurité : au cas où aucun adulte n'est trouvé, on évite un crash
|
||||
if (empty($budgetParents)) {
|
||||
$budgetParents[] = ['id' => 0, 'name' => 'Utilisateur', 'color' => '#0891b2'];
|
||||
}
|
||||
|
||||
$currentYear = date('Y');
|
||||
|
||||
// Cartographie dynamique
|
||||
// Cartographie dynamique (Supporte 1, 2 ou N adultes)
|
||||
$parentMapping = [];
|
||||
foreach ($budgetParents as $index => $parent) {
|
||||
$num = $index + 1;
|
||||
@@ -21,6 +24,10 @@ foreach ($budgetParents as $index => $parent) {
|
||||
];
|
||||
}
|
||||
|
||||
// 1.5 Récupération des VRAIS comptes bancaires créés dans les paramètres
|
||||
$stmtAccounts = $pdo->query("SELECT name FROM pf_bank_accounts ORDER BY is_default DESC, name ASC");
|
||||
$bankAccounts = $stmtAccounts->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// 2. Récupération Config Salaires
|
||||
$salaryConfig = [];
|
||||
$stmt = $pdo->prepare("SELECT * FROM pf_salary_config WHERE year = ?");
|
||||
@@ -29,7 +36,7 @@ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$salaryConfig[$row['person']] = $row;
|
||||
}
|
||||
|
||||
// Sécurité profils de salaire vides
|
||||
// Sécurité profils de salaire vides (Garantit l'affichage à 0 même si non configuré)
|
||||
foreach ($parentMapping as $map) {
|
||||
if (!isset($salaryConfig[$map['name']])) {
|
||||
$salaryConfig[$map['name']] = ['salary'=>0, 'mensualite'=>0, 'frais_func'=>0, 'eco_perso'=>0, 'eco_family'=>0];
|
||||
@@ -103,21 +110,28 @@ function getTranslatedMonthName($dateString) {
|
||||
return tr('month_' . $m) . ' ' . $y;
|
||||
}
|
||||
|
||||
// --- 🧠 PREPARATION DATA TRICOUNT / AVANCES ---
|
||||
// --- 🧠 PREPARATION DATA TRICOUNT / AVANCES (Dynamique) ---
|
||||
$stmtAdvancesList = $pdo->query("SELECT * FROM pf_advances WHERE is_resolved = 0 ORDER BY advance_date DESC");
|
||||
$activeAdvances = $stmtAdvancesList->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$advTotal = [$p1_name => 0, $p2_name => 0];
|
||||
$livretTotal = [$p1_name => 0, $p2_name => 0];
|
||||
$advTotal = [];
|
||||
$livretTotal = [];
|
||||
$labelsCC = [];
|
||||
$labelsLivret = [];
|
||||
|
||||
$labelsCC = [$p1_name => [], $p2_name => []];
|
||||
$labelsLivret = [$p1_name => [], $p2_name => []];
|
||||
foreach ($parentMapping as $map) {
|
||||
$advTotal[$map['name']] = 0;
|
||||
$livretTotal[$map['name']] = 0;
|
||||
$labelsCC[$map['name']] = [];
|
||||
$labelsLivret[$map['name']] = [];
|
||||
}
|
||||
|
||||
foreach ($activeAdvances as $adv) {
|
||||
$p = $adv['payer'];
|
||||
$amt = (float)$adv['amount'];
|
||||
$labelStr = htmlspecialchars($adv['description']) . ' (' . number_format($amt, 0, ',', ' ') . '€)';
|
||||
|
||||
// Si un ancien nom traîne en base, on l'initialise
|
||||
if (!isset($advTotal[$p])) {
|
||||
$advTotal[$p] = 0; $livretTotal[$p] = 0;
|
||||
$labelsCC[$p] = []; $labelsLivret[$p] = [];
|
||||
@@ -132,10 +146,16 @@ foreach ($activeAdvances as $adv) {
|
||||
}
|
||||
}
|
||||
|
||||
$balanceDiff = abs(($advTotal[$p1_name] ?? 0) - ($advTotal[$p2_name] ?? 0));
|
||||
// Calcul de la dette croisée (Valable surtout si 2 parents)
|
||||
$balanceDiff = 0;
|
||||
$owedTo = '';
|
||||
if (($advTotal[$p1_name] ?? 0) > ($advTotal[$p2_name] ?? 0)) $owedTo = $p1_name;
|
||||
elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_name;
|
||||
if (count($parentMapping) >= 2) {
|
||||
$p1 = $parentMapping[0]['name'];
|
||||
$p2 = $parentMapping[1]['name'];
|
||||
$balanceDiff = abs(($advTotal[$p1] ?? 0) - ($advTotal[$p2] ?? 0));
|
||||
if (($advTotal[$p1] ?? 0) > ($advTotal[$p2] ?? 0)) $owedTo = $p1;
|
||||
elseif (($advTotal[$p2] ?? 0) > ($advTotal[$p1] ?? 0)) $owedTo = $p2;
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -260,10 +280,11 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
$isIndicative = (strpos($cat['name'], 'Eco P') === 0);
|
||||
$rowClass = $isIndicative ? 'row-indicative' : '';
|
||||
$inputClass = $isIndicative ? 'ignore-calc' : '';
|
||||
|
||||
|
||||
// Retrocompatibilité pour les anciens noms d'économies hardcodés
|
||||
$catDisplayName = $cat['name'];
|
||||
if ($catDisplayName === 'Eco P1') $catDisplayName = 'Eco ' . $p1_name;
|
||||
if ($catDisplayName === 'Eco P2') $catDisplayName = 'Eco ' . $p2_name;
|
||||
if (isset($parentMapping[0]) && $catDisplayName === 'Eco P1') $catDisplayName = 'Eco ' . $parentMapping[0]['name'];
|
||||
if (isset($parentMapping[1]) && $catDisplayName === 'Eco P2') $catDisplayName = 'Eco ' . $parentMapping[1]['name'];
|
||||
?>
|
||||
<tr class="<?= $rowClass ?>">
|
||||
<td class="col-sticky">
|
||||
@@ -323,6 +344,7 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (count($parentMapping) > 1): ?>
|
||||
<div style="margin: 30px 0; background: var(--bg-panel); padding: 24px; border-radius: var(--radius); border: 1px solid var(--border-light); box-shadow: var(--shadow);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px;">
|
||||
<h3 style="margin: 0; font-size: 1.3rem; font-weight: 800;"><?= tr('bud_adv_title') ?></h3>
|
||||
@@ -332,57 +354,39 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 24px; flex-wrap: wrap;">
|
||||
<div style="flex: 1; min-width: 240px; background: rgba(8, 145, 178, 0.06); border: 1px solid rgba(8, 145, 178, 0.2); padding: 18px; border-radius: 12px;">
|
||||
<div style="font-size: 0.85rem; color: #0891b2; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($p1_name)) ?></div>
|
||||
<?php foreach ($parentMapping as $idx => $map):
|
||||
$pName = $map['name'];
|
||||
$bgClass = ($idx === 0) ? 'rgba(8, 145, 178, 0.06)' : 'rgba(217, 119, 6, 0.06)';
|
||||
$bdClass = ($idx === 0) ? 'rgba(8, 145, 178, 0.2)' : 'rgba(217, 119, 6, 0.2)';
|
||||
$colorMain = ($idx === 0) ? '#0891b2' : '#d97706';
|
||||
$colorDark = ($idx === 0) ? '#164e63' : '#78350f';
|
||||
?>
|
||||
<div style="flex: 1; min-width: 240px; background: <?= $bgClass ?>; border: 1px solid <?= $bdClass ?>; padding: 18px; border-radius: 12px;">
|
||||
<div style="font-size: 0.85rem; color: <?= $colorMain ?>; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($pName)) ?></div>
|
||||
<div style="margin-top: 10px;">
|
||||
<div style="display: flex; align-items: baseline; gap: 6px;">
|
||||
<span style="font-size: 1.5rem; font-weight: 800; color: #164e63; font-family: monospace;"><?= number_format($advTotal[$p1_name] ?? 0, 2, ',', ' ') ?> €</span>
|
||||
<span style="font-size: 1.5rem; font-weight: 800; color: <?= $colorDark ?>; font-family: monospace;"><?= number_format($advTotal[$pName] ?? 0, 2, ',', ' ') ?> €</span>
|
||||
</div>
|
||||
<small style="color: var(--text-muted); font-size: 0.75rem; font-weight: 500;"><?= tr('bud_adv_cc_label') ?></small>
|
||||
<?php if (!empty($labelsCC[$p1_name])): ?>
|
||||
<div style="font-size: 0.75rem; color: #0e7490; margin-top: 6px; line-height: 1.4; font-style: italic;">
|
||||
<?= implode(', ', $labelsCC[$p1_name]) ?>
|
||||
<?php if (!empty($labelsCC[$pName])): ?>
|
||||
<div style="font-size: 0.75rem; color: <?= $colorMain ?>; margin-top: 6px; line-height: 1.4; font-style: italic;">
|
||||
<?= implode(', ', $labelsCC[$pName]) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if(($livretTotal[$p1_name] ?? 0) > 0): ?>
|
||||
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed rgba(8, 145, 178, 0.3);">
|
||||
<div style="font-size: 1.2rem; font-weight: 800; color: #4338ca; font-family: monospace;">+ <?= number_format($livretTotal[$p1_name], 2, ',', ' ') ?> €</div>
|
||||
<small style="color: #4338ca; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
|
||||
<?php if (!empty($labelsLivret[$p1_name])): ?>
|
||||
<div style="font-size: 0.75rem; color: #3730a3; margin-top: 4px; line-height: 1.4;">
|
||||
<?= implode(', ', $labelsLivret[$p1_name]) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div style="flex: 1; min-width: 240px; background: rgba(217, 119, 6, 0.06); border: 1px solid rgba(217, 119, 6, 0.2); padding: 18px; border-radius: 12px;">
|
||||
<div style="font-size: 0.85rem; color: #d97706; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($p2_name)) ?></div>
|
||||
<div style="margin-top: 10px;">
|
||||
<div style="display: flex; align-items: baseline; gap: 6px;">
|
||||
<span style="font-size: 1.5rem; font-weight: 800; color: #78350f; font-family: monospace;"><?= number_format($advTotal[$p2_name] ?? 0, 2, ',', ' ') ?> €</span>
|
||||
</div>
|
||||
<small style="color: var(--text-muted); font-size: 0.75rem; font-weight: 500;"><?= tr('bud_adv_cc_label') ?></small>
|
||||
<?php if (!empty($labelsCC[$p2_name])): ?>
|
||||
<div style="font-size: 0.75rem; color: #b45309; margin-top: 6px; line-height: 1.4; font-style: italic;">
|
||||
<?= implode(', ', $labelsCC[$p2_name]) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if(($livretTotal[$p2_name] ?? 0) > 0): ?>
|
||||
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed rgba(217, 119, 6, 0.3);">
|
||||
<div style="font-size: 1.2rem; font-weight: 800; color: #b45309; font-family: monospace;">+ <?= number_format($livretTotal[$p2_name], 2, ',', ' ') ?> €</div>
|
||||
<small style="color: #b45309; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
|
||||
<?php if (!empty($labelsLivret[$p2_name])): ?>
|
||||
<div style="font-size: 0.75rem; color: #92400e; margin-top: 4px; line-height: 1.4;">
|
||||
<?= implode(', ', $labelsLivret[$p2_name]) ?>
|
||||
<?php if(($livretTotal[$pName] ?? 0) > 0): ?>
|
||||
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed <?= $bdClass ?>;">
|
||||
<div style="font-size: 1.2rem; font-weight: 800; color: <?= $colorDark ?>; font-family: monospace;">+ <?= number_format($livretTotal[$pName], 2, ',', ' ') ?> €</div>
|
||||
<small style="color: <?= $colorDark ?>; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
|
||||
<?php if (!empty($labelsLivret[$pName])): ?>
|
||||
<div style="font-size: 0.75rem; color: <?= $colorDark ?>; margin-top: 4px; line-height: 1.4;">
|
||||
<?= implode(', ', $labelsLivret[$pName]) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div style="flex: 1; min-width: 240px; background: var(--bg-page); border: 1px solid var(--border-light); padding: 18px; border-radius: 12px; display: flex; flex-direction: column; justify-content: center;">
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px;">
|
||||
@@ -411,10 +415,19 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($activeAdvances as $adv): ?>
|
||||
<?php foreach ($activeAdvances as $adv):
|
||||
// Attribution de la couleur selon l'utilisateur
|
||||
$colorP = '#64748b'; // Defaut
|
||||
foreach($parentMapping as $idx => $m) {
|
||||
if($m['name'] === $adv['payer']) {
|
||||
$colorP = ($idx === 0) ? '#0891b2' : '#d97706';
|
||||
break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td style="color: var(--text-muted);"><?= date('d/m/Y', strtotime($adv['advance_date'])) ?></td>
|
||||
<td style="font-weight: 700; color: <?= $adv['payer'] === $p1_name ? '#0891b2' : '#d97706' ?>;"><?= htmlspecialchars($adv['payer']) ?></td>
|
||||
<td style="font-weight: 700; color: <?= $colorP ?>;"><?= htmlspecialchars($adv['payer']) ?></td>
|
||||
<td>
|
||||
<?= htmlspecialchars($adv['description']) ?>
|
||||
<?php if ($adv['from_savings']): ?>
|
||||
@@ -441,11 +454,19 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; // Fin du Tricount conditionnel ?>
|
||||
|
||||
<?php
|
||||
$focusMonth = $months[0];
|
||||
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
|
||||
|
||||
// Génération dynamique des cibles basées UNIQUEMENT sur les comptes bancaires
|
||||
$targetsOrder = [];
|
||||
foreach ($bankAccounts as $accName) {
|
||||
$targetsOrder[] = 'vers ' . $accName;
|
||||
}
|
||||
|
||||
// On ajoute quand même les cibles historiques enregistrées dans les catégories
|
||||
// (pour ne pas casser l'affichage si on supprime un compte plus tard)
|
||||
$allTargets = $targetsOrder;
|
||||
foreach($cats as $c) {
|
||||
$t = trim($c['transfer_dest'] ?? '');
|
||||
@@ -506,33 +527,38 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="addCatModal" class="pf-modal">
|
||||
<div class="pf-modal-content">
|
||||
<div class="prev-header-left">
|
||||
<h3 class="pf-modal-title"><?= tr('bud_prev_new_line_title') ?></h3>
|
||||
<button onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');">×</button>
|
||||
<div id="addCatModal" class="pf-modal" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');">
|
||||
<div class="pf-modal-content" onclick="event.stopPropagation()">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<h3 class="pf-modal-title" style="margin: 0; font-size: 1.25rem;"><?= tr('bud_prev_new_line_title') ?></h3>
|
||||
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer; color: var(--text-muted, #64748b);">×</button>
|
||||
</div>
|
||||
|
||||
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
|
||||
<input type="hidden" name="action" value="add_category">
|
||||
<div class="form-group">
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="pf-label"><?= tr('bud_prev_label_name') ?></label>
|
||||
<input type="text" name="name" class="pf-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?? 'Objectif Mensuel (€)' ?></label>
|
||||
<input type="number" step="1" name="target" class="pf-input" placeholder="Ex: 150">
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?></label>
|
||||
<input type="number" step="1" name="target" class="pf-input" placeholder="<?= tr('bud_prev_target_ph') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?? 'Destination Virement (Optionnel)' ?></label>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?></label>
|
||||
<select name="transfer_dest" class="pf-input">
|
||||
<option value="" selected>-- <?= tr('bud_prev_none') ?? 'Aucune' ?> --</option>
|
||||
<option value="vers L.Pol">vers L.Pol</option>
|
||||
<option value="vers L.Pep">vers L.Pep</option>
|
||||
<option value="vers L.Perso">vers L.Perso</option>
|
||||
<option value="vers commune">vers commune</option>
|
||||
<option value="">-- <?= tr('bud_prev_none') ?> --</option>
|
||||
<?php foreach ($bankAccounts as $accName): ?>
|
||||
<option value="vers <?= htmlspecialchars($accName) ?>"><?= tr('bud_prev_transfer_to') ?> <?= htmlspecialchars($accName) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label>
|
||||
<select name="holiday_id" class="pf-input">
|
||||
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
|
||||
@@ -541,7 +567,8 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 1.5rem;">
|
||||
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn"><?= tr('bud_add_title') ?></button>
|
||||
</div>
|
||||
@@ -549,34 +576,39 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editCatModal" class="pf-modal">
|
||||
<div class="pf-modal-content">
|
||||
<div class="prev-header-left">
|
||||
<h3 class="pf-modal-title"><?= tr('bud_prev_edit_line_title') ?></h3>
|
||||
<button onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');">×</button>
|
||||
<div id="editCatModal" class="pf-modal" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');">
|
||||
<div class="pf-modal-content" onclick="event.stopPropagation()">
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<h3 class="pf-modal-title" style="margin: 0; font-size: 1.25rem;"><?= tr('bud_prev_edit_line_title') ?></h3>
|
||||
<button type="button" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-modal-close" style="background:none; border:none; font-size:1.5rem; cursor:pointer; color: var(--text-muted, #64748b);">×</button>
|
||||
</div>
|
||||
|
||||
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
|
||||
<input type="hidden" name="action" value="update_category">
|
||||
<input type="hidden" name="cat_id" id="edit_cat_id">
|
||||
<div class="form-group">
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="pf-label"><?= tr('bud_label_name') ?></label>
|
||||
<input type="text" name="name" id="edit_cat_name" class="pf-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?? 'Objectif Mensuel (€)' ?></label>
|
||||
<input type="number" step="1" name="target" id="edit_cat_target" class="pf-input" placeholder="Ex: 150">
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?></label>
|
||||
<input type="number" step="1" name="target" id="edit_cat_target" class="pf-input" placeholder="<?= tr('bud_prev_target_ph') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?? 'Destination Virement (Optionnel)' ?></label>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?></label>
|
||||
<select name="transfer_dest" id="edit_cat_transfer_dest" class="pf-input">
|
||||
<option value="">-- <?= tr('bud_prev_none') ?? 'Aucune' ?> --</option>
|
||||
<option value="vers L.Pol">vers L.Pol</option>
|
||||
<option value="vers L.Pep">vers L.Pep</option>
|
||||
<option value="vers L.Perso">vers L.Perso</option>
|
||||
<option value="vers commune">vers commune</option>
|
||||
<option value="">-- <?= tr('bud_prev_none') ?> --</option>
|
||||
<?php foreach ($bankAccounts as $accName): ?>
|
||||
<option value="vers <?= htmlspecialchars($accName) ?>"><?= tr('bud_prev_transfer_to') ?> <?= htmlspecialchars($accName) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="pf-label">🌴 <?= tr('bud_prev_link_holiday') ?></label>
|
||||
<select name="holiday_id" id="edit_cat_holiday" class="pf-input">
|
||||
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
|
||||
@@ -585,7 +617,8 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
<div class="modal-footer" style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 1.5rem;">
|
||||
<button type="button" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||
</div>
|
||||
@@ -604,8 +637,9 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('bud_adv_who_paid') ?></label>
|
||||
<select name="payer" class="pf-input" required>
|
||||
<option value="<?= htmlspecialchars($p1_name) ?>"><?= htmlspecialchars($p1_name) ?></option>
|
||||
<option value="<?= htmlspecialchars($p2_name) ?>"><?= htmlspecialchars($p2_name) ?></option>
|
||||
<?php foreach ($parentMapping as $map): ?>
|
||||
<option value="<?= htmlspecialchars($map['name']) ?>"><?= htmlspecialchars($map['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
@@ -644,8 +678,9 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label"><?= tr('bud_adv_who_paid') ?></label>
|
||||
<select name="payer" id="edit_adv_payer" class="pf-input" required>
|
||||
<option value="<?= htmlspecialchars($p1_name) ?>"><?= htmlspecialchars($p1_name) ?></option>
|
||||
<option value="<?= htmlspecialchars($p2_name) ?>"><?= htmlspecialchars($p2_name) ?></option>
|
||||
<?php foreach ($parentMapping as $map): ?>
|
||||
<option value="<?= htmlspecialchars($map['name']) ?>"><?= htmlspecialchars($map['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="pf-form-group">
|
||||
@@ -684,20 +719,6 @@ elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_n
|
||||
|
||||
<script>
|
||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
|
||||
// Dictionnaire I18N injecté proprement depuis PHP
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
'bud_prev_label_name': <?= json_encode(tr('bud_prev_label_name')) ?>,
|
||||
'bud_prev_err_no_history': <?= json_encode(tr('bud_prev_err_no_history')) ?>,
|
||||
'bud_prev_confirm_copy': <?= json_encode(tr('bud_prev_confirm_copy')) ?>,
|
||||
'bud_prev_confirm_transfers': <?= json_encode(tr('bud_prev_confirm_transfers')) ?>,
|
||||
'bud_prev_confirm_del_line': <?= json_encode(tr('bud_prev_confirm_del_line')) ?>,
|
||||
'bud_adv_confirm_resolve': <?= json_encode(tr('bud_adv_confirm_resolve') ?? 'Confirmer le remboursement ?') ?>,
|
||||
'bud_adv_confirm_delete': <?= json_encode(tr('bud_adv_confirm_delete') ?? 'Supprimer définitivement ?') ?>,
|
||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>
|
||||
};
|
||||
|
||||
window.CONFIG = window.CONFIG || {};
|
||||
window.CONFIG.parentMapping = <?= json_encode($parentMapping) ?>;
|
||||
window.CONFIG.CURRENCY = '<?= defined('CURRENCY') ? CURRENCY : "€" ?>';
|
||||
@@ -746,7 +767,7 @@ function duplicateMonth() {
|
||||
const parentMap = window.CONFIG.parentMapping;
|
||||
|
||||
if (!sourceDateStr) {
|
||||
alert(window.I18N['bud_prev_err_no_history']);
|
||||
alert(tr('bud_prev_err_no_history'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -757,7 +778,7 @@ function duplicateMonth() {
|
||||
|
||||
const sourceName = formatMonth(sourceDateStr);
|
||||
const targetName = formatMonth(targetDateStr);
|
||||
const message = window.I18N['bud_prev_confirm_copy'].replace('%s', sourceName).replace('%t', targetName);
|
||||
const message = tr('bud_prev_confirm_copy').replace('%s', sourceName).replace('%t', targetName);
|
||||
|
||||
if(!confirm(message)) return;
|
||||
|
||||
@@ -883,10 +904,9 @@ function updateSummaryTable() {
|
||||
function saveData(action, data) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', action);
|
||||
formData.append('ajax', '1'); // <-- Sécurisation pour pachaFetch / backend
|
||||
formData.append('ajax', '1');
|
||||
for (const key in data) formData.append(key, data[key]);
|
||||
|
||||
// On utilise fetch ici en mode aveugle pour ne pas bloquer l'UI lors de la frappe rapide
|
||||
fetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
|
||||
}
|
||||
|
||||
@@ -894,7 +914,7 @@ async function validateTransfers(personCss, month) {
|
||||
const parentMap = window.CONFIG.parentMapping.find(m => m.css === personCss);
|
||||
if (!parentMap) return;
|
||||
|
||||
const msg = window.I18N['bud_prev_confirm_transfers'].replace('%p', parentMap.name).replace('%m', month);
|
||||
const msg = tr('bud_prev_confirm_transfers').replace('%p', parentMap.name).replace('%m', month);
|
||||
if (!confirm(msg)) return;
|
||||
|
||||
const formData = new FormData();
|
||||
@@ -908,10 +928,10 @@ async function validateTransfers(personCss, month) {
|
||||
if(result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(window.I18N['bud_err_tech'] + " : " + result.error);
|
||||
alert(tr('bud_err_tech') + " : " + result.error);
|
||||
}
|
||||
} catch(e) {
|
||||
alert(window.I18N['bud_err_tech']);
|
||||
alert(tr('bud_err_tech'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,15 +952,15 @@ async function saveGenericNote(noteType, refId, content) {
|
||||
setTimeout(() => indicator.style.opacity = '0', 2000);
|
||||
}
|
||||
} else {
|
||||
alert(window.I18N['bud_err_tech'] + " : " + data.error);
|
||||
alert(tr('bud_err_tech') + " : " + data.error);
|
||||
}
|
||||
} catch(e) {
|
||||
alert(window.I18N['bud_err_tech']);
|
||||
alert(tr('bud_err_tech'));
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCategory(id) {
|
||||
if (!confirm(window.I18N['bud_prev_confirm_del_line'])) return;
|
||||
if (!confirm(tr('bud_prev_confirm_del_line'))) return;
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'delete_category');
|
||||
formData.append('id', id);
|
||||
@@ -980,29 +1000,28 @@ async function handleAdvanceSubmit(event, form) {
|
||||
|
||||
const endpoint = form.getAttribute('action');
|
||||
const formData = new FormData(form);
|
||||
formData.append('ajax', '1'); // <-- Correction de l'erreur d'empty string
|
||||
formData.append('ajax', '1');
|
||||
|
||||
try {
|
||||
// Utilisation robuste de pachaFetch
|
||||
const result = await pachaFetch(endpoint, { method: 'POST', body: formData });
|
||||
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert((window.I18N['bud_err_tech'] || "Erreur") + " : " + (result.error || "Opération échouée"));
|
||||
alert((tr('bud_err_tech') || "Erreur") + " : " + (result.error || "Opération échouée"));
|
||||
btnSubmit.disabled = false;
|
||||
btnSubmit.innerHTML = oldText;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("AJAX Error:", err);
|
||||
alert(window.I18N['bud_err_tech'] || "Une erreur technique est survenue.");
|
||||
alert(tr('bud_err_tech') || "Une erreur technique est survenue.");
|
||||
btnSubmit.disabled = false;
|
||||
btnSubmit.innerHTML = oldText;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeResolveAdvance(id) {
|
||||
if (!confirm(window.I18N['bud_adv_confirm_resolve'])) return;
|
||||
if (!confirm(tr('bud_adv_confirm_resolve'))) return;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'resolve_advance');
|
||||
fd.append('id', id);
|
||||
@@ -1017,7 +1036,7 @@ async function executeResolveAdvance(id) {
|
||||
}
|
||||
|
||||
async function executeDeleteAdvance(id) {
|
||||
if (!confirm(window.I18N['bud_adv_confirm_delete'])) return;
|
||||
if (!confirm(tr('bud_adv_confirm_delete'))) return;
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'delete_advance');
|
||||
fd.append('id', id);
|
||||
@@ -1087,7 +1106,6 @@ document.addEventListener('click', function(e) {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', recalcAllAllocations);
|
||||
|
||||
// Soumission standard des modales de catégories avec pachaFetch
|
||||
document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => {
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -1097,7 +1115,7 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerText = '⏳ ...';
|
||||
const formData = new FormData(form);
|
||||
formData.append('ajax', '1'); // Toujours sécuriser
|
||||
formData.append('ajax', '1');
|
||||
const actionUrl = form.getAttribute('action');
|
||||
|
||||
const result = await pachaFetch(actionUrl, { method: 'POST', body: formData });
|
||||
@@ -1107,10 +1125,10 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
|
||||
document.body.classList.remove('no-scroll');
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
||||
alert((tr('bud_err_tech') || 'Erreur') + " : " + (result.error || "Inconnue"));
|
||||
}
|
||||
} catch (error) {
|
||||
alert(window.I18N['bud_err_tech'] || "Une erreur technique est survenue.");
|
||||
alert(tr('bud_err_tech') || "Une erreur technique est survenue.");
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerText = originalText;
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
<?php
|
||||
// Sécurité : Ce fichier ne doit pas être appelé directement
|
||||
if (!defined('CURRENCY')) {
|
||||
define('CURRENCY', '€');
|
||||
}
|
||||
?>
|
||||
<div class="budget-settings-backdrop" id="modal-budget-settings">
|
||||
<div class="budget-settings-modal">
|
||||
<div class="bs-header">
|
||||
<h3>⚙️ <?= tr('budget_settings_title') ?></h3>
|
||||
<button class="bs-close" onclick="closeBudgetSettings()">×</button>
|
||||
</div>
|
||||
<div class="bs-layout">
|
||||
<div class="bs-sidebar">
|
||||
<button class="bs-tab-btn active" onclick="switchBsTab('accounts', this)">
|
||||
<?= tr('bs_tab_accounts') ?>
|
||||
<?php if (!$hasAccounts): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
|
||||
</button>
|
||||
<button class="bs-tab-btn" onclick="switchBsTab('categories', this)">
|
||||
<?= tr('bs_tab_categories') ?>
|
||||
<?php if (!$hasCategories): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
|
||||
</button>
|
||||
<button class="bs-tab-btn" onclick="switchBsTab('rules', this)"><?= tr('bs_tab_rules') ?></button>
|
||||
<button class="bs-tab-btn" onclick="switchBsTab('salaries', this)">
|
||||
<?= tr('bs_tab_salaries') ?>
|
||||
<?php if (!$hasSalaries): ?><span class="alert-dot alert-dot-inline">!</span><?php endif; ?>
|
||||
</button>
|
||||
<button class="bs-tab-btn" onclick="switchBsTab('csv', this)"><?= tr('bs_tab_csv') ?></button>
|
||||
</div>
|
||||
|
||||
<div class="bs-content">
|
||||
|
||||
<?php if (!isset($isBudgetSetupOk) || !$isBudgetSetupOk || !$hasBudgetItems): ?>
|
||||
<div class="bs-onboarding-card">
|
||||
<h4 class="bs-onboarding-title">
|
||||
<span>🚀</span> <?= tr('bs_onb_title') ?>
|
||||
</h4>
|
||||
<p class="bs-onboarding-desc"><?= tr('bs_onb_desc') ?></p>
|
||||
<ul class="bs-onboarding-list">
|
||||
<li>
|
||||
<strong class="<?= $hasCategories ? 'text-success' : 'text-danger' ?>"><?= $hasCategories ? '✅' : '❌' ?></strong>
|
||||
<a class="bs-onboarding-link" onclick="switchBsTab('categories', document.querySelectorAll('.bs-tab-btn')[1]); return false;"><?= tr('bs_onb_cat_link') ?></a> : <?= tr('bs_onb_cat_desc') ?>
|
||||
</li>
|
||||
<li>
|
||||
<strong class="<?= $hasAccounts ? 'text-success' : 'text-danger' ?>"><?= $hasAccounts ? '✅' : '❌' ?></strong>
|
||||
<a class="bs-onboarding-link" onclick="switchBsTab('accounts', document.querySelectorAll('.bs-tab-btn')[0]); return false;"><?= tr('bs_onb_acc_link') ?></a> : <?= tr('bs_onb_acc_desc') ?>
|
||||
</li>
|
||||
<li>
|
||||
<strong class="<?= $hasSalaries ? 'text-success' : 'text-danger' ?>"><?= $hasSalaries ? '✅' : '❌' ?></strong>
|
||||
<a class="bs-onboarding-link" onclick="switchBsTab('salaries', document.querySelectorAll('.bs-tab-btn')[3]); return false;"><?= tr('bs_onb_sal_link') ?></a> : <?= tr('bs_onb_sal_desc') ?>
|
||||
</li>
|
||||
<li style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed rgba(180, 83, 9, 0.3);">
|
||||
<strong class="<?= $hasBudgetItems ? 'text-success' : 'text-warning' ?>" style="color: <?= $hasBudgetItems ? '' : '#f59e0b' ?>"><?= $hasBudgetItems ? '✅' : '⏳' ?></strong>
|
||||
<a href="?tab=recap" onclick="closeBudgetSettings();" class="bs-onboarding-link"><?= tr('bs_onb_items_link') ?></a> : <?= tr('bs_onb_items_desc') ?>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="pane-accounts" class="bs-pane active">
|
||||
<div class="bs-pane-header">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_accounts') ?></h4>
|
||||
<div class="bs-currency-selector">
|
||||
<span><?= tr('bs_currency_label') ?> :</span>
|
||||
<select id="bs-currency-select" class="pf-input bs-currency-select" onchange="updateBudgetCurrency(this)">
|
||||
<option value="€">EUR (€)</option>
|
||||
<option value="$">USD ($)</option>
|
||||
<option value="£">GBP (£)</option>
|
||||
<option value="CHF">CHF</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="bs-list-accounts" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||
<hr class="bs-divider">
|
||||
<h5 class="bs-section-subtitle"><?= tr('bs_add_account_title') ?></h5>
|
||||
<form id="form-add-account" class="bs-form-inline">
|
||||
<input type="text" name="name" class="pf-input bs-input-flex" placeholder="<?= tr('bs_account_name_ph') ?>" required>
|
||||
<select name="type" class="pf-input bs-input-fixed">
|
||||
<option value="checking"><?= tr('bs_type_checking') ?></option>
|
||||
<option value="savings"><?= tr('bs_type_savings') ?></option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="pane-categories" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_categories') ?></h4>
|
||||
<div id="bs-list-categories" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||
<hr class="bs-divider">
|
||||
<h5 class="bs-section-subtitle"><?= tr('bs_add_category_title') ?></h5>
|
||||
<form id="form-add-category" class="bs-form-inline align-center">
|
||||
<input type="text" name="code" class="pf-input bs-input-fixed" placeholder="<?= tr('bs_cat_code_ph') ?>" required>
|
||||
<input type="text" name="label" class="pf-input bs-input-flex" placeholder="<?= tr('bs_cat_label_ph') ?>" required>
|
||||
|
||||
<select name="type" class="pf-input bs-input-fixed">
|
||||
<option value="Expense"><?= tr('bs_type_expense') ?></option>
|
||||
<option value="Income"><?= tr('bs_type_income') ?></option>
|
||||
<option value="Savings"><?= tr('bs_type_savings_cat') ?></option>
|
||||
</select>
|
||||
|
||||
<select name="icon" class="pf-input bs-input-icon">
|
||||
<option value="📌">📌</option><option value="🛒">🛒</option><option value="🥖">🥖</option>
|
||||
<option value="🍽️">🍽️</option><option value="⛽">⛽</option><option value="🚗">🚗</option>
|
||||
<option value="🚆">🚆</option><option value="🏠">🏠</option><option value="⚡">⚡</option>
|
||||
<option value="💧">💧</option><option value="⚕️">⚕️</option><option value="🎒">🎒</option>
|
||||
<option value="👕">👕</option><option value="📱">📱</option><option value="🎮">🎮</option>
|
||||
<option value="✈️">✈️</option><option value="🎁">🎁</option><option value="🐶">🐶</option>
|
||||
<option value="💵">💵</option><option value="🐷">🐷</option><option value="📈">📈</option>
|
||||
</select>
|
||||
|
||||
<input type="color" name="color" value="#3b82f6" class="bs-input-color">
|
||||
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="pane-rules" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_rules') ?></h4>
|
||||
<input type="text" id="input-search-rules" class="bs-rule-search" placeholder="<?= tr('bs_rule_search_ph') ?>" onkeyup="filterRules()">
|
||||
<div id="bs-list-rules" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||
<hr class="bs-divider">
|
||||
<h5 class="bs-section-subtitle"><?= tr('bs_add_rule_title') ?></h5>
|
||||
<form id="form-add-rule" class="bs-form-inline align-center">
|
||||
<input type="text" name="keyword" class="pf-input bs-input-flex" placeholder="<?= tr('bs_rule_keyword_ph') ?>" required>
|
||||
<select name="category" id="select-rule-category" class="pf-input bs-input-fixed" required></select>
|
||||
<button type="submit" class="btn btn-secondary"><?= tr('btn_add') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="pane-salaries" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_tab_salaries') ?> (<?= date('Y') ?>)</h4>
|
||||
<div id="bs-list-salaries" class="bs-list-container">⏳ <?= tr('loading') ?></div>
|
||||
</div>
|
||||
|
||||
<div id="pane-csv" class="bs-pane">
|
||||
<h4 class="bs-section-title"><?= tr('bs_csv_title') ?></h4>
|
||||
<p class="pf-muted-tiny bs-desc"><?= tr('bs_csv_desc') ?></p>
|
||||
|
||||
<div id="csv-drop-zone" class="bs-csv-dropzone">
|
||||
<div class="bs-csv-icon">📥</div>
|
||||
<h5 class="bs-csv-title"><?= tr('bs_csv_drop_title') ?></h5>
|
||||
<p class="pf-muted-tiny bs-csv-subtitle"><?= tr('bs_csv_drop_desc') ?></p>
|
||||
<input type="file" id="csv_file_input" accept=".csv" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div id="csv-preview-container" class="bs-csv-preview"></div>
|
||||
|
||||
<h5 class="bs-section-subtitle bordered"><?= tr('bs_csv_settings_title') ?></h5>
|
||||
<form id="form-csv-mapping" onsubmit="saveCsvMapping(event)">
|
||||
<div class="bs-grid-2">
|
||||
<div>
|
||||
<label class="pf-label"><?= tr('bs_csv_delimiter') ?></label>
|
||||
<select name="csv_delimiter" id="csv_delimiter" class="pf-input">
|
||||
<option value=";"><?= tr('bs_csv_delim_semi') ?></option>
|
||||
<option value=","><?= tr('bs_csv_delim_comma') ?></option>
|
||||
<option value="\t"><?= tr('bs_csv_delim_tab') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="pf-label"><?= tr('bs_csv_date_format') ?></label>
|
||||
<select name="csv_date_format" id="csv_date_format" class="pf-input">
|
||||
<option value="d/m/Y">JJ/MM/AAAA</option>
|
||||
<option value="Y-m-d">AAAA-MM-JJ</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="bs-section-subtitle">
|
||||
<?= tr('bs_csv_col_index') ?>
|
||||
<small class="bs-text-muted-normal"><?= tr('bs_csv_col_index_help') ?></small>
|
||||
</h5>
|
||||
<div class="bs-grid-2">
|
||||
<div>
|
||||
<label class="pf-label"><?= tr('bs_csv_col_date') ?></label>
|
||||
<input type="number" min="0" name="csv_col_date" id="csv_col_date" class="pf-input" value="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="pf-label"><?= tr('bs_csv_col_label') ?></label>
|
||||
<input type="number" min="0" name="csv_col_label" id="csv_col_label" class="pf-input" value="1" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bs-card-gray">
|
||||
<label class="pf-label"><?= tr('bs_csv_amount_mgmt') ?></label>
|
||||
<select name="csv_amount_type" id="csv_amount_type" class="pf-input bs-input-mb" onchange="toggleCsvAmountCols(this.value)">
|
||||
<option value="single"><?= tr('bs_csv_amount_single') ?></option>
|
||||
<option value="split"><?= tr('bs_csv_amount_split') ?></option>
|
||||
</select>
|
||||
|
||||
<div class="bs-grid-2">
|
||||
<div>
|
||||
<label class="pf-label" id="lbl_col_debit"><?= tr('bs_csv_col_amount') ?></label>
|
||||
<input type="number" min="0" name="csv_col_debit" id="csv_col_debit" class="pf-input" value="8" required>
|
||||
</div>
|
||||
<div id="wrapper_col_credit" style="display:none;">
|
||||
<label class="pf-label"><?= tr('bs_csv_col_credit') ?></label>
|
||||
<input type="number" min="0" name="csv_col_credit" id="csv_col_credit" class="pf-input" value="9">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bs-input-mb">
|
||||
<label class="pf-label"><?= tr('bs_csv_col_ref') ?></label>
|
||||
<input type="number" min="0" name="csv_col_ref" id="csv_col_ref" class="pf-input" value="3">
|
||||
</div>
|
||||
<div class="bs-text-right">
|
||||
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save_format') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tabsContainer = document.querySelector('.budget-tabs-container');
|
||||
const activeTab = document.querySelector('.tab-item.active');
|
||||
if (tabsContainer && activeTab) {
|
||||
if (tabsContainer.scrollWidth > tabsContainer.clientWidth) {
|
||||
const scrollPos = activeTab.offsetLeft - (tabsContainer.offsetWidth / 2) + (activeTab.offsetWidth / 2);
|
||||
tabsContainer.scrollTo({ left: scrollPos, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function openBudgetSettings() {
|
||||
document.getElementById('modal-budget-settings').classList.add('show');
|
||||
document.body.classList.add('no-scroll');
|
||||
loadBudgetSettingsData();
|
||||
}
|
||||
|
||||
function closeBudgetSettings() {
|
||||
document.getElementById('modal-budget-settings').classList.remove('show');
|
||||
document.body.classList.remove('no-scroll');
|
||||
}
|
||||
|
||||
document.getElementById('modal-budget-settings')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'modal-budget-settings') closeBudgetSettings();
|
||||
});
|
||||
|
||||
function switchBsTab(tabId, btnEl) {
|
||||
document.querySelectorAll('.bs-tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.bs-pane').forEach(p => p.classList.remove('active'));
|
||||
btnEl.classList.add('active');
|
||||
document.getElementById('pane-' + tabId).classList.add('active');
|
||||
}
|
||||
|
||||
async function loadBudgetSettingsData() {
|
||||
try {
|
||||
const response = await pachaFetch('/modules/budget/includes/api/settings.php?action=get_all', { method: 'GET' });
|
||||
if (!response.success) throw new Error(response.error || tr('error_occured'));
|
||||
renderAccounts(response.data.accounts);
|
||||
renderCategories(response.data.categories);
|
||||
populateRuleCategories(response.data.categories);
|
||||
renderRules(response.data.rules);
|
||||
renderSalaries(response.data.salaries, response.data.year);
|
||||
const currencySelect = document.getElementById('bs-currency-select');
|
||||
if (currencySelect && response.data.currency) currencySelect.value = response.data.currency;
|
||||
} catch (err) {
|
||||
console.error("Erreur chargement paramètres :", err);
|
||||
const errorMsg = `<div class="pf-alert pf-alert--error bs-input-mb">❌ ${tr('error_loading_settings')} : ${err.message}</div>`;
|
||||
['accounts', 'categories', 'rules', 'salaries'].forEach(id => {
|
||||
const el = document.getElementById('bs-list-' + id);
|
||||
if (el) el.innerHTML = errorMsg;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 1. ONGLET : COMPTES
|
||||
function renderAccounts(accounts) {
|
||||
const container = document.getElementById('bs-list-accounts');
|
||||
if (!container) return;
|
||||
if (!accounts || accounts.length === 0) {
|
||||
container.innerHTML = `<em class="text-muted">${tr('bs_empty_accounts')}</em>`;
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
accounts.forEach(acc => {
|
||||
const typeBadge = acc.account_type === 'checking' ? tr('bs_type_checking') : tr('bs_type_savings');
|
||||
const defaultBadge = acc.is_default == 1 ? `<span class="bs-badge-default">${tr('bs_badge_default')}</span>` : '';
|
||||
const ownerText = acc.owner_name ? `${tr('bs_owner')} : ${acc.owner_name}` : tr('bs_shared_account');
|
||||
html += `
|
||||
<div class="bs-list-item">
|
||||
<div>
|
||||
<strong>${acc.name}</strong> ${defaultBadge}<br>
|
||||
<small class="text-muted">${typeBadge} · ${ownerText}</small>
|
||||
</div>
|
||||
<div class="pf-flex-gap-8">
|
||||
<button type="button" class="btn-icon-action delete" onclick="deleteAccount(${acc.id})" title="${tr('btn_delete')}">🗑️</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 2. ONGLET : CATÉGORIES
|
||||
function renderCategories(categories) {
|
||||
const container = document.getElementById('bs-list-categories');
|
||||
if (!container) return;
|
||||
if (!categories || categories.length === 0) {
|
||||
container.innerHTML = `<em class="text-muted">${tr('bs_empty_categories')}</em>`;
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
categories.forEach(cat => {
|
||||
let typeLabel = cat.type === 'Income' ? tr('bs_type_income') : cat.type === 'Expense' ? tr('bs_type_expense') : tr('bs_type_savings_cat');
|
||||
html += `
|
||||
<div class="bs-list-item" style="border-left: 4px solid ${cat.color || '#ccc'};">
|
||||
<div>
|
||||
<span style="font-size:1.2rem; margin-right:8px;">${cat.icon || '📌'}</span>
|
||||
<strong>${cat.label}</strong> <small class="text-muted">(${cat.code} — ${typeLabel})</small>
|
||||
</div>
|
||||
<div class="pf-flex-gap-8">
|
||||
<button type="button" class="btn-icon-action delete" onclick="deleteCategory(${cat.id})" title="${tr('btn_delete')}">🗑️</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 3. ONGLET : RÈGLES D'IMPORT
|
||||
function renderRules(rules) {
|
||||
const container = document.getElementById('bs-list-rules');
|
||||
if (!container) return;
|
||||
if (!rules || rules.length === 0) {
|
||||
container.innerHTML = `<em class="text-muted">${tr('bs_empty_rules')}</em>`;
|
||||
return;
|
||||
}
|
||||
const groupedRules = {};
|
||||
rules.forEach(r => {
|
||||
const catName = r.cat_label || r.category;
|
||||
if (!groupedRules[catName]) groupedRules[catName] = [];
|
||||
groupedRules[catName].push(r);
|
||||
});
|
||||
let html = '';
|
||||
for (const [catName, catRules] of Object.entries(groupedRules)) {
|
||||
html += `
|
||||
<details class="pf-accordion js-rule-group" data-catname="${catName.toLowerCase()}">
|
||||
<summary class="pf-accordion-summary bs-accordion-summary">
|
||||
<span>📁 ${catName}</span>
|
||||
<span class="bs-accordion-count">${catRules.length}</span>
|
||||
</summary>
|
||||
<div class="pf-accordion-content" style="padding: 0.5rem;">
|
||||
<div class="bs-rule-tags">`;
|
||||
catRules.forEach(r => {
|
||||
html += `<div class="bs-rule-tag js-rule-tag" data-keyword="${r.keyword.toLowerCase()}">
|
||||
${r.keyword}
|
||||
<button type="button" class="bs-rule-tag-del" onclick="deleteRule(${r.id})" title="${tr('btn_delete')}">×</button>
|
||||
</div>`;
|
||||
});
|
||||
html += ` </div>
|
||||
</div>
|
||||
</details>`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function filterRules() {
|
||||
const searchVal = document.getElementById('input-search-rules').value.toLowerCase();
|
||||
document.querySelectorAll('.js-rule-group').forEach(group => {
|
||||
let hasVisibleTag = false;
|
||||
const catName = group.getAttribute('data-catname');
|
||||
group.querySelectorAll('.js-rule-tag').forEach(tag => {
|
||||
const keyword = tag.getAttribute('data-keyword');
|
||||
if (keyword.includes(searchVal)) { tag.style.display = 'inline-flex'; hasVisibleTag = true; }
|
||||
else { tag.style.display = 'none'; }
|
||||
});
|
||||
if (catName.includes(searchVal) && searchVal.length > 0) {
|
||||
group.querySelectorAll('.js-rule-tag').forEach(tag => tag.style.display = 'inline-flex');
|
||||
hasVisibleTag = true;
|
||||
}
|
||||
if (hasVisibleTag) {
|
||||
group.style.display = 'block';
|
||||
if (searchVal.length > 0) group.setAttribute('open', '');
|
||||
} else {
|
||||
group.style.display = 'none'; group.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 5. ONGLET : SALAIRES
|
||||
function renderSalaries(salaries, year) {
|
||||
const container = document.getElementById('bs-list-salaries');
|
||||
if (!container) return;
|
||||
if (!salaries || salaries.length === 0) {
|
||||
container.innerHTML = `<em class="text-muted">${tr('bs_empty_salaries')} ${year}.</em>`;
|
||||
return;
|
||||
}
|
||||
const currency = '<?= CURRENCY ?>';
|
||||
let html = `<p class="bs-desc">${tr('bs_salaries_year_desc')} <strong>${year}</strong>.</p>`;
|
||||
salaries.forEach(s => {
|
||||
html += `
|
||||
<div class="bs-list-item" id="salary-item-${s.id}">
|
||||
<div>
|
||||
<strong>${s.person}</strong><br>
|
||||
<small class="text-muted">
|
||||
${tr('bs_salary_net')} : ${s.salary} ${currency}<br>
|
||||
${tr('bs_salary_fees')} : ${s.mensualite} ${currency}
|
||||
</small>
|
||||
</div>
|
||||
<div class="pf-flex-gap-8">
|
||||
<button type="button" class="btn-icon-action edit" data-id="${s.id}" data-person="${s.person}" data-salary="${s.salary}" data-mensualite="${s.mensualite}" onclick="inlineEditSalary(this)" title="${tr('btn_edit')}">✏️</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// --- CRUD ACTIONS ---
|
||||
async function deleteAccount(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete'))) return; try { const fd = new FormData(); fd.append('action', 'delete_account'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
|
||||
const formAddAccount = document.getElementById('form-add-account');
|
||||
if (formAddAccount) formAddAccount.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddAccount.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddAccount); fd.append('action', 'add_account'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddAccount.reset(); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
|
||||
|
||||
async function deleteCategory(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete_cat'))) return; try { const fd = new FormData(); fd.append('action', 'delete_category'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
|
||||
const formAddCategory = document.getElementById('form-add-category');
|
||||
if (formAddCategory) formAddCategory.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddCategory.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddCategory); fd.append('action', 'add_category'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddCategory.reset(); formAddCategory.querySelector('input[type="color"]').value = "#3b82f6"; await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
|
||||
|
||||
function populateRuleCategories(categories) { const select = document.getElementById('select-rule-category'); if (!select) return; select.innerHTML = ''; if (!categories) return; categories.forEach(cat => { const opt = document.createElement('option'); opt.value = cat.code; opt.textContent = `${cat.icon || ''} ${cat.label}`; select.appendChild(opt); }); }
|
||||
async function deleteRule(id) { if (!await pachaConfirm(tr('btn_delete'), tr('bs_confirm_delete_rule'))) return; try { const fd = new FormData(); fd.append('action', 'delete_rule'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
|
||||
const formAddRule = document.getElementById('form-add-rule');
|
||||
if (formAddRule) formAddRule.addEventListener('submit', async (e) => { e.preventDefault(); const btn = formAddRule.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(formAddRule); fd.append('action', 'add_rule'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); formAddRule.reset(); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } });
|
||||
|
||||
function inlineEditSalary(btnEl) {
|
||||
const id = btnEl.dataset.id; const person = btnEl.dataset.person; const salary = btnEl.dataset.salary; const mensualite = btnEl.dataset.mensualite; const currency = '<?= CURRENCY ?>';
|
||||
document.getElementById(`salary-item-${id}`).innerHTML = `
|
||||
<form onsubmit="submitInlineSalary(event, ${id})" class="bs-form-inline align-center">
|
||||
<div class="bs-input-flex"><strong>${person}</strong></div>
|
||||
<div style="display:flex; align-items:center; gap:4px;"><small class="text-muted">${tr('bs_salary_net')}:</small><input type="number" step="0.01" name="salary" class="pf-input bs-input-fixed" value="${salary}" required> ${currency}</div>
|
||||
<div style="display:flex; align-items:center; gap:4px;"><small class="text-muted">${tr('bs_salary_fees')}:</small><input type="number" step="0.01" name="mensualite" class="pf-input bs-input-fixed" value="${mensualite}" required> ${currency}</div>
|
||||
<button type="submit" class="btn btn-secondary">💾</button>
|
||||
<button type="button" class="btn btn-ghost" onclick="loadBudgetSettingsData()">❌</button>
|
||||
</form>`;
|
||||
}
|
||||
async function submitInlineSalary(event, id) { event.preventDefault(); const form = event.target; const btn = form.querySelector('button'); btn.innerText = '⏳'; btn.disabled = true; try { const fd = new FormData(form); fd.append('action', 'save_salary'); fd.append('id', id); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); btn.innerText = '💾'; btn.disabled = false; } }
|
||||
|
||||
async function updateBudgetCurrency(selectEl) { try { const fd = new FormData(); fd.append('action', 'save_currency'); fd.append('currency', selectEl.value); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); if (typeof showToast === 'function') { showToast(`${tr('bs_currency_updated')} ✅`); } await loadBudgetSettingsData(); } catch (err) { alert("Erreur : " + err.message); } }
|
||||
|
||||
// CSV
|
||||
function toggleCsvAmountCols(type) { if (type === 'split') { document.getElementById('wrapper_col_credit').style.display = 'block'; document.getElementById('lbl_col_debit').innerText = tr('bs_csv_col_debit'); } else { document.getElementById('wrapper_col_credit').style.display = 'none'; document.getElementById('lbl_col_debit').innerText = tr('bs_csv_col_amount'); } }
|
||||
function renderCsvMapping(mapping) { if (!mapping) return; document.getElementById('csv_delimiter').value = mapping.delimiter || ';'; document.getElementById('csv_date_format').value = mapping.date_format || 'd/m/Y'; document.getElementById('csv_col_date').value = mapping.col_date ?? 0; document.getElementById('csv_col_label').value = mapping.col_label ?? 1; document.getElementById('csv_col_ref').value = mapping.col_ref ?? 3; const amountType = mapping.amount_type || 'split'; document.getElementById('csv_amount_type').value = amountType; document.getElementById('csv_col_debit').value = mapping.col_debit ?? 8; document.getElementById('csv_col_credit').value = mapping.col_credit ?? 9; toggleCsvAmountCols(amountType); }
|
||||
async function saveCsvMapping(e) { e.preventDefault(); const form = e.target; const btn = form.querySelector('button'); const oldText = btn.innerText; btn.innerText = '⏳...'; btn.disabled = true; try { const fd = new FormData(form); fd.append('action', 'save_csv_mapping'); await pachaFetch('/modules/budget/includes/api/settings.php', { method: 'POST', body: fd }); if (typeof showToast === 'function') showToast(tr('bs_csv_saved')); else alert(tr('bs_csv_saved')); } catch (err) { alert(tr('error_occured') + " : " + err.message); } finally { btn.innerText = oldText; btn.disabled = false; } }
|
||||
|
||||
const dropZone = document.getElementById('csv-drop-zone'); const fileInput = document.getElementById('csv_file_input');
|
||||
if (dropZone && fileInput) {
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.backgroundColor = 'rgba(0, 123, 255, 0.05)'; dropZone.style.borderColor = 'var(--primary-color, #007bff)'; });
|
||||
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.style.backgroundColor = ''; dropZone.style.borderColor = 'var(--border-light, #ccc)'; });
|
||||
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.style.backgroundColor = ''; dropZone.style.borderColor = 'var(--border-light, #ccc)'; if (e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; handleCsvUpload(e.dataTransfer.files[0]); } });
|
||||
fileInput.addEventListener('change', function() { if (this.files.length) { handleCsvUpload(this.files[0]); } });
|
||||
}
|
||||
function handleCsvUpload(file) {
|
||||
if (!file.name.endsWith('.csv')) { alert(tr('bs_csv_err_type')); return; }
|
||||
if (typeof showToast === 'function') { showToast(`${tr('bs_csv_file_ok')} : ${file.name}`); }
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const text = e.target.result; const delimiter = document.getElementById('csv_delimiter').value || ';';
|
||||
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
|
||||
if (lines.length > 0) {
|
||||
const headers = lines[0].split(delimiter).map(h => h.replace(/^"|"$/g, '').trim());
|
||||
const sampleRows = [];
|
||||
for (let i = 1; i < Math.min(lines.length, 3); i++) { sampleRows.push(lines[i].split(delimiter).map(d => d.replace(/^"|"$/g, '').trim())); }
|
||||
renderCsvTablePreview(headers, sampleRows);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file, 'ISO-8859-1');
|
||||
}
|
||||
function renderCsvTablePreview(headers, sampleRows) {
|
||||
const container = document.getElementById('csv-preview-container');
|
||||
let html = `<h5 class="bs-section-subtitle">👁️ ${tr('bs_csv_preview_title')}</h5>`;
|
||||
html += `<div class="bs-csv-table-wrapper"><table class="bs-csv-table"><thead class="bs-csv-thead"><tr>`;
|
||||
headers.forEach((header, index) => {
|
||||
const cleanHeader = header || `(${tr('bs_csv_empty_col')})`;
|
||||
html += `<th class="bs-csv-th"><div class="bs-csv-th-index">N° ${index}</div><div>${cleanHeader}</div></th>`;
|
||||
});
|
||||
html += `</tr></thead><tbody>`;
|
||||
if (sampleRows.length === 0) { html += `<tr><td colspan="${headers.length}" class="bs-csv-td-empty">${tr('bs_csv_no_data')}</td></tr>`; }
|
||||
else { sampleRows.forEach(row => { html += `<tr class="bs-csv-tr">`; for (let i = 0; i < headers.length; i++) { const cellData = row[i] !== undefined ? row[i] : ''; const displayData = cellData.length > 40 ? cellData.substring(0, 40) + '...' : cellData; html += `<td class="bs-csv-td">${displayData}</td>`; } html += `</tr>`; }); }
|
||||
html += `</tbody></table></div>`;
|
||||
container.innerHTML = html; container.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user