This commit is contained in:
2026-04-13 11:06:08 +02:00
parent c4b80c3650
commit feb2f158dd
19 changed files with 1738 additions and 917 deletions
+11 -11
View File
@@ -1,12 +1,13 @@
<?php
require __DIR__ . '/includes/auth.php';
require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php';
require_login('/login.php');
// Gestion de l'onglet actif (par défaut 'recap')
// Gestion de l'onglet actif (par défaut 'suivi')
$tab = $_GET['tab'] ?? 'suivi';
$pageTitle = "PachaFamily - Budget";
$pageTitle = tr('budget_page_title');
$activePage = "budget";
$pageCss = "/modules/budget/budget.css";
@@ -15,27 +16,27 @@ require __DIR__ . '/header.php';
<div class="pf-container">
<div class="pf-hero" style="text-align: center; margin-bottom: 30px;">
<h1 style="margin-bottom: 20px;">Gestion du Budget</h1>
<h1 style="margin-bottom: 20px;"><?= tr('budget_main_header') ?></h1>
<nav class="budget-tabs-container">
<a href="?tab=suivi" class="tab-item <?= $tab == 'suivi' ? 'active' : '' ?>">
<span class="tab-icon">🗓️</span>
<span>Suivi Mensuel</span>
<span><?= tr('budget_tab_tracking') ?></span>
</a>
<a href="?tab=budget_prev" class="tab-item <?= $tab == 'budget_prev' ? 'active' : '' ?>">
<span class="tab-icon">🎯</span>
<span>Budget 2026</span>
<span><?= tr('budget_tab_prev') ?></span>
</a>
<a href="?tab=epargne" class="tab-item <?= $tab == 'epargne' ? 'active' : '' ?>">
<span class="tab-icon">🐷</span>
<span>Épargne</span>
<span><?= tr('budget_tab_savings') ?></span>
</a>
<a href="?tab=recap" class="tab-item <?= $tab == 'recap' ? 'active' : '' ?>">
<span class="tab-icon">📊</span>
<span>Récapitulatif</span>
<span><?= tr('budget_tab_recap') ?></span>
</a>
</nav>
</div>
@@ -43,7 +44,6 @@ require __DIR__ . '/header.php';
<section class="pf-section">
<?php
// ROUTAGE DYNAMIQUE
// Sécurisation basique pour éviter d'inclure n'importe quoi
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev'];
if (in_array($tab, $allowedTabs)) {
@@ -53,12 +53,12 @@ require __DIR__ . '/header.php';
include $viewPath;
} else {
echo "<div style='text-align:center; padding:50px; color:#ef4444;'>";
echo "<h3>Oups ! Fichier introuvable.</h3>";
echo "<p>Vérifiez que le fichier <code>views/" . htmlspecialchars($tab) . ".php</code> existe bien.</p>";
echo "<h3>" . tr('budget_err_file_not_found') . "</h3>";
echo "<p>" . sprintf(tr('budget_err_file_detail'), htmlspecialchars($tab)) . "</p>";
echo "</div>";
}
} else {
echo "<p>Page non autorisée.</p>";
echo "<p>" . tr('budget_err_not_authorized') . "</p>";
}
?>
</section>
+54 -69
View File
@@ -6,9 +6,9 @@ error_reporting(E_ALL);
require __DIR__ . '/includes/auth.php';
require_login();
require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php';
$pageTitle = "PachaFamily - Family Calendar";
$pageTitle = tr('fc_page_title');
$activePage = "family-calendar";
$bodyClass = "pf-family-calendar";
$pageCss = "/modules/family-calendar/family-calendar.css";
@@ -16,20 +16,18 @@ $pageCss = "/modules/family-calendar/family-calendar.css";
require __DIR__ . '/header.php';
?>
<div class="pf-container" style="max-width:100%; padding:0;">
<div class="fc-header-row">
<h1>Family Calendar</h1>
<h1><?= tr('fc_main_header') ?></h1>
<div style="display: flex; gap: 10px;">
<button id="btnOpenSnapshotModal" class="pf-btn-icon-text">
<span class="icon">⚖️</span>
<span class="text">Corriger les soldes</span>
<span class="text"><?= tr('fc_btn_correct_balances') ?></span>
</button>
<button id="btnOpenHolidays" class="pf-btn-icon-text">
<span class="icon">🏖️</span>
<span class="text">Voir les vacances scolaires</span>
<span class="text"><?= tr('fc_btn_school_holidays') ?></span>
</button>
</div>
</div>
@@ -37,20 +35,19 @@ require __DIR__ . '/header.php';
<div id="modalHolidays" class="fc-modal-overlay" style="display: none;">
<div class="fc-modal-content">
<div class="fc-modal-header">
<h2>Vacances Scolaires (Zone C)</h2>
<h2><?= tr('fc_modal_holidays_title') ?></h2>
<button id="btnCloseHolidays" class="fc-modal-close">×</button>
</div>
<div class="fc-modal-body">
<table id="schoolHolidaysTable" class="fc-holidays-table">
<thead>
<tr>
<th>Période</th>
<th>Du</th>
<th>Au</th>
<th><?= tr('fc_col_period') ?></th>
<th><?= tr('fc_col_from') ?></th>
<th><?= tr('fc_col_to') ?></th>
</tr>
</thead>
<tbody>
</tbody>
<tbody></tbody>
</table>
</div>
</div>
@@ -59,20 +56,20 @@ require __DIR__ . '/header.php';
<div id="modalSnapshot" class="fc-modal-overlay" style="display: none;">
<div class="fc-modal-content" style="max-width: 400px;">
<div class="fc-modal-header">
<h2>Ajuster un solde</h2>
<h2><?= tr('fc_modal_snap_title') ?></h2>
<button id="btnCloseSnapshot" class="fc-modal-close">×</button>
</div>
<div class="fc-modal-body" style="padding: 24px;">
<form id="formSnapshot">
<div style="margin-bottom: 16px;">
<label class="pf-label">Personne</label>
<label class="pf-label"><?= tr('fc_label_person') ?></label>
<select id="snapPerson" class="pf-input" required>
<option value="2">Alex</option>
<option value="3">Laia</option>
</select>
</div>
<div style="margin-bottom: 16px;">
<label class="pf-label">Type de congé</label>
<label class="pf-label"><?= tr('fc_label_leave_type') ?></label>
<select id="snapType" class="pf-input" required>
<option value="CP">CP</option>
<option value="JRA">JRA</option>
@@ -80,14 +77,14 @@ require __DIR__ . '/header.php';
</select>
</div>
<div style="margin-bottom: 16px;">
<label class="pf-label">Date d'application</label>
<input type="date" id="snapDate" class="pf-input" required title="Choisissez de préférence le 1er jour d'un mois">
<label class="pf-label"><?= tr('fc_label_apply_date') ?></label>
<input type="date" id="snapDate" class="pf-input" required title="<?= tr('fc_snap_date_hint') ?>">
</div>
<div style="margin-bottom: 24px;">
<label class="pf-label">Solde restant à cette date</label>
<input type="number" step="0.5" id="snapBalance" class="pf-input" placeholder="ex: 12.5" required>
<label class="pf-label"><?= tr('fc_label_remaining_balance') ?></label>
<input type="number" step="0.5" id="snapBalance" class="pf-input" placeholder="<?= tr('fc_placeholder_balance') ?>" required>
</div>
<button type="submit" class="pf-btn" style="width: 100%;">Enregistrer le correctif</button>
<button type="submit" class="pf-btn" style="width: 100%;"><?= tr('fc_btn_save_snap') ?></button>
</form>
</div>
</div>
@@ -97,9 +94,9 @@ require __DIR__ . '/header.php';
<div class="fc-month-calendar-wrapper">
<div class="fc-month-header">
<div class="fc-view-controls">
<button class="fc-view-button fc-view-button--active" data-view="1month">1 mois</button>
<button class="fc-view-button" data-view="2months">2 mois</button>
<button class="fc-view-button" data-view="year">Année</button>
<button class="fc-view-button fc-view-button--active" data-view="1month"><?= tr('fc_view_1m') ?></button>
<button class="fc-view-button" data-view="2months"><?= tr('fc_view_2m') ?></button>
<button class="fc-view-button" data-view="year"><?= tr('fc_view_year') ?></button>
</div>
<h3 id="fc-current-month-year"></h3>
<div class="fc-nav-controls">
@@ -107,7 +104,6 @@ require __DIR__ . '/header.php';
<button id="fc-next-month" class="fc-nav-button"></button>
</div>
</div>
<div class="fc-calendar-container">
<div id="fc-month-calendar" class="fc-month-calendar"></div>
<div id="fc-month-selectionMenu" class="fc-selection-menu" style="display:none;"></div>
@@ -115,11 +111,9 @@ require __DIR__ . '/header.php';
</div>
</section>
<section class="pf-section">
<div class="fc-week-header">
<h2>Planning hebdo</h2>
<h2><?= tr('fc_weekly_planning') ?></h2>
<div class="fc-week-nav-controls">
<button id="fc-prev-school-year" class="fc-nav-button"></button>
<span id="fc-current-school-year-label"></span>
@@ -129,28 +123,21 @@ require __DIR__ . '/header.php';
<div id="planningTable-wrapper">
<table id="planningTable">
<colgroup>
<col class="col-month"><col class="col-month">
<col class="col-day"><col class="col-day"><col class="col-day"><col class="col-day"><col class="col-day">
<col class="col-total"><col class="col-total"><col class="col-total"><col class="col-total"><col class="col-total"><col class="col-total">
<col class="col-alex-sub"><col class="col-alex-sub"><col class="col-alex-sub"><col class="col-alex-sub"><col class="col-alex-sub"><col class="col-alex-sub">
<col class="col-laia-sub"><col class="col-laia-sub"><col class="col-laia-sub"><col class="col-laia-sub"><col class="col-laia-sub"><col class="col-laia-sub">
</colgroup>
<thead>
<tr>
<th rowspan="3" class="col-month col-sticky-mois">Mois</th>
<th rowspan="3" class="col-month col-sticky-mois">Sem.</th>
<th rowspan="3" class="col-day">Lun</th>
<th rowspan="3" class="col-day">Mar</th>
<th rowspan="3" class="col-day">Mer</th>
<th rowspan="3" class="col-day">Jeu</th>
<th rowspan="3" class="col-day">Ven</th>
<th rowspan="3" class="col-total rotated-text"><span>Off Carole</span></th>
<th rowspan="3" class="col-total rotated-text"><span>Extra Off</span></th>
<th rowspan="3" class="col-total rotated-text"><span>Centre</span></th>
<th rowspan="3" class="col-total rotated-text"><span>Avis</span></th>
<th rowspan="3" class="col-total rotated-text"><span>Pep Malade</span></th>
<th rowspan="3" class="col-total rotated-text"><span>Présence</span></th>
<th rowspan="3" class="col-month col-sticky-mois"><?= tr('fc_col_month') ?></th>
<th rowspan="3" class="col-month col-sticky-mois"><?= tr('fc_col_week_short') ?></th>
<th rowspan="3" class="col-day"><?= tr('day_mon') ?></th>
<th rowspan="3" class="col-day"><?= tr('day_tue') ?></th>
<th rowspan="3" class="col-day"><?= tr('day_wed') ?></th>
<th rowspan="3" class="col-day"><?= tr('day_thu') ?></th>
<th rowspan="3" class="col-day"><?= tr('day_fri') ?></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_off_carole') ?></span></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_extra_off') ?></span></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_centre') ?></span></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_avis') ?></span></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_pep_sick') ?></span></th>
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_presence') ?></span></th>
<th colspan="6" class="col-alex header-group">ALEX</th>
<th colspan="6" class="col-laia header-group">LAIA</th>
</tr>
@@ -159,12 +146,12 @@ require __DIR__ . '/header.php';
<th colspan="2" class="col-laia-sub">CP</th><th colspan="2" class="col-laia-sub">JRA</th><th colspan="2" class="col-laia-sub">JA</th>
</tr>
<tr>
<th class="col-alex-sub col-alex-av">Av.</th><th class="col-alex-sub col-alex-use">Use</th>
<th class="col-alex-sub col-alex-av">Av.</th><th class="col-alex-sub col-alex-use">Use</th>
<th class="col-alex-sub col-alex-av">Av.</th><th class="col-alex-sub col-alex-use">Use</th>
<th class="col-laia-sub col-laia-av">Av.</th><th class="col-laia-sub col-laia-use">Use</th>
<th class="col-laia-sub col-laia-av">Av.</th><th class="col-laia-sub col-laia-use">Use</th>
<th class="col-laia-sub col-laia-av">Av.</th><th class="col-laia-sub col-laia-use">Use</th>
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
<th class="col-alex-sub col-alex-av"><?= tr('fc_col_av') ?></th><th class="col-alex-sub col-alex-use"><?= tr('fc_col_use') ?></th>
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
<th class="col-laia-sub col-laia-av"><?= tr('fc_col_av') ?></th><th class="col-laia-sub col-laia-use"><?= tr('fc_col_use') ?></th>
</tr>
</thead>
<tbody id="planningBody"></tbody>
@@ -175,39 +162,37 @@ require __DIR__ . '/header.php';
<section class="pf-section pf-section--bottom-panels">
<div class="fc-bottom-grid">
<div class="pf-card">
<h2 class="pf-card-title">Légende</h2>
<h2 class="pf-card-title"><?= tr('fc_legend_title') ?></h2>
<div class="pf-card-body">
<div class="pf-legend-grid">
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span>Vacances Scolaires</span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span>Férié</span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-off-carole"></div><span>Off Carole</span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-extra-off-carole"></div><span>Extra Off</span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-centre"></div><span>Centre</span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-avis"></div><span>Avis</span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-pep-sick"></div><span>Pep Malade</span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-school-holiday"></div><span><?= tr('leg_school_holidays') ?></span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-public-holiday"></div><span><?= tr('leg_public_holiday') ?></span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-off-carole"></div><span><?= tr('leg_off_carole') ?></span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-extra-off-carole"></div><span><?= tr('leg_extra_off') ?></span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-centre"></div><span><?= tr('leg_centre') ?></span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-avis"></div><span><?= tr('leg_avis') ?></span></div>
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-pep-sick"></div><span><?= tr('leg_pep_sick') ?></span></div>
</div>
</div>
</div>
<div class="pf-card">
<div class="pf-card-title fc-summary-header">
<span>Récapitulatif</span>
<span><?= tr('fc_summary_title') ?></span>
<div class="fc-summary-controls">
<select id="summType" class="fc-summ-select"> <option value="year">Année</option>
<option value="month">Mois</option>
<select id="summType" class="fc-summ-select">
<option value="year"><?= tr('fc_summ_year') ?></option>
<option value="month"><?= tr('fc_summ_month') ?></option>
</select>
<select id="summValue" class="fc-summ-select"></select>
</div>
</div>
<div class="pf-card-body">
<div id="globalSummary" class="fc-summary-vertical">
<div id="globalSummary" class="fc-summary-vertical"></div>
</div>
</div>
</div>
</div>
</section>
</div>
+68 -73
View File
@@ -4,13 +4,14 @@
require __DIR__ . '/includes/auth.php';
require_login('/login.php');
require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php'; // Toujours s'assurer que tr() est dispo
if (session_status() === PHP_SESSION_NONE) { session_start(); }
// --- 1. CONFIGURATION & DONNÉES ---
$year = (int)date('Y');
$pageTitle = "PachaFamily - Llista de regals";
$pageTitle = tr('gift_page_title');
$activePage = "gift-list";
$bodyClass = "pf-gift-list";
$pageCss = "/modules/gift-list/gift-list.css";
@@ -42,14 +43,15 @@ $adultsByChildForAnniv = [
'Guim' => $baseAdults,
];
// Labels & Icônes
// Labels & Icônes (Utilisation des clés de traduction pour l'affichage)
$allOccasionLabels = [
'TIO' => 'Tió',
'NOEL' => 'Nadal',
'ROIS' => 'Reis',
'ANNIV' => 'Anniversary',
'SANT' => 'Sant',
'TIO' => tr('gift_occ_tio'),
'NOEL' => tr('gift_occ_noel'),
'ROIS' => tr('gift_occ_rois'),
'ANNIV' => tr('gift_occ_anniv'),
'SANT' => tr('gift_occ_sant'),
];
$occasionIcons = [
'TIO' => '/modules/gift-list/assets/img/tio.png',
'NOEL' => '/modules/gift-list/assets/img/santa.png',
@@ -62,7 +64,6 @@ $tableGifts = 'pf_gifts';
// --- 2. RÉCUPÉRATION DES DONNÉES ---
// Préparation requête
$inMarks = implode(',', array_fill(0, count($allowedOccasions), '?'));
$sql = "SELECT * FROM {$tableGifts} WHERE year = ? AND occasion IN ($inMarks) ORDER BY adult_name, child_name, occasion, created_at";
$stmt = $pdo->prepare($sql);
@@ -70,26 +71,21 @@ $params = array_merge([$year], $allowedOccasions);
$stmt->execute($params);
$gifts = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
// Indexation [occasion][child][adult] pour la matrice
$byOccasion = [];
foreach ($gifts as $gift) {
$byOccasion[$gift['occasion']][$gift['child_name']][$gift['adult_name']][] = $gift;
}
// Occasions à afficher
$occasionsToShow = array_values(array_intersect(array_keys($allOccasionLabels), $allowedOccasions));
// --- 3. CALCUL TRICOUNT (Backend) ---
// On pré-calcule ici pour alléger la vue HTML
// --- 3. CALCUL TRICOUNT ---
$people = $baseAdults;
$adultsInDb = array_column($gifts, 'adult_name');
$payersInDb = array_column($gifts, 'payer_name'); // Peut contenir des NULL si colonne vide, mais array_unique gère
// Nettoyage et fusion des participants
$payersInDb = array_column($gifts, 'payer_name');
$people = array_values(array_unique(array_merge($people, $adultsInDb, $payersInDb)));
$people = array_filter($people); // Enlève les vides éventuels
$people = array_filter($people);
// Matrice de dettes
$matrix = [];
foreach ($people as $p1) {
foreach ($people as $p2) $matrix[$p1][$p2] = 0.0;
@@ -100,7 +96,6 @@ foreach ($gifts as $g) {
$payer = $g['payer_name'] ?? $g['adult_name'];
$amt = (float)$g['amount'];
// Si payé par quelqu'un d'autre que le bénéficiaire (l'adulte responsable)
if ($amt > 0 && $adult && $payer && $adult !== $payer) {
if (isset($matrix[$adult][$payer])) {
$matrix[$adult][$payer] += $amt;
@@ -108,7 +103,6 @@ foreach ($gifts as $g) {
}
}
// Résolution des dettes (Liquidations)
$settlements = [];
$countPeople = count($people);
for ($i = 0; $i < $countPeople; $i++) {
@@ -132,18 +126,18 @@ require __DIR__ . '/header.php';
<div class="pf-container cl-view-<?= htmlspecialchars($currentView) ?>">
<div class="cl-titlebar">
<h1>Llista de regals <?= htmlspecialchars($year) ?></h1>
<div class="cl-view-switch" aria-label="Canvia la vista">
<a href="?view=nadal" class="cl-view-btn <?= $currentView === 'nadal' ? 'is-active' : '' ?>">Nadal</a>
<a href="?view=anniversary" class="cl-view-btn <?= $currentView === 'anniversary' ? 'is-active' : '' ?>">Anniversary</a>
<h1><?= sprintf(tr('gift_main_title'), htmlspecialchars($year)) ?></h1>
<div class="cl-view-switch" aria-label="<?= tr('gift_aria_change_view') ?>">
<a href="?view=nadal" class="cl-view-btn <?= $currentView === 'nadal' ? 'is-active' : '' ?>"><?= tr('gift_view_nadal') ?></a>
<a href="?view=anniversary" class="cl-view-btn <?= $currentView === 'anniversary' ? 'is-active' : '' ?>"><?= tr('gift_view_anniv') ?></a>
</div>
</div>
<section class="pf-section pf-section--panel">
<h2>Vista per festa</h2>
<h2><?= tr('gift_view_by_party') ?></h2>
<?php if (empty($gifts)): ?>
<p class="cl-legend">No hi ha cap regal registrat per a <?= htmlspecialchars($year) ?> en aquesta vista.</p>
<p class="cl-legend"><?= sprintf(tr('gift_no_gifts'), htmlspecialchars($year)) ?></p>
<?php endif; ?>
<?php foreach ($occasionsToShow as $occCode): ?>
@@ -158,12 +152,10 @@ require __DIR__ . '/header.php';
<div class="cl-occasion-children-tables">
<?php foreach ($children as $childName): ?>
<?php
// Détermine quels adultes participent pour cet enfant
$adultsForChild = ($currentView === 'anniversary')
? ($adultsByChildForAnniv[$childName] ?? $baseAdults)
: $baseAdults;
// Prépare listes/total par adulte
$lists = [];
$totals = [];
foreach ($adultsForChild as $adultName) {
@@ -171,7 +163,6 @@ require __DIR__ . '/header.php';
$totals[$adultName] = array_sum(array_column($lists[$adultName], 'amount'));
}
// Calcul hauteur tableau
$counts = array_map('count', $lists);
$maxRowsChild = !empty($counts) ? max($counts) : 0;
?>
@@ -185,7 +176,7 @@ require __DIR__ . '/header.php';
<caption>
<?= htmlspecialchars($childName) ?>
<button type="button" class="cl-child-add-btn" title="Afegeix un regal"
<button type="button" class="cl-child-add-btn" title="<?= tr('gift_add_gift') ?>"
data-year="<?= $year ?>"
data-child="<?= htmlspecialchars($childName) ?>"
data-occasion="<?= htmlspecialchars($occCode) ?>"
@@ -239,7 +230,7 @@ require __DIR__ . '/header.php';
<div class="cl-gift-right">
<span class="cl-gift-amount">(<?= number_format($amt, 0, ',', ' ') ?> €)</span>
<span class="cl-gift-actions">
<button type="button" class="cl-gift-action-btn cl-gift-edit" aria-label="Edita"
<button type="button" class="cl-gift-action-btn cl-gift-edit" aria-label="<?= tr('edit') ?>"
data-id="<?= $giftId ?>"
data-year="<?= $year ?>"
data-child="<?= htmlspecialchars($childName) ?>"
@@ -251,7 +242,7 @@ require __DIR__ . '/header.php';
data-link="<?= htmlspecialchars($gift['product_link'] ?? '') ?>">
</button>
<button type="button" class="cl-gift-action-btn cl-gift-delete" aria-label="Eliminar" data-id="<?= $giftId ?>">
<button type="button" class="cl-gift-action-btn cl-gift-delete" aria-label="<?= tr('delete') ?>" data-id="<?= $giftId ?>">
×
</button>
</span>
@@ -259,7 +250,7 @@ require __DIR__ . '/header.php';
</div>
<?php if (!empty($payer) && $payer !== $gift['adult_name']): ?>
<small style="color:#b91c1c; font-style:italic; display:block; font-size:0.75em;">(pagat per <?= htmlspecialchars($payer) ?>)</small>
<small style="color:#b91c1c; font-style:italic; display:block; font-size:0.75em;">(<?= sprintf(tr('gift_paid_by'), htmlspecialchars($payer)) ?>)</small>
<?php endif; ?>
</div>
<?php else: ?>
@@ -279,9 +270,8 @@ require __DIR__ . '/header.php';
</section>
<section class="pf-section pf-section--panel">
<h2>Resum del pressupost</h2>
<h2><?= tr('gift_summary_title') ?></h2>
<?php
// Calcul agrégé SQL pour vérification
$stmtSum = $pdo->prepare("SELECT adult_name, child_name, occasion, SUM(amount) AS total FROM {$tableGifts} WHERE year = ? AND occasion IN ($inMarks) GROUP BY adult_name, child_name, occasion ORDER BY adult_name, child_name, occasion");
$stmtSum->execute($params);
$sums = $stmtSum->fetchAll(PDO::FETCH_ASSOC);
@@ -289,7 +279,7 @@ require __DIR__ . '/header.php';
<div class="cl-budget-wrapper">
<table class="pf-table pf-table--compact">
<thead>
<tr><th>Adult</th><th>Infant</th><th>Festa</th><th>Total</th></tr>
<tr><th><?= tr('gift_col_adult') ?></th><th><?= tr('gift_col_child') ?></th><th><?= tr('gift_col_party') ?></th><th>Total</th></tr>
</thead>
<tbody>
<?php foreach ($sums as $row): ?>
@@ -311,7 +301,7 @@ require __DIR__ . '/header.php';
<table class="pf-table pf-table--compact cl-debt-matrix">
<thead>
<tr>
<th class="cl-matrix-corner"><span>Deutor ↓</span><span>Creditor →</span></th>
<th class="cl-matrix-corner"><span><?= tr('gift_debtor') ?> ↓</span><span><?= tr('gift_creditor') ?> →</span></th>
<?php foreach ($people as $p): ?><th><?= htmlspecialchars($p) ?></th><?php endforeach; ?>
</tr>
</thead>
@@ -334,24 +324,24 @@ require __DIR__ . '/header.php';
</table>
</div>
<h3 style="margin-top:16px; font-size:1.1rem; color:#374151;">Liquidacions</h3>
<h3 style="margin-top:16px; font-size:1.1rem; color:#374151;"><?= tr('gift_liquidations') ?></h3>
<?php if (empty($settlements)): ?>
<p class="cl-legend">Cap deute pendent.</p>
<p class="cl-legend"><?= tr('gift_no_debt') ?></p>
<?php else: ?>
<ul class="hol-list">
<?php foreach ($settlements as $s): ?>
<li><strong><?= htmlspecialchars($s['from']) ?></strong> ha de pagar <strong><?= number_format($s['amount'], 2, ',', ' ') ?> €</strong> a <?= htmlspecialchars($s['to']) ?></li>
<li><strong><?= htmlspecialchars($s['from']) ?></strong> <?= tr('gift_owes') ?> <strong><?= number_format($s['amount'], 2, ',', ' ') ?> €</strong> <?= tr('gift_to') ?> <?= htmlspecialchars($s['to']) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</section>
<section class="pf-section pf-section--panel">
<h2>Llista detallada de regals</h2>
<h2><?= tr('gift_detailed_list') ?></h2>
<div class="cl-detail-wrapper">
<table class="pf-table pf-table--compact">
<thead>
<tr><th>Adult</th><th>Infant</th><th>Festa</th><th>Regal</th><th>€</th><th>Enllaç</th></tr>
<tr><th><?= tr('gift_col_adult') ?></th><th><?= tr('gift_col_child') ?></th><th><?= tr('gift_col_party') ?></th><th><?= tr('gift_col_gift') ?></th><th>€</th><th><?= tr('gift_col_link') ?></th></tr>
</thead>
<tbody>
<?php foreach ($gifts as $g): ?>
@@ -375,6 +365,14 @@ require __DIR__ . '/header.php';
</div>
<script>
// Pont de traduction JS
window.I18N = {
...window.I18N,
'gift_modal_title_add': "<?= tr('gift_modal_title_add') ?>",
'gift_modal_title_edit': "<?= tr('gift_modal_title_edit') ?>",
'gift_confirm_delete': "<?= tr('gift_confirm_delete') ?>"
};
document.addEventListener('DOMContentLoaded', function () {
const modal = document.getElementById('cl-gift-modal');
const backdrop = modal ? modal.querySelector('.cl-modal-backdrop') : null;
@@ -383,9 +381,10 @@ document.addEventListener('DOMContentLoaded', function () {
function toggleModal(show) {
if (!modal) return;
modal.classList.toggle('cl-open', show);
if(show) document.body.classList.add('no-scroll');
else document.body.classList.remove('no-scroll');
}
// Gestion des options de select (Adultes dynamiques selon l'enfant)
function populateSelects(adults) {
const selects = [document.getElementById('clm-adult'), document.getElementById('clm-payer')];
selects.forEach(sel => {
@@ -409,7 +408,6 @@ document.addEventListener('DOMContentLoaded', function () {
populateSelects(adults);
// Valeurs par défaut
document.getElementById('clm-action').value = 'create';
document.getElementById('clm-id').value = '';
document.getElementById('clm-year').value = d.year;
@@ -420,7 +418,7 @@ document.addEventListener('DOMContentLoaded', function () {
document.getElementById('clm-amount').value = '';
document.getElementById('clm-link').value = '';
document.getElementById('cl-modal-title').textContent = `Afegeix un regal per ${d.child}`;
document.getElementById('cl-modal-title').textContent = tr('gift_modal_title_add').replace('%s', d.child);
toggleModal(true);
});
});
@@ -431,12 +429,9 @@ document.addEventListener('DOMContentLoaded', function () {
if (!btn) return;
const d = btn.dataset;
// Pour l'édition, on s'assure que l'adulte actuel est dans la liste (même si pas standard)
const adultSelect = document.getElementById('clm-adult');
const payerSelect = document.getElementById('clm-payer');
// On ne recharge pas toute la liste adults ici car complexe à récupérer du DOM,
// on ajoute juste l'option si manquante.
[adultSelect, payerSelect].forEach(sel => {
const val = (sel === adultSelect) ? d.adult : (d.payer || d.adult);
if (!Array.from(sel.options).some(o => o.value === val)) {
@@ -458,7 +453,7 @@ document.addEventListener('DOMContentLoaded', function () {
document.getElementById('clm-amount').value = d.amount;
document.getElementById('clm-link').value = d.link;
document.getElementById('cl-modal-title').textContent = `Edita el regal`;
document.getElementById('cl-modal-title').textContent = tr('gift_modal_title_edit');
toggleModal(true);
});
@@ -467,7 +462,7 @@ document.addEventListener('DOMContentLoaded', function () {
const btn = e.target.closest('.cl-gift-delete');
if (!btn) return;
if (confirm('Vols eliminar aquest regal?')) {
if (confirm(tr('gift_confirm_delete'))) {
const form = document.getElementById('cl-delete-form');
document.getElementById('cld-id').value = btn.dataset.id;
form.submit();
@@ -483,9 +478,9 @@ document.addEventListener('DOMContentLoaded', function () {
<div id="cl-gift-modal" class="cl-modal" aria-hidden="true">
<div class="cl-modal-backdrop"></div>
<div class="cl-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="cl-modal-title">
<div class="cl-modal-dialog pf-modal-content" role="dialog" aria-modal="true" aria-labelledby="cl-modal-title">
<form method="post" action="/modules/gift-list/save-gift.php" class="cl-modal-form">
<h3 id="cl-modal-title">Regal</h3>
<h3 id="cl-modal-title" style="margin-top:0;"><?= tr('gift_col_gift') ?></h3>
<input type="hidden" name="year" id="clm-year">
<input type="hidden" name="child_name" id="clm-child">
@@ -493,34 +488,34 @@ document.addEventListener('DOMContentLoaded', function () {
<input type="hidden" name="action" id="clm-action">
<input type="hidden" name="gift_id" id="clm-id">
<label class="clm-label">
Adult
<select name="adult_name" id="clm-adult" required></select>
</label>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label"><?= tr('gift_col_adult') ?></label>
<select name="adult_name" id="clm-adult" class="pf-input" required></select>
</div>
<label class="clm-label">
Pagat per
<select name="payer_name" id="clm-payer" required></select>
</label>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label"><?= tr('gift_modal_payer') ?></label>
<select name="payer_name" id="clm-payer" class="pf-input" required></select>
</div>
<label class="clm-label">
Nom del regal
<input type="text" name="gift_description" id="clm-gift" placeholder="p. ex., Lego Star Wars" required>
</label>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label"><?= tr('gift_modal_gift_name') ?></label>
<input type="text" name="gift_description" id="clm-gift" class="pf-input" placeholder="<?= tr('gift_modal_ph_name') ?>" required>
</div>
<label class="clm-label">
Preu (€)
<input type="number" name="amount" id="clm-amount" placeholder="49.99" step="0.01" min="0">
</label>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label"><?= tr('gift_modal_price') ?></label>
<input type="number" name="amount" id="clm-amount" class="pf-input" placeholder="49.99" step="0.01" min="0">
</div>
<label class="clm-label">
Enllaç (opcional)
<input type="url" name="product_link" id="clm-link" placeholder="https://...">
</label>
<div class="form-group" style="margin-bottom:25px;">
<label class="pf-label"><?= tr('gift_modal_link') ?></label>
<input type="url" name="product_link" id="clm-link" class="pf-input" placeholder="https://...">
</div>
<div class="cl-modal-actions">
<button type="button" class="clm-cancel">Cancel·la</button>
<button type="submit" class="clm-ok">Guardar</button>
<div class="modal-footer" style="padding-top:15px; border-top:1px solid #e2e8f0; display:flex; justify-content:flex-end; gap:10px;">
<button type="button" class="clm-cancel pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="clm-ok pf-btn"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
+26 -8
View File
@@ -4,7 +4,10 @@ if (session_status() === PHP_SESSION_NONE) session_start();
// 1. Inclusion du moteur de traduction
require_once __DIR__ . '/includes/i18n.php';
// Petite fonction pour changer de langue sans perdre les paramètres de l'URL (ex: id=5)
/**
* Génère une URL avec le paramètre de langue mis à jour
* sans perdre les autres paramètres (ex: id, tab, etc.)
*/
function getLangUrl($newLang) {
$params = $_GET;
$params['lang'] = $newLang;
@@ -52,13 +55,13 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; // Récupération de la langue act
<div class="pf-user-badge">
<?= htmlspecialchars($_SESSION['user']['display_name'] ?? $_SESSION['user']['username']) ?>
</div>
<a href="/logout.php" class="pf-logout-btn">Déconnexion</a>
<a href="/logout.php" class="pf-logout-btn"><?= tr('btn_logout') ?></a>
<?php else: ?>
<a href="/login.php" class="pf-nav-link">Connexion</a>
<a href="/login.php" class="pf-nav-link"><?= tr('btn_login') ?></a>
<?php endif; ?>
</div>
<button class="pf-burger-btn" aria-label="Menu">☰</button>
<button class="pf-burger-btn" aria-label="<?= tr('aria_menu') ?>">☰</button>
</header>
<div class="pf-mobile-menu">
@@ -67,24 +70,39 @@ $currentLang = $_SESSION['app_lang'] ?? 'fr'; // Récupération de la langue act
<a href="/budget.php" class="pf-mobile-nav-link"><?= tr('menu_budget') ?></a>
<a href="/holidays.php" class="pf-mobile-nav-link"><?= tr('menu_holidays') ?></a>
<a href="/gift-list.php" class="pf-mobile-nav-link"><?= tr('menu_gifts') ?></a>
<hr style="width: 80%; border: 0; border-top: 1px solid #eee; margin: 10px 0;">
<?php if (isset($_SESSION['user'])): ?>
<a href="/logout.php" class="pf-mobile-nav-link pf-mobile-logout">Se déconnecter</a>
<a href="/logout.php" class="pf-mobile-nav-link pf-mobile-logout"><?= tr('btn_logout') ?></a>
<?php else: ?>
<a href="/login.php" class="pf-mobile-nav-link">Se connecter</a>
<a href="/login.php" class="pf-mobile-nav-link"><?= tr('btn_login') ?></a>
<?php endif; ?>
</div>
<main class="pf-main">
<script>
// Script simple pour le menu mobile
window.I18N_LANG = '<?= $currentLang === "ca" ? "ca-ES" : "fr-FR" ?>';
window.I18N = <?php echo json_encode($current_translations_array ?? []); ?>;
function tr(key) {
return window.I18N[key] || key;
}
// Gestion du menu mobile
const burgerBtn = document.querySelector('.pf-burger-btn');
const mobileMenu = document.querySelector('.pf-mobile-menu');
if(burgerBtn) {
if(burgerBtn && mobileMenu) {
burgerBtn.addEventListener('click', () => {
const isOpen = mobileMenu.classList.toggle('is-open');
burgerBtn.textContent = isOpen ? '✕' : '☰';
// Bonus sécurité : empêcher le scroll si le menu est ouvert
if (isOpen) {
document.body.classList.add('no-scroll');
} else {
document.body.classList.remove('no-scroll');
}
});
}
</script>
+7 -3
View File
@@ -4,14 +4,18 @@
require __DIR__ . '/includes/auth.php';
require_login('/login.php');
require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php'; // Toujours s'assurer de l'accès à tr()
if (session_status() === PHP_SESSION_NONE) { session_start(); }
// 1. On récupère la vue demandée (par défaut 'list')
$tab = $_GET['tab'] ?? 'list';
// 2. Configuration des variables pour le header
$pageTitle = ($tab === 'holiday_detail') ? "PachaFamily - Détail du voyage" : "PachaFamily - Mes Vacances";
// 2. Configuration des variables pour le header (Titres traduits)
$pageTitle = ($tab === 'holiday_detail')
? tr('hdl_title_detail')
: tr('hdl_title_list');
$activePage = "holidays";
$bodyClass = "pf-holidays";
$pageCss = "/modules/holidays/holidays.css";
@@ -27,7 +31,7 @@ if ($tab === 'holiday_detail' && isset($_GET['id'])) {
require __DIR__ . '/modules/holidays/views/list.php';
}
// 4. Inclusion du JS global du module (s'applique aux deux vues)
// 4. Inclusion du JS global du module (Pont i18n déjà géré dans le header)
echo '<script src="/modules/holidays/holidays.js"></script>';
require __DIR__ . '/footer.php';
+422 -20
View File
@@ -1,15 +1,215 @@
<?php
// Dictionnaire Catalan
return [
// ==========================================
// GLOBAL & NAVIGATION
// ==========================================
'menu_home' => 'Inici',
'menu_budget' => 'Pressupost',
'menu_calendar' => 'Calendari',
'menu_gifts' => 'Regals',
'menu_budget' => 'Pressupost',
'menu_holidays' => 'Viatges',
'outbound_trip' => 'Anada',
'return_trip' => 'Tornada',
'menu_gifts' => 'Regals',
'nav_home' => 'Inici',
'nav_calendar' => 'Calendari',
'nav_budget' => 'Pressupost',
'nav_holidays' => 'Viatges',
'nav_gifts' => 'Regals',
'aria_menu' => 'Menú mòbil',
// Boutons et actions globales
'actions' => 'Accions',
'edit' => 'Modificar',
'delete' => 'Suprimir',
'save' => 'Desar',
'cancel' => 'Cancel·lar',
'btn_save' => 'Guardar',
'btn_cancel' => 'Cancel·lar',
'btn_delete' => 'Eliminar',
'btn_close' => 'Tancar',
'btn_back' => '◀ Tornar',
'btn_edit_bases' => '⚙️ Modificar les bases',
// Mots génériques & Erreurs
'error' => 'Error.',
'confirm_delete' => 'Estàs segur que vols suprimir aquest element?',
// Mois & Jours
'month_01' => 'Gener', 'month_02' => 'Febrer', 'month_03' => 'Març',
'month_04' => 'Abril', 'month_05' => 'Maig', 'month_06' => 'Juny',
'month_07' => 'Juliol','month_08' => 'Agost', 'month_09' => 'Setembre',
'month_10' => 'Octubre','month_11' => 'Novembre','month_12' => 'Desembre',
'day_mon' => 'Dl', 'day_tue' => 'Dt', 'day_wed' => 'Dc',
'day_thu' => 'Dj', 'day_fri' => 'Dv', 'day_sat' => 'Ds', 'day_sun' => 'Dg',
// ==========================================
// AUTHENTIFICATION / LOGIN
// ==========================================
'login_title' => 'Connexió - PachaFamily',
'login_header' => 'Connexió',
'label_username' => "Nom d'usuari",
'label_password' => 'Contrasenya',
'placeholder_username' => 'ex: pacha',
'btn_login' => 'Connexió',
'btn_login_submit' => 'Connectar-se',
'logout' => 'Tancar sessió',
'btn_logout' => 'Tancar sessió',
'error_missing_fields' => 'Camps obligatoris.',
'error_invalid_credentials' => 'Identificadors incorrectes.',
// ==========================================
// ACCUEIL / INDEX
// ==========================================
'home_title' => 'PachaFamily - Inici',
'home_welcome' => 'Benvinguts a PachaFamily',
'home_subtitle' => "Centre de control de l'organització familiar.",
'home_logged_as' => 'Connectat com a',
'home_modules_title' => 'Mòduls',
'mod_calendar_name' => 'Calendari Familiar',
'mod_calendar_desc' => "Gestioneu les vacances, les modalitats de cura i l'organització setmanal.",
'mod_holidays_name' => 'Vacances',
'mod_holidays_desc' => 'Planifiqueu les vostres properes escapades, destinacions i idees de viatge.',
'mod_gifts_name' => 'Regals',
'mod_gifts_desc' => "Planifiqueu els regals per a cada infant, per Nadal i els aniversaris.",
'mod_budget_name' => 'Pressupost',
'mod_budget_desc' => 'Seguiment de despeses fixes, ingressos i balanç del compte familiar.',
'cta_open' => 'Obrir',
'cta_explore' => 'Explorar',
'cta_view_lists' => 'Veure les llistes',
'cta_manage' => 'Gestionar',
// ==========================================
// FAMILY CALENDAR
// ==========================================
'fc_page_title' => 'PachaFamily - Calendari',
'fc_main_header' => 'Calendari Familiar',
'fc_btn_correct_balances' => 'Corregir els saldos',
'fc_btn_school_holidays' => 'Vacances escolars',
'fc_modal_holidays_title' => 'Vacances Escolars (Zona C)',
'fc_modal_snap_title' => 'Ajustar un saldo',
'fc_col_period' => 'Període',
'fc_col_from' => 'Des del',
'fc_col_to' => 'Fins al',
'fc_label_person' => 'Persona',
'fc_label_leave_type' => 'Tipus de permís',
'fc_label_apply_date' => "Data d'aplicació",
'fc_snap_date_hint' => "Trieu preferiblement el primer dia d'un mes",
'fc_label_remaining_balance'=> 'Saldo restant en aquesta data',
'fc_placeholder_balance' => 'ex: 12.5',
'fc_btn_save_snap' => 'Desar el correctiu',
'fc_view_1m' => '1 mes',
'fc_view_2m' => '2 mesos',
'fc_view_year' => 'Any',
'fc_weekly_planning' => 'Planificació setmanal',
'fc_col_month' => 'Mes',
'fc_col_week_short' => 'Set.',
'fc_col_av' => 'Av.',
'fc_col_use' => 'Use',
'fc_legend_title' => 'Llegenda',
'fc_summary_title' => 'Resum',
'fc_summ_year' => 'Any',
'fc_summ_month' => 'Mes',
'fc_school_year' => 'Curs escolar',
'fc_unit_days' => 'dies',
'fc_menu_carole' => 'Permisos Carole',
'fc_clear' => 'Esborrar',
'fc_menu_kids_leaves' => 'Permisos nens',
'leg_school_holidays' => 'Vacances Escolars',
'leg_public_holiday' => 'Festiu',
'leg_off_carole' => 'Off Carole',
'leg_extra_off' => 'Extra Off',
'leg_centre' => 'Casal',
'leg_avis' => 'Avís',
'leg_pep_sick' => 'Pep Malalt',
'leg_presence' => 'Presència',
'vac_toussaint' => 'Vacances de Tots Sants',
'vac_noel' => 'Vacances de Nadal',
'vac_hiver' => "Vacances d'Hivern",
'vac_printemps' => 'Vacances de Primavera',
'vac_ascension' => "Pont de l'Ascensió",
'vac_ete' => "Vacances d'Estiu",
'btn_off' => 'Off',
'btn_extra' => 'Extra',
// ==========================================
// HOLIDAYS (VOYAGES)
// ==========================================
'hdl_title_list' => 'PachaFamily - Les meves vacances',
'hdl_title_detail' => 'PachaFamily - Detall del viatge',
'hdl_main_title' => 'Les meves vacances',
'hdl_filter_all' => 'Mostrar-ho tot',
'hdl_filter_year' => 'Any',
'hdl_left_to_pay_label' => 'Resta per pagar',
'hdl_btn_create' => 'Crear un viatge',
'hdl_no_active_trips' => 'Cap viatge en curs per aquest període. Planifiquem alguna cosa!',
'hdl_history_title' => 'Historial',
'hdl_dates_to_define' => 'Dates per definir',
'hdl_total_budget' => 'Pressupost Total',
'hdl_paid' => 'Pagat',
'hdl_saved' => 'Finançat',
'hdl_left_to_pay' => 'Resta per pagar',
'hdl_status_booked' => 'Confirmat',
'hdl_status_planned' => 'Planificat',
'hdl_status_draft' => 'Esborrany',
'hdl_status_passed' => 'Passat',
'hdl_status_archived' => 'Arxivat',
'hdl_modal_title' => 'Planificar el viatge',
'hdl_label_name' => 'Nom del viatge',
'hdl_ph_name' => 'Ex: Octubre - Portugal',
'hdl_label_status' => 'Estat',
'hdl_label_period' => 'Període (Text lliure)',
'hdl_ph_period' => 'Ex: Octubre 2026',
'hdl_label_from' => 'Des del',
'hdl_label_to' => 'Fins al',
'hdl_cat_transport' => 'Transport',
'hdl_add_transport' => 'Afegir un transport',
'hdl_cat_accommodation' => 'Allotjament',
'hdl_add_accommodation' => 'Afegir un allotjament',
'hdl_cat_activity' => 'Activitat',
'hdl_add_activity' => 'Afegir una activitat',
'hdl_label_budget_food' => 'Pressupost Menjar i Beguda (€)',
'hdl_label_budget_extras' => 'Pressupost Extras (€)',
'hdl_label_notes' => 'Notes',
'hdl_ph_notes' => 'Idees soltes...',
'hdl_label_budget_food_extras' => 'Pressupost Menjar i Extres',
'hdl_map_itinerary' => 'Itinerari',
'hdl_outbound' => 'Anada',
'hdl_return' => 'Tornada',
'hdl_btn_add_step' => 'Afegir una etapa',
'hdl_step_details' => 'Detalls del roadtrip',
'hdl_no_steps' => 'Cap etapa definida. Comenceu per afegir-ne una!',
'hdl_general_costs' => 'Despeses generals i Pressupostos',
'hdl_food_bev' => 'Menjar i Beguda',
'hdl_extras' => 'Extres',
'hdl_view_planning' => 'Veure la planificació',
'hdl_passing_point' => 'Punt de pas (sense despeses)',
'hdl_search_location' => 'Cercar un lloc',
'hdl_ph_search' => 'Ciutat, hotel, adreça...',
'hdl_label_step_name' => "Nom de l'etapa",
'hdl_label_arrival' => 'Arribada',
'hdl_label_departure' => 'Sortida',
'hdl_planned_expenses' => 'Despeses previstes per a aquesta etapa',
'hdl_btn_add_expense' => '+ Afegir una despesa',
'hdl_save_fav' => 'Desar als meus preferits',
'hdl_planning_title' => 'Planificació de letapa',
'hdl_to_pay' => 'Per pagar',
'hdl_js_search_loading' => 'Cerca en curs...',
'hdl_js_no_result' => 'No shan trobat resultats.',
'hdl_js_network_error' => 'Error de connexió al servei de mapes.',
'hdl_js_confirm_del_trip' => 'Estàs segur que vols suprimir aquest viatge sencer?',
'hdl_js_confirm_del_step' => 'Suprimir aquesta etapa?',
'hdl_js_step_label' => 'Etapa',
'hdl_js_ph_expense_name' => 'Ex: Hotel, Peatge...',
'hdl_quick_edit_title' => 'Modificació ràpida',
'hdl_js_edit_step' => 'Modificar letapa',
'hdl_js_missing_dates_title'=> 'Dates mancants',
'hdl_js_missing_dates_msg' => 'Cal definir una data darribada i sortida per organitzar la planificació.',
'hdl_to_place' => 'Per situar',
// Anciennes clés Holidays non préfixées (conservades)
'outbound_trip' => 'Anada',
'return_trip' => 'Tornada',
'period' => 'Període',
'budget_food' => 'Pressupost Menjar i Extres',
'total_cost' => 'Cost Total Estimat',
@@ -17,16 +217,10 @@ return [
'provisioned' => 'Provisionat',
'left_to_pay' => 'Pendent de pagament',
'map_itinerary' => '🗺️ Itinerari',
'outbound_trip' => 'Anada',
'return_trip' => 'Tornada',
'place_step' => '📍 Afegir una etapa',
'step_details' => '📝 Detall de les etapes',
'no_steps' => 'Cap etapa planificada.',
'travel_notes' => '📝 Notes del viatge',
'edit' => 'Modificar',
// --- Paraules diverses & Tooltips ---
'error' => 'Error.',
'to_be_defined' => 'A definir',
'funded' => 'Finançat (Provisió)',
'view_planning' => 'Veure el planning',
@@ -34,8 +228,6 @@ return [
'from_date' => 'Del',
'to_date' => 'al',
'passing_point' => 'Punt de pas (Cap despesa)',
// --- Modal : Afegir / Editar una etapa ---
'search_location' => 'Cercar una ubicació geogràfica',
'search_placeholder' => 'Ex: París, Ibis Barcelona...',
'step_name' => 'Nom de l\'etapa',
@@ -44,15 +236,7 @@ return [
'planned_expenses' => 'Despeses previstes',
'add_expense' => '+ Afegir una despesa',
'save_favorite' => 'Guardar als meus preferits',
// --- Botons genèrics ---
'btn_delete' => 'Eliminar',
'btn_cancel' => 'Cancel·lar',
'btn_save' => 'Guardar',
'btn_close' => 'Tancar',
'planning_title' => 'Planning',
// --- Javascript (holidays.js) ---
'modal_plan_trip' => 'Planificar el viatge',
'modal_quick_edit' => 'Edició ràpida',
'err_modal_missing' => 'Error: La modal holidayModal no es troba.',
@@ -75,4 +259,222 @@ return [
'missing_dates_msg' => 'Si us plau, modifica aquesta etapa (✏️) i omple la seva <strong>Data d\'arribada i de sortida</strong>.',
'to_place' => '📥 A col·locar',
// ==========================================
// BUDGET (GLOBAL & CATEGORIES)
// ==========================================
'budget_page_title' => 'PachaFamily - Pressupost',
'budget_main_header' => 'Gestió del Pressupost',
'budget_tab_tracking' => 'Seguiment Mensual',
'budget_tab_prev' => 'Pressupost 2026',
'budget_tab_savings' => 'Estalvis',
'budget_tab_recap' => 'Resum',
'budget_err_file_not_found' => 'Vaja! Fitxer no trobat.',
'budget_err_file_detail' => 'Comproveu que el fitxer views/%s.php existeix.',
'budget_err_not_authorized' => 'Pàgina no autoritzada.',
'cat_income' => 'Ingressos',
'cat_fmcg' => 'Compres (FMCG)',
'cat_fuel' => 'Benzina',
'cat_school' => 'Escola / Llar dinfants',
'cat_fixed' => 'Despeses Fixes',
'cat_others' => 'Altres / Imprevistos',
'cat_savings' => 'Estalvis',
// --- BUDGET : SUIVI ---
'bud_rem_school' => 'Escola (Resta estimada)',
'bud_rem_fmcg' => 'Compres (Resta estimada)',
'bud_rem_fuel' => 'Benzina (Resta estimada)',
'bud_closed_archived' => 'Aquest mes està tancat i arxivat.',
'bud_budget_of' => 'Pressupost:',
'bud_confirm_close' => 'Tancar aquest mes? Això congelarà els saldos actuals i canviarà el seguiment per defecte al mes vinent.',
'bud_close_btn' => 'Tancar',
'bud_confirm_reopen' => 'Reobrir aquest mes? Els seus saldos es recalcularan en directe.',
'bud_reopen_btn' => 'Reobrir',
'bud_active_month_btn' => 'Mes Actiu',
'bud_bank_balance' => 'Saldo bancari',
'bud_frozen' => 'Congelat',
'bud_current' => 'Actual',
'bud_update_btn' => 'Act.',
'bud_theoretical_balance' => 'Saldo teòric final de mes',
'bud_upcoming_charges' => 'Càrrecs pendents',
'bud_all_paid' => 'Tot està pagat!',
'bud_capacity_tooltip' => 'Càlcul: Report Mes Anterior (%s€) + Sous (%s€) + Altres ingressos (%s€)',
'bud_max_capacity' => 'Capacitat Màx',
'bud_actual' => 'Actual',
'bud_theoretical' => 'Teòric',
'bud_import_csv' => 'Importar CSV',
'bud_csv_file' => 'Fitxer CSV (Punt i coma)',
'bud_preview' => 'Previsualitzar',
'bud_validate_import' => 'Validar importació',
'bud_assign_month' => 'Assignar al mes',
'bud_op_date' => 'Data Operació',
'bud_label' => 'Concepte',
'bud_amount' => 'Import',
'bud_category' => 'Categoria',
'bud_ignore' => 'Ignorar',
'bud_to_define' => 'Per definir',
'bud_is_charge' => 'Despesa?',
'bud_is_income' => 'Ingrés?',
'btn_import' => 'Importar',
'bud_no_lines' => 'Cap línia.',
'bud_confirm_delete' => 'Suprimir aquesta línia?',
'bud_new_transaction' => 'Nova transacció',
'bud_assign_to_month' => 'Assignar al mes de gestió',
'bud_real_date' => 'Data real de l\'operació',
'bud_type' => 'Tipus',
'bud_type_expense' => 'Despesa',
'bud_type_income' => 'Ingrés / Reemborsament',
'bud_beneficiary' => 'Beneficiari',
'bud_fixed_charge' => 'Despesa Fixa',
'bud_expected_income' => 'Ingrés Esperat',
'bud_amount_eur' => 'Import (€)',
'bud_update_balance' => 'Act. Saldo Bancari',
'bud_balance_eur' => 'Saldo (€)',
'bud_add_title' => 'Afegir',
'bud_edit_title' => 'Modificar la transacció',
'bud_to_define_js' => 'per definir',
// --- BUDGET : RECAP ---
'bud_recap_monthly_title' => 'Resum Mensual',
'bud_recap_btn_add' => 'Afegir una despesa / ingrés',
'bud_planned_amount' => 'Import Previst',
'bud_day' => 'Dia',
'bud_month_state' => 'Estat del mes',
'bud_est_short' => 'Est.',
'bud_reg_planned_in' => 'Regularització prevista al %s',
'bud_bonus' => 'Bonificació',
'bud_overrun' => 'Excés',
'bud_remaining' => 'Resta',
'bud_exact_amount' => 'Import exacte',
'bud_per_month_short' => 'És a dir',
'bud_month_short' => 'mes',
'bud_freq_mensuel' => 'Mensual',
'bud_freq_annuel' => 'Anual',
'bud_state_validated' => 'Validat',
'bud_state_partial' => 'Parcial',
'bud_state_waiting' => 'En espera',
'bud_total_income_smoothed' => 'Total Ingressos (Prorratejats)',
'bud_total_expenses_smoothed'=> 'Total Despeses (Prorratejades)',
'bud_theoretical_balance_recap'=> 'Equilibri teòric',
'bud_recap_footer_note' => 'L\'estat del pagament s\'actualitza automàticament en funció de les operacions importades a la pestanya "Seguiment Mensual".',
'bud_recap_modal_add' => 'Afegir un element',
'bud_recap_modal_edit' => 'Modificar',
'bud_label_name' => 'Nom',
'bud_ph_item_name' => 'Ex: Lloguer, Sou...',
'bud_label_keywords' => 'Paraules clau (Detecció Auto)',
'bud_ph_keywords' => 'Ex: NETFLIX, PRIME VIDEO (separades per coma)',
'bud_help_keywords' => 'Si una despesa del mes conté aquesta paraula, la línia passarà a "Validat".',
'bud_label_day' => 'Dia (1-31)',
'bud_cat_expense' => 'Despesa',
'bud_label_frequency' => 'Freqüència',
'bud_label_amount_type' => 'Tipus d\'import',
'bud_type_fixed' => 'Fix (Factura)',
'bud_type_variable' => 'Variable (Estimació)',
'bud_label_regularization' => 'Regularització',
'bud_reg_none' => 'Cap',
'bud_recap_confirm_delete' => 'Segur que vols suprimir aquest element?',
'bud_err_delete' => 'Error en suprimir.',
// --- BUDGET : EPARGNE ---
'bud_sav_add_month' => 'Introduir un mes',
'bud_sav_add_one_month' => '+1 Mes',
'bud_sav_no_data' => 'Cap dada per a %s.',
'bud_sav_post_month' => 'Concepte / Mes',
'bud_sav_from_date' => 'Des del %s',
'bud_sav_edit_modal' => 'Modificar amb Modal',
'bud_sav_delete_month' => 'Suprimir tot el mes',
'bud_sav_total_bank' => 'Total Banc',
'bud_sav_extra' => 'Extra',
'bud_sav_modal_title_add' => 'Introduir un mes',
'bud_sav_modal_title_edit' => 'Modificar:',
'bud_sav_month_concerned' => 'Mes afectat',
'bud_sav_total_bank_eur' => 'Total al Banc (€)',
'bud_sav_ventilation' => 'Desglossament',
'bud_sav_adj_help' => 'Utilitzeu l\'ajust (+/-) per recalcular automàticament.',
'bud_sav_add_line' => 'Línia',
'bud_sav_current' => 'Actual',
'bud_sav_adjust' => 'Ajust (+/-)',
'bud_sav_new' => 'Nou',
'bud_sav_sum_mode_title' => 'Activar la suma ràpida',
'bud_sav_selection' => 'Selecció:',
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
'bud_sav_saving' => 'Desant...',
'bud_err_tech' => 'S\'ha produït un error tècnic.',
'bud_sav_confirm_delete_month' => 'Suprimir TOT el mes de %m per a %o?',
'bud_sav_prompt_duplicate' => "Vols copiar les dades de %s a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:",
'bud_err_server' => 'Error del servidor: ',
'bud_err_network_dup' => 'Error de xarxa en duplicar.',
// --- BUDGET : PREVISIONNEL ---
'bud_prev_incomes' => 'Ingressos',
'bud_prev_person' => 'Persona',
'bud_prev_salary' => 'Sou',
'bud_prev_monthly_payment' => 'Mensualitat',
'bud_prev_joint_account' => 'Cte Comú',
'bud_prev_func_expenses' => 'Desp. Func.',
'bud_prev_perso_savings' => 'Est. Perso',
'bud_prev_family_savings' => 'Est. Família',
'bud_prev_available' => 'Dispo',
'bud_prev_perso_remaining' => 'Restant Perso',
'bud_prev_budget_alloc' => 'Distribució Pressupostària',
'bud_prev_today' => 'Avui',
'bud_prev_new_line' => 'Nova Línia',
'bud_prev_remaining' => 'Restant',
'bud_prev_confirm_del_line' => 'Suprimir aquesta línia i tot el seu historial?',
'bud_prev_notes_for' => 'Notes per a',
'bud_prev_saved' => 'Desat',
'bud_prev_notes_ph' => 'Escriu les teves observacions, recordatoris o comentaris per a aquest mes aquí...',
'bud_prev_save_note' => 'Desar la nota',
'bud_prev_transfers_to_make' => 'Transferències a fer',
'bud_prev_destination' => 'Destinació',
'bud_prev_done' => 'FET',
'bud_prev_validate' => 'Validar',
'bud_prev_grand_total' => 'GRAN TOTAL',
'bud_prev_new_line_title' => 'Nova Línia de Pressupost',
'bud_prev_edit_line_title' => 'Modificar la línia',
'bud_prev_label_target' => 'Objectiu (Destinació)',
'bud_prev_choose' => 'Tria',
'bud_prev_link_holiday' => 'Enllaçar a un viatge (Opcional)',
'bud_prev_no_link' => 'No associar',
'bud_prev_err_no_history' => 'Impossible: no hi ha historial disponible per copiar.',
'bud_prev_confirm_copy' => "Vols copiar les dades de %s a %t?\n\n⚠️ Això sobreescriurà tots els valors ja presents per a %t.",
'bud_prev_confirm_transfers' => "Confirmes que %p ha fet totes les transferències per a %m?\n\nAixò actualitzarà l'Estalvi automàticament.",
// ==========================================
// CADEAUX (GIFTS)
// ==========================================
'gift_page_title' => 'PachaFamily - Llista de regals',
'gift_occ_tio' => 'Tió',
'gift_occ_noel' => 'Nadal',
'gift_occ_rois' => 'Reis',
'gift_occ_anniv' => 'Aniversari',
'gift_occ_sant' => 'Sant',
'gift_main_title' => 'Llista de regals %s',
'gift_aria_change_view' => 'Canvia la vista',
'gift_view_nadal' => 'Nadal',
'gift_view_anniv' => 'Aniversaris',
'gift_view_by_party' => 'Vista per festa',
'gift_no_gifts' => 'No hi ha cap regal registrat per a %s en aquesta vista.',
'gift_add_gift' => 'Afegeix un regal',
'gift_paid_by' => 'pagat per %s',
'gift_summary_title' => 'Resum del pressupost',
'gift_col_adult' => 'Adult',
'gift_col_child' => 'Infant',
'gift_col_party' => 'Festa',
'gift_debtor' => 'Deutor',
'gift_creditor' => 'Creditor',
'gift_liquidations' => 'Liquidacions',
'gift_no_debt' => 'Cap deute pendent.',
'gift_owes' => 'ha de pagar',
'gift_to' => 'a',
'gift_detailed_list' => 'Llista detallada de regals',
'gift_col_gift' => 'Regal',
'gift_col_link' => 'Enllaç',
'gift_modal_title_add' => 'Afegeix un regal per a %s',
'gift_modal_title_edit' => 'Edita el regal',
'gift_modal_payer' => 'Pagat per',
'gift_modal_gift_name' => 'Nom del regal',
'gift_modal_ph_name' => 'p. ex., Lego Star Wars',
'gift_modal_price' => 'Preu (€)',
'gift_modal_link' => 'Enllaç (opcional)',
'gift_confirm_delete' => 'Vols eliminar aquest regal?',
];
+422 -19
View File
@@ -1,14 +1,215 @@
<?php
// Dictionnaire Français
return [
// ==========================================
// GLOBAL & NAVIGATION
// ==========================================
'menu_home' => 'Accueil',
'menu_budget' => 'Budget',
'menu_calendar' => 'Calendrier',
'menu_gifts' => 'Cadeaux',
'menu_budget' => 'Budget',
'menu_holidays' => 'Voyages',
'outbound_trip' => 'Aller',
'menu_gifts' => 'Cadeaux',
'nav_home' => 'Accueil',
'nav_calendar' => 'Calendrier',
'nav_budget' => 'Budget',
'nav_holidays' => 'Voyages',
'nav_gifts' => 'Cadeaux',
'aria_menu' => 'Menu mobile',
// Boutons et actions globales
'actions' => 'Actions',
'edit' => 'Modifier',
'delete' => 'Supprimer',
'save' => 'Enregistrer',
'cancel' => 'Annuler',
'btn_save' => 'Enregistrer',
'btn_cancel' => 'Annuler',
'btn_delete' => 'Supprimer',
'btn_close' => 'Fermer',
'btn_back' => '◀ Retour',
'btn_edit_bases' => '⚙️ Modifier les bases',
// Mots génériques & Erreurs
'error' => 'Erreur.',
'confirm_delete' => 'Es-tu sûr de vouloir supprimer cet élément ?',
// Mois & Jours
'month_01' => 'Janvier', 'month_02' => 'Février', 'month_03' => 'Mars',
'month_04' => 'Avril', 'month_05' => 'Mai', 'month_06' => 'Juin',
'month_07' => 'Juillet', 'month_08' => 'Août', 'month_09' => 'Septembre',
'month_10' => 'Octobre', 'month_11' => 'Novembre','month_12' => 'Décembre',
'day_mon' => 'Lun', 'day_tue' => 'Mar', 'day_wed' => 'Mer',
'day_thu' => 'Jeu', 'day_fri' => 'Ven', 'day_sat' => 'Sam', 'day_sun' => 'Dim',
// ==========================================
// AUTHENTIFICATION / LOGIN
// ==========================================
'login_title' => 'Connexion - PachaFamily',
'login_header' => 'Connexion',
'label_username' => 'Identifiant',
'label_password' => 'Mot de passe',
'placeholder_username' => 'ex: pacha',
'btn_login' => 'Connexion',
'btn_login_submit' => 'Se connecter',
'logout' => 'Déconnexion',
'btn_logout' => 'Déconnexion',
'error_missing_fields' => 'Champs obligatoires.',
'error_invalid_credentials' => 'Identifiants incorrects.',
// ==========================================
// ACCUEIL / INDEX
// ==========================================
'home_title' => 'PachaFamily - Accueil',
'home_welcome' => 'Bienvenue sur PachaFamily',
'home_subtitle' => "Centre de contrôle de l'organisation familiale.",
'home_logged_as' => 'Connecté en tant que',
'home_modules_title' => 'Modules',
'mod_calendar_name' => 'Calendrier Familial',
'mod_calendar_desc' => "Gérez les congés, les modes de garde et l'organisation hebdomadaire.",
'mod_holidays_name' => 'Vacances',
'mod_holidays_desc' => 'Planifiez vos prochaines escapades, destinations et idées de voyage.',
'mod_gifts_name' => 'Cadeaux',
'mod_gifts_desc' => 'Planifiez les cadeaux pour chaque enfant, pour Noël et les anniversaires.',
'mod_budget_name' => 'Budget',
'mod_budget_desc' => 'Suivi des dépenses fixes, revenus et équilibre du compte familial.',
'cta_open' => 'Ouvrir',
'cta_explore' => 'Explorer',
'cta_view_lists' => 'Voir les listes',
'cta_manage' => 'Gérer',
// ==========================================
// FAMILY CALENDAR
// ==========================================
'fc_page_title' => 'PachaFamily - Calendrier',
'fc_main_header' => 'Calendrier Familial',
'fc_btn_correct_balances' => 'Corriger les soldes',
'fc_btn_school_holidays' => 'Vacances scolaires',
'fc_modal_holidays_title' => 'Vacances Scolaires (Zone C)',
'fc_modal_snap_title' => 'Ajuster un solde',
'fc_col_period' => 'Période',
'fc_col_from' => 'Du',
'fc_col_to' => 'Au',
'fc_label_person' => 'Personne',
'fc_label_leave_type' => 'Type de congé',
'fc_label_apply_date' => "Date d'application",
'fc_snap_date_hint' => "Choisissez de préférence le 1er jour d'un mois",
'fc_label_remaining_balance'=> 'Solde restant à cette date',
'fc_placeholder_balance' => 'ex: 12.5',
'fc_btn_save_snap' => 'Enregistrer le correctif',
'fc_view_1m' => '1 mois',
'fc_view_2m' => '2 mois',
'fc_view_year' => 'Année',
'fc_weekly_planning' => 'Planning hebdomadaire',
'fc_col_month' => 'Mois',
'fc_col_week_short' => 'Sem.',
'fc_col_av' => 'Av.',
'fc_col_use' => 'Use',
'fc_legend_title' => 'Légende',
'fc_summary_title' => 'Récapitulatif',
'fc_summ_year' => 'Année',
'fc_summ_month' => 'Mois',
'fc_school_year' => 'Année scolaire',
'fc_unit_days' => 'jours',
'fc_menu_carole' => 'Congés Carole',
'fc_clear' => 'Effacer',
'fc_menu_kids_leaves' => 'Congés Enfants',
'leg_school_holidays' => 'Vacances Scolaires',
'leg_public_holiday' => 'Férié',
'leg_off_carole' => 'Off Carole',
'leg_extra_off' => 'Extra Off',
'leg_centre' => 'Centre',
'leg_avis' => 'Avis',
'leg_pep_sick' => 'Pep Malade',
'leg_presence' => 'Présence',
'vac_toussaint' => 'Vacances de la Toussaint',
'vac_noel' => 'Vacances de Noël',
'vac_hiver' => "Vacances d'Hiver",
'vac_printemps' => 'Vacances de Printemps',
'vac_ascension' => 'Pont de lAscension',
'vac_ete' => "Vacances d'Été",
'btn_off' => 'Off',
'btn_extra' => 'Extra',
// ==========================================
// HOLIDAYS (VOYAGES)
// ==========================================
'hdl_title_list' => 'PachaFamily - Mes Vacances',
'hdl_title_detail' => 'PachaFamily - Détail du voyage',
'hdl_main_title' => 'Mes Vacances',
'hdl_filter_all' => 'Tout afficher',
'hdl_filter_year' => 'Année',
'hdl_left_to_pay_label' => 'Reste à payer',
'hdl_btn_create' => 'Créer un voyage',
'hdl_no_active_trips' => 'Aucun voyage en cours pour cette période. Planifions quelque chose !',
'hdl_history_title' => 'Historique',
'hdl_dates_to_define' => 'Dates à définir',
'hdl_total_budget' => 'Budget Total',
'hdl_paid' => 'Payé',
'hdl_saved' => 'Financé',
'hdl_left_to_pay' => 'Reste à payer',
'hdl_status_booked' => 'Confirmé',
'hdl_status_planned' => 'Planifié',
'hdl_status_draft' => 'Brouillon',
'hdl_status_passed' => 'Terminé',
'hdl_status_archived' => 'Archivé',
'hdl_modal_title' => 'Planifier le voyage',
'hdl_label_name' => 'Nom du voyage',
'hdl_ph_name' => 'Ex: Octobre - Portugal',
'hdl_label_status' => 'Statut',
'hdl_label_period' => 'Période (Texte libre)',
'hdl_ph_period' => 'Ex: Octobre 2026',
'hdl_label_from' => 'Du',
'hdl_label_to' => 'Au',
'hdl_cat_transport' => 'Transport',
'hdl_add_transport' => 'Ajouter un transport',
'hdl_cat_accommodation' => 'Hébergement',
'hdl_add_accommodation' => 'Ajouter un hébergement',
'hdl_cat_activity' => 'Activité',
'hdl_add_activity' => 'Ajouter une activité',
'hdl_label_budget_food' => 'Budget Food & Bev (€)',
'hdl_label_budget_extras' => 'Budget Extras (€)',
'hdl_label_notes' => 'Notes',
'hdl_ph_notes' => 'Idées en vrac...',
'hdl_label_budget_food_extras' => 'Budget Food & Extras',
'hdl_map_itinerary' => 'Itinéraire',
'hdl_outbound' => 'Aller',
'hdl_return' => 'Retour',
'hdl_btn_add_step' => 'Ajouter une étape',
'hdl_step_details' => 'Détails du roadtrip',
'hdl_no_steps' => 'Aucune étape définie. Commencez par en ajouter une !',
'hdl_general_costs' => 'Frais généraux & Budgets',
'hdl_food_bev' => 'Food & Bev',
'hdl_extras' => 'Extras',
'hdl_view_planning' => 'Voir le planning',
'hdl_passing_point' => 'Point de passage (sans frais)',
'hdl_search_location' => 'Chercher un lieu',
'hdl_ph_search' => 'Ville, hôtel, adresse...',
'hdl_label_step_name' => "Nom de l'étape",
'hdl_label_arrival' => 'Arrivée',
'hdl_label_departure' => 'Départ',
'hdl_planned_expenses' => 'Dépenses prévues pour cette étape',
'hdl_btn_add_expense' => '+ Ajouter une dépense',
'hdl_save_fav' => 'Enregistrer dans mes favoris',
'hdl_planning_title' => 'Planning de l’étape',
'hdl_to_pay' => 'À payer',
'hdl_js_search_loading' => 'Recherche en cours...',
'hdl_js_no_result' => 'Aucun résultat trouvé.',
'hdl_js_network_error' => 'Erreur de connexion au service de carte.',
'hdl_js_confirm_del_trip' => 'Es-tu sûr de vouloir supprimer ce voyage entier ?',
'hdl_js_confirm_del_step' => 'Supprimer cette étape ?',
'hdl_js_step_label' => 'Étape',
'hdl_js_ph_expense_name' => 'Ex: Hôtel, Péage...',
'hdl_quick_edit_title' => 'Modification rapide',
'hdl_js_edit_step' => 'Modifier l’étape',
'hdl_js_missing_dates_title'=> 'Dates manquantes',
'hdl_js_missing_dates_msg' => 'Veuillez définir une date darrivée et de départ pour organiser le planning.',
'hdl_to_place' => 'À placer',
// Anciennes clés Holidays non préfixées (conservées pour compatibilité si utilisées)
'outbound_trip' => 'Aller',
'return_trip' => 'Retour',
'period' => 'Période',
'budget_food' => 'Budget Food & Extras',
'total_cost' => 'Coût Total Estimé',
@@ -16,16 +217,10 @@ return [
'provisioned' => 'Provisionné',
'left_to_pay' => 'Reste à payer',
'map_itinerary' => '🗺️ Itinéraire',
'outbound_trip' => 'Aller',
'return_trip' => 'Retour',
'place_step' => '📍 Placer une étape',
'step_details' => '📝 Détail des étapes',
'no_steps' => 'Aucune étape planifiée.',
'travel_notes' => '📝 Notes du voyage',
'edit' => 'Modifier',
// --- Mots divers & Infobulles ---
'error' => 'Erreur.',
'to_be_defined' => 'À définir',
'funded' => 'Financé (Provision)',
'view_planning' => 'Voir le planning',
@@ -33,8 +228,6 @@ return [
'from_date' => 'Du',
'to_date' => 'au',
'passing_point' => 'Point de passage (Aucune dépense)',
// --- Modale : Placer / Éditer une étape ---
'search_location' => 'Rechercher un lieu géographique',
'search_placeholder' => 'Ex: Paris, Ibis Barcelone...',
'step_name' => 'Nom de l\'étape',
@@ -43,15 +236,7 @@ return [
'planned_expenses' => 'Dépenses prévues',
'add_expense' => '+ Ajouter une dépense',
'save_favorite' => 'Sauvegarder dans mes favoris',
// --- Boutons génériques ---
'btn_delete' => 'Supprimer',
'btn_cancel' => 'Annuler',
'btn_save' => 'Enregistrer',
'btn_close' => 'Fermer',
'planning_title' => 'Planning',
// --- Javascript (holidays.js) ---
'modal_plan_trip' => 'Planifier le voyage',
'modal_quick_edit' => 'Modification rapide',
'err_modal_missing' => 'Erreur : La modale holidayModal est introuvable.',
@@ -74,4 +259,222 @@ return [
'missing_dates_msg' => 'Veuillez modifier cette étape (✏️) et renseigner sa <strong>Date d\'arrivée et de départ</strong>.',
'to_place' => '📥 À Placer',
// ==========================================
// BUDGET (GLOBAL & CATEGORIES)
// ==========================================
'budget_page_title' => 'PachaFamily - Budget',
'budget_main_header' => 'Gestion du Budget',
'budget_tab_tracking' => 'Suivi Mensuel',
'budget_tab_prev' => 'Budget 2026',
'budget_tab_savings' => 'Épargne',
'budget_tab_recap' => 'Récapitulatif',
'budget_err_file_not_found' => 'Oups ! Fichier introuvable.',
'budget_err_file_detail' => 'Vérifiez que le fichier views/%s.php existe bien.',
'budget_err_not_authorized' => 'Page non autorisée.',
'cat_income' => 'Revenus',
'cat_fmcg' => 'Courses (FMCG)',
'cat_fuel' => 'Essence',
'cat_school' => 'École / Garde',
'cat_fixed' => 'Charges Fixes',
'cat_others' => 'Autres / Imprévus',
'cat_savings' => 'Épargne',
// --- BUDGET : SUIVI ---
'bud_rem_school' => 'École (Reste estimé)',
'bud_rem_fmcg' => 'Courses (Reste estimé)',
'bud_rem_fuel' => 'Essence (Reste estimé)',
'bud_closed_archived' => 'Ce mois est clôturé et archivé.',
'bud_budget_of' => 'Budget :',
'bud_confirm_close' => 'Clôturer ce mois ? Cela figera les soldes actuels et basculera le suivi par défaut sur le mois prochain.',
'bud_close_btn' => 'Clôturer',
'bud_confirm_reopen' => 'Rouvrir ce mois ? Ses soldes seront recalculés en direct.',
'bud_reopen_btn' => 'Rouvrir',
'bud_active_month_btn' => 'Mois Actif',
'bud_bank_balance' => 'Solde bancaire',
'bud_frozen' => 'Figé',
'bud_current' => 'Actuel',
'bud_update_btn' => 'MàJ',
'bud_theoretical_balance' => 'Solde théorique fin de mois',
'bud_upcoming_charges' => 'Charges à venir',
'bud_all_paid' => 'Tout est payé !',
'bud_capacity_tooltip' => 'Calcul : Report Mois Précédent (%s€) + Salaires (%s€) + Autres rentrées (%s€)',
'bud_max_capacity' => 'Capacité Max',
'bud_actual' => 'Actuel',
'bud_theoretical' => 'Théorique',
'bud_import_csv' => 'Importer CSV',
'bud_csv_file' => 'Fichier CSV (Point-virgule)',
'bud_preview' => 'Prévisualiser',
'bud_validate_import' => 'Valider l\'importation',
'bud_assign_month' => 'Imputer au mois',
'bud_op_date' => 'Date Opération',
'bud_label' => 'Libellé',
'bud_amount' => 'Montant',
'bud_category' => 'Catégorie',
'bud_ignore' => 'Ignorer',
'bud_to_define' => 'À définir',
'bud_is_charge' => 'Charge ?',
'bud_is_income' => 'Revenu ?',
'btn_import' => 'Importer',
'bud_no_lines' => 'Aucune ligne.',
'bud_confirm_delete' => 'Supprimer cette ligne ?',
'bud_new_transaction' => 'Nouvelle transaction',
'bud_assign_to_month' => 'Imputer au mois de gestion',
'bud_real_date' => 'Date réelle de l\'opération',
'bud_type' => 'Type',
'bud_type_expense' => 'Dépense',
'bud_type_income' => 'Revenu / Remboursement',
'bud_beneficiary' => 'Bénéficiaire',
'bud_fixed_charge' => 'Charge Fixe',
'bud_expected_income' => 'Revenu Attendu',
'bud_amount_eur' => 'Montant (€)',
'bud_update_balance' => 'MàJ Solde Bancaire',
'bud_balance_eur' => 'Solde (€)',
'bud_add_title' => 'Ajouter',
'bud_edit_title' => 'Modifier la transaction',
'bud_to_define_js' => 'à définir',
// --- BUDGET : RECAP ---
'bud_recap_monthly_title' => 'Récapitulatif Mensuel',
'bud_recap_btn_add' => 'Ajouter un frais / revenu',
'bud_planned_amount' => 'Montant Prévu',
'bud_day' => 'Jour',
'bud_month_state' => 'État du mois',
'bud_est_short' => 'Est.',
'bud_reg_planned_in' => 'Régul. prévue en %s',
'bud_bonus' => 'Bonus',
'bud_overrun' => 'Dépassement',
'bud_remaining' => 'Reste',
'bud_exact_amount' => 'Montant exact',
'bud_per_month_short' => 'Soit',
'bud_month_short' => 'mois',
'bud_freq_mensuel' => 'Mensuel',
'bud_freq_annuel' => 'Annuel',
'bud_state_validated' => 'Validé',
'bud_state_partial' => 'Partiel',
'bud_state_waiting' => 'En attente',
'bud_total_income_smoothed' => 'Total Revenus (Lissés)',
'bud_total_expenses_smoothed'=> 'Total Dépenses (Lissées)',
'bud_theoretical_balance_recap'=> 'Équilibre théorique',
'bud_recap_footer_note' => 'L\'état de paiement se met à jour automatiquement en fonction des opérations importées dans l\'onglet "Suivi Mensuel".',
'bud_recap_modal_add' => 'Ajouter un élément',
'bud_recap_modal_edit' => 'Modifier',
'bud_label_name' => 'Nom',
'bud_ph_item_name' => 'Ex: Loyer, Salaire...',
'bud_label_keywords' => 'Mots-clés (Détection Auto)',
'bud_ph_keywords' => 'Ex: NETFLIX, PRIME VIDEO (séparés par virgule)',
'bud_help_keywords' => 'Si une dépense du mois contient ce mot, la ligne passera en "Validé".',
'bud_label_day' => 'Jour (1-31)',
'bud_cat_expense' => 'Dépense (Frais)',
'bud_label_frequency' => 'Fréquence',
'bud_label_amount_type' => 'Type de montant',
'bud_type_fixed' => 'Fixe (Facture)',
'bud_type_variable' => 'Variable (Estimation)',
'bud_label_regularization' => 'Régularisation',
'bud_reg_none' => 'Aucune',
'bud_recap_confirm_delete' => 'Voulez-vous vraiment supprimer cet élément ?',
'bud_err_delete' => 'Erreur lors de la suppression.',
// --- BUDGET : EPARGNE ---
'bud_sav_add_month' => 'Saisir un mois',
'bud_sav_add_one_month' => '+1 Mois',
'bud_sav_no_data' => 'Aucune donnée pour %s.',
'bud_sav_post_month' => 'Poste / Mois',
'bud_sav_from_date' => 'Dès le %s',
'bud_sav_edit_modal' => 'Modifier avec Modale',
'bud_sav_delete_month' => 'Supprimer tout le mois',
'bud_sav_total_bank' => 'Total Banque',
'bud_sav_extra' => 'Extra',
'bud_sav_modal_title_add' => 'Saisir un mois',
'bud_sav_modal_title_edit' => 'Modifier :',
'bud_sav_month_concerned' => 'Mois concerné',
'bud_sav_total_bank_eur' => 'Total en Banque (€)',
'bud_sav_ventilation' => 'Ventilation',
'bud_sav_adj_help' => 'Utilisez l\'ajustement (+/-) pour recalculer automatiquement.',
'bud_sav_add_line' => 'Ligne',
'bud_sav_current' => 'Actuel',
'bud_sav_adjust' => 'Ajust (+/-)',
'bud_sav_new' => 'Nouveau',
'bud_sav_sum_mode_title' => 'Activer l\'addition rapide',
'bud_sav_selection' => 'Sélection :',
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
'bud_sav_saving' => 'Enregistrement...',
'bud_err_tech' => 'Une erreur technique est survenue.',
'bud_sav_confirm_delete_month' => 'Supprimer TOUT le mois de %m pour %o ?',
'bud_sav_prompt_duplicate' => "Voulez-vous copier les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
'bud_err_server' => 'Erreur serveur : ',
'bud_err_network_dup' => 'Erreur réseau lors de la duplication.',
// --- BUDGET : PREVISIONNEL ---
'bud_prev_incomes' => 'Revenus',
'bud_prev_person' => 'Personne',
'bud_prev_salary' => 'Salaire',
'bud_prev_monthly_payment' => 'Mensualité',
'bud_prev_joint_account' => 'Cpt Commun',
'bud_prev_func_expenses' => 'Frais Func.',
'bud_prev_perso_savings' => 'Eco Perso',
'bud_prev_family_savings' => 'Eco Family',
'bud_prev_available' => 'Dispo',
'bud_prev_perso_remaining' => 'Restant Perso',
'bud_prev_budget_alloc' => 'Répartition Budgétaire',
'bud_prev_today' => 'Auj.',
'bud_prev_new_line' => 'Nouvelle Ligne',
'bud_prev_remaining' => 'Restant',
'bud_prev_confirm_del_line' => 'Supprimer cette ligne et tout son historique ?',
'bud_prev_notes_for' => 'Notes pour',
'bud_prev_saved' => 'Enregistré',
'bud_prev_notes_ph' => 'Écrivez vos remarques, rappels ou commentaires pour ce mois ici...',
'bud_prev_save_note' => 'Enregistrer la note',
'bud_prev_transfers_to_make' => 'Virements à effectuer',
'bud_prev_destination' => 'Destination',
'bud_prev_done' => 'FAIT',
'bud_prev_validate' => 'Valider',
'bud_prev_grand_total' => 'GRAND TOTAL',
'bud_prev_new_line_title' => 'Nouvelle Ligne de Budget',
'bud_prev_edit_line_title' => 'Modifier la ligne',
'bud_prev_label_target' => 'Cible (Destination)',
'bud_prev_choose' => 'Choisir',
'bud_prev_link_holiday' => 'Lier à un voyage (Optionnel)',
'bud_prev_no_link' => 'Ne pas associer',
'bud_prev_err_no_history' => 'Impossible : pas d\'historique disponible pour copier.',
'bud_prev_confirm_copy' => "Voulez-vous copier les données de %s vers %t ?\n\n⚠️ Cela écrasera toutes les valeurs déjà présentes pour %t.",
'bud_prev_confirm_transfers' => "Confirmer que %p a bien effectué tous ses virements pour %m ?\n\nCela mettra à jour l'Épargne automatiquement.",
// ==========================================
// CADEAUX (GIFTS)
// ==========================================
'gift_page_title' => 'PachaFamily - Liste de cadeaux',
'gift_occ_tio' => 'Tió',
'gift_occ_noel' => 'Noël',
'gift_occ_rois' => 'Rois',
'gift_occ_anniv' => 'Anniversaire',
'gift_occ_sant' => 'Saint',
'gift_main_title' => 'Liste de cadeaux %s',
'gift_aria_change_view' => 'Changer la vue',
'gift_view_nadal' => 'Noël',
'gift_view_anniv' => 'Anniversaires',
'gift_view_by_party' => 'Vue par fête',
'gift_no_gifts' => 'Aucun cadeau enregistré pour %s dans cette vue.',
'gift_add_gift' => 'Ajouter un cadeau',
'gift_paid_by' => 'payé par %s',
'gift_summary_title' => 'Résumé du budget',
'gift_col_adult' => 'Adulte',
'gift_col_child' => 'Enfant',
'gift_col_party' => 'Fête',
'gift_debtor' => 'Débiteur',
'gift_creditor' => 'Créancier',
'gift_liquidations' => 'Liquidations',
'gift_no_debt' => 'Aucune dette en cours.',
'gift_owes' => 'doit',
'gift_to' => 'à',
'gift_detailed_list' => 'Liste détaillée des cadeaux',
'gift_col_gift' => 'Cadeau',
'gift_col_link' => 'Lien',
'gift_modal_title_add' => 'Ajouter un cadeau pour %s',
'gift_modal_title_edit' => 'Modifier le cadeau',
'gift_modal_payer' => 'Payé par',
'gift_modal_gift_name' => 'Nom du cadeau',
'gift_modal_ph_name' => 'ex: Lego Star Wars',
'gift_modal_price' => 'Prix (€)',
'gift_modal_link' => 'Lien (optionnel)',
'gift_confirm_delete' => 'Voulez-vous vraiment supprimer ce cadeau ?',
];
+20 -17
View File
@@ -3,8 +3,11 @@
require __DIR__ . '/includes/auth.php';
require_login('/login.php');
// Inclusion i18n (déjà fait dans header, mais par sécurité si besoin de tr() avant)
require_once __DIR__ . '/includes/i18n.php';
// Configuration de la page
$pageTitle = "PachaFamily - Accueil";
$pageTitle = tr('home_title');
$activePage = "home";
$bodyClass = "pf-home"; // Important pour l'image de fond définie dans home.css
$pageCss = "/modules/home/home.css";
@@ -15,56 +18,56 @@ require __DIR__ . '/header.php';
<div class="pf-container">
<div class="pf-hero">
<h1>Bienvenue sur PachaFamily</h1>
<p>Centre de contrôle de l'organisation familiale.</p>
<h1><?= tr('home_welcome') ?></h1>
<p><?= tr('home_subtitle') ?></p>
<?php if (isset($_SESSION['user'])): ?>
<div class="pf-user-info" style="margin-top: 12px; font-weight: 500;">
Connecté en tant que
<?= tr('home_logged_as') ?>
<strong><?= htmlspecialchars($_SESSION['user']['display_name'] ?? $_SESSION['user']['username']) ?></strong>
</div>
<?php endif; ?>
</div>
<section class="pf-section">
<h2 style="color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.6);">Modules</h2>
<h2 style="color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.6);"><?= tr('home_modules_title') ?></h2>
<div class="pf-modules-grid">
<a href="/family-calendar.php" class="pf-module-card">
<div class="pf-card-icon">📅</div>
<h3 class="pf-card-title">Family calendar</h3>
<h3 class="pf-card-title"><?= tr('mod_calendar_name') ?></h3>
<div class="pf-card-desc">
Gérez les congés, les modes de garde et l'organisation hebdomadaire.
<?= tr('mod_calendar_desc') ?>
</div>
<span class="pf-card-cta">Ouvrir</span>
<span class="pf-card-cta"><?= tr('cta_open') ?></span>
</a>
<a href="/holidays.php" class="pf-module-card">
<div class="pf-card-icon">🏖️</div>
<h3 class="pf-card-title">Vacances</h3>
<h3 class="pf-card-title"><?= tr('mod_holidays_name') ?></h3>
<div class="pf-card-desc">
Planifiez vos prochaines escapades, destinations et idées de voyage.
<?= tr('mod_holidays_desc') ?>
</div>
<span class="pf-card-cta">Explorer</span>
<span class="pf-card-cta"><?= tr('cta_explore') ?></span>
</a>
<a href="/gift-list.php" class="pf-module-card">
<div class="pf-card-icon">🎁</div>
<h3 class="pf-card-title">Cadeaux</h3>
<h3 class="pf-card-title"><?= tr('mod_gifts_name') ?></h3>
<div class="pf-card-desc">
Planifiez les cadeaux pour chaque enfant, pour Noël, les anniversaires et autres.
<?= tr('mod_gifts_desc') ?>
</div>
<span class="pf-card-cta">Voir les listes</span>
<span class="pf-card-cta"><?= tr('cta_view_lists') ?></span>
</a>
<a href="/budget.php" class="pf-module-card">
<div class="pf-card-icon">💰</div>
<h3 class="pf-card-title">Budget</h3>
<h3 class="pf-card-title"><?= tr('mod_budget_name') ?></h3>
<div class="pf-card-desc">
Suivi des dépenses fixes, revenus et équilibre du compte familial.
<?= tr('mod_budget_desc') ?>
</div>
<span class="pf-card-cta">Gérer</span>
<span class="pf-card-cta"><?= tr('cta_manage') ?></span>
</a>
</div>
+9 -8
View File
@@ -1,8 +1,9 @@
<?php
session_start();
require __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/i18n.php'; // Toujours s'assurer que tr() est dispo
$pageTitle = "Connexion";
$pageTitle = tr('login_title');
$activePage = "login";
if (isset($_SESSION['user'])) {
@@ -17,7 +18,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$password = $_POST['password'] ?? '';
if (!$username || !$password) {
$error = "Champs obligatoires.";
$error = tr('error_missing_fields');
} else {
$stmt = $pdo->prepare("SELECT id, username, password_hash, display_name FROM pf_users WHERE username = ?");
$stmt->execute([$username]);
@@ -33,7 +34,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Location: ' . $redirectTo);
exit;
} else {
$error = "Identifiants incorrects.";
$error = tr('error_invalid_credentials');
}
}
}
@@ -42,7 +43,7 @@ require __DIR__ . '/header.php';
?>
<div class="pf-login-container">
<h1 style="text-align:center; font-size:1.8rem;">Connexion</h1>
<h1 style="text-align:center; font-size:1.8rem;"><?= tr('login_header') ?></h1>
<?php if ($error): ?>
<div class="pf-error"><?= htmlspecialchars($error) ?></div>
@@ -50,16 +51,16 @@ require __DIR__ . '/header.php';
<form method="post" action="/login.php<?= isset($_GET['redirect']) ? '?redirect=' . urlencode($_GET['redirect']) : '' ?>">
<div class="pf-form-group">
<label for="username">Identifiant</label>
<input type="text" id="username" name="username" required autofocus placeholder="ex: pacha">
<label for="username"><?= tr('label_username') ?></label>
<input type="text" id="username" name="username" required autofocus placeholder="<?= tr('placeholder_username') ?>">
</div>
<div class="pf-form-group">
<label for="password">Mot de passe</label>
<label for="password"><?= tr('label_password') ?></label>
<input type="password" id="password" name="password" required placeholder="••••••">
</div>
<button type="submit" class="pf-btn">Se connecter</button>
<button type="submit" class="pf-btn"><?= tr('btn_login_submit') ?></button>
</form>
</div>
+89 -84
View File
@@ -36,7 +36,6 @@ $nextMonthLink = date('Y-m-01', strtotime("+1 month", $focusTs));
$cycleConfigs = [];
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
// Convertit "03-2026" en "2026-03-01" (Notre Tiroir)
$parts = explode('-', $row['reference_id']);
if (count($parts) == 2) {
$mKey = $parts[1] . '-' . $parts[0] . '-01';
@@ -81,25 +80,32 @@ $currentNote = $stmtNote->fetchColumn();
// 7. Récupération des vacances actives
$activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN ('draft', 'planned', 'booked') ORDER BY start_date ASC")->fetchAll(PDO::FETCH_ASSOC);
// Helper function pour les mois
function getTranslatedMonthName($dateString) {
$m = date('m', strtotime($dateString));
$y = date('Y', strtotime($dateString));
return tr('month_' . $m) . ' ' . $y;
}
?>
<div class="prev-container">
<div>
<div class="prev-section-header">
<h2>Revenus <?= $currentYear ?></h2>
<h2><?= tr('bud_prev_incomes') ?> <?= $currentYear ?></h2>
</div>
<table class="prev-salary-table">
<thead>
<tr>
<th>Personne</th>
<th>Salaire</th>
<th>Mensualité<br><small style="font-weight:normal; text-transform:none;">(Cpt Commun)</small></th>
<th>Frais Func.</th>
<th>Eco Perso</th>
<th style="background:#f0f9ff; color:#0284c7;">Eco Family<br><small style="font-weight:normal; text-transform:none;">(Dispo)</small></th>
<th>Restant Perso</th>
<th><?= tr('bud_prev_person') ?></th>
<th><?= tr('bud_prev_salary') ?></th>
<th><?= tr('bud_prev_monthly_payment') ?><br><small style="font-weight:normal; text-transform:none;">(<?= tr('bud_prev_joint_account') ?>)</small></th>
<th><?= tr('bud_prev_func_expenses') ?></th>
<th><?= tr('bud_prev_perso_savings') ?></th>
<th style="background:#f0f9ff; color:#0284c7;"><?= tr('bud_prev_family_savings') ?><br><small style="font-weight:normal; text-transform:none;">(<?= tr('bud_prev_available') ?>)</small></th>
<th><?= tr('bud_prev_perso_remaining') ?></th>
</tr>
</thead>
<tbody>
@@ -124,8 +130,6 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
@@ -133,20 +137,19 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<div>
<div class="prev-section-header">
<div style="display:flex; gap:10px; align-items:center;">
<h2>Répartition Budgétaire</h2>
<h2><?= tr('bud_prev_budget_alloc') ?></h2>
<div class="nav-group">
<a href="?tab=budget_prev&focus_date=<?= $prevMonthLink ?>" class="btn-nav">◀</a>
<a href="?tab=budget_prev&focus_date=<?= date('Y-m-01') ?>" class="btn-nav">Auj.</a>
<a href="?tab=budget_prev&focus_date=<?= date('Y-m-01') ?>" class="btn-nav"><?= tr('bud_prev_today') ?></a>
<a href="?tab=budget_prev&focus_date=<?= $nextMonthLink ?>" class="btn-nav">▶</a>
</div>
</div>
<div style="display:flex; gap:10px;">
<button class="pf-btn btn-secondary" onclick="duplicateMonth()">
🔁 +1 Mois
🔁 <?= tr('bud_sav_add_one_month') ?>
</button>
<button class="pf-btn" onclick="document.getElementById('addCatModal').style.display='flex'">
Nouvelle Ligne
<button class="pf-btn" onclick="document.getElementById('addCatModal').style.display='flex'; document.body.classList.add('no-scroll');">
<?= tr('bud_prev_new_line') ?>
</button>
</div>
</div>
@@ -161,21 +164,18 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
$cls = $isCurrent ? 'current' : '';
?>
<th colspan="3" class="th-month <?= $cls ?>">
<?= date('F Y', strtotime($month)) ?>
<span style="text-transform:capitalize;"><?= getTranslatedMonthName($month) ?></span>
<?php
// AFFICHAGE DU CYCLE CONFIGURÉ
if (isset($cycleConfigs[$month]) && !empty($cycleConfigs[$month]['start_date'])) {
$cStart = date('d/m', strtotime($cycleConfigs[$month]['start_date']));
echo "<div style='font-size:0.75rem; font-weight:normal; color:#64748b; margin-top:2px;'>Dès le $cStart</div>";
echo "<div style='font-size:0.75rem; font-weight:normal; color:#64748b; margin-top:2px;'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</div>";
}
?>
</th>
<?php endforeach; ?>
</tr>
<tr>
<th class="col-sticky header-cell">
</th>
<th class="col-sticky header-cell"></th>
<?php foreach ($months as $month): ?>
<th class="th-sub">Global</th>
<th class="th-sub txt-alex">Alex</th>
@@ -194,7 +194,7 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</tr>
<tr class="row-restant">
<td class="col-sticky" style="text-align:right !important;">Restant (Eco Family)</td>
<td class="col-sticky" style="text-align:right !important;"><?= tr('bud_prev_remaining') ?> (Eco Family)</td>
<?php foreach ($months as $m): ?>
<td style="border-left:2px solid #cbd5e1; background:#e2e8f0;">-</td>
<td class="val-ok sum-target" id="restant_alloc_alex_<?= $m ?>">0</td>
@@ -222,7 +222,7 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<div class="row-actions">
<button type="button"
class="btn-icon-action edit"
title="Modifier"
title="<?= tr('edit') ?>"
data-id="<?= $cat['id'] ?>"
data-name="<?= htmlspecialchars($cat['name']) ?>"
data-target="<?= htmlspecialchars($cat['target']) ?>"
@@ -232,8 +232,8 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</button>
<a href="?tab=budget_prev&id=<?= $cat['id'] ?>&action=delete_category"
onclick="return confirm('Supprimer cette ligne et tout son historique ?')"
class="btn-icon-action delete" title="Supprimer">
onclick="return confirm('<?= tr('bud_prev_confirm_del_line') ?>')"
class="btn-icon-action delete" title="<?= tr('delete') ?>">
&times;
</a>
</div>
@@ -271,10 +271,10 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<div style="margin: 20px 0; background: white; padding: 20px; border-radius: 12px; box-shadow: var(--shadow-sm); border: 1px solid #e2e8f0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h3 style="margin: 0; font-size: 1.1rem; color: #1e293b;">
📝 Notes pour <?= date('F Y', strtotime($focusDate)) ?>
📝 <?= tr('bud_prev_notes_for') ?> <span style="text-transform:capitalize;"><?= getTranslatedMonthName($focusDate) ?></span>
</h3>
<span id="note-save-indicator" style="font-size:0.85rem; color:#10b981; font-weight:bold; opacity:0; transition:opacity 0.3s;">
✓ Enregistré
✓ <?= tr('bud_prev_saved') ?>
</span>
</div>
@@ -282,20 +282,18 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
id="monthNoteArea"
class="pf-input"
rows="3"
placeholder="Écrivez vos remarques, rappels ou commentaires pour ce mois ici..."
placeholder="<?= tr('bud_prev_notes_ph') ?>"
style="width: 100%; resize: vertical; border-color: #cbd5e1; background: #f8fafc; margin-bottom: 10px;"
><?= htmlspecialchars((string)$currentNote) ?></textarea>
<div style="text-align: right;">
<button type="button" class="pf-btn" style="width: auto; display: inline-flex;"
onclick="saveGenericNote('budget_prev', '<?= $focusDate ?>', document.getElementById('monthNoteArea').value)">
Enregistrer la note
<?= tr('bud_prev_save_note') ?>
</button>
</div>
</div>
<?php
$focusMonth = $months[0];
@@ -335,24 +333,24 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<div class="recap-wrapper">
<div class="recap-card">
<div class="recap-header">
Virements à effectuer - <?= date('F Y', strtotime($focusMonth)) ?>
<?= tr('bud_prev_transfers_to_make') ?> - <span style="text-transform:capitalize;"><?= getTranslatedMonthName($focusMonth) ?></span>
</div>
<table class="recap-table">
<thead>
<tr>
<th style="text-align:left;">Destination</th>
<th style="text-align:left;"><?= tr('bud_prev_destination') ?></th>
<th class="col-alex">
<div style="display:flex; flex-direction:column; align-items:center; gap:5px;">
<span>ALEX</span>
<?php if($isValidatedAlex): ?>
<div style="background:#10b981; color:white; padding:4px 8px; border-radius:4px; font-size:0.7rem; display:flex; align-items:center; gap:4px;">
FAIT
<?= tr('bud_prev_done') ?>
</div>
<?php else: ?>
<button onclick="validateTransfers('Alex', '<?= $focusMonth ?>')" class="pf-btn btn-small" style="background:white; color:#0891b2; border:1px solid #0891b2; font-size:0.7rem; padding:2px 8px; height:auto;">
Valider
<?= tr('bud_prev_validate') ?>
</button>
<?php endif; ?>
</div>
@@ -363,11 +361,11 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<span>LAIA</span>
<?php if($isValidatedLaia): ?>
<div style="background:#10b981; color:white; padding:4px 8px; border-radius:4px; font-size:0.7rem; display:flex; align-items:center; gap:4px;">
FAIT
<?= tr('bud_prev_done') ?>
</div>
<?php else: ?>
<button onclick="validateTransfers('Laia', '<?= $focusMonth ?>')" class="pf-btn btn-small" style="background:white; color:#d97706; border:1px solid #d97706; font-size:0.7rem; padding:2px 8px; height:auto;">
Valider
<?= tr('bud_prev_validate') ?>
</button>
<?php endif; ?>
</div>
@@ -390,7 +388,7 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</tbody>
<tfoot>
<tr class="row-grand-total">
<td>GRAND TOTAL</td>
<td><?= tr('bud_prev_grand_total') ?></td>
<td class="col-alex sum-target" id="grand_total_alex">0 €</td>
<td class="col-laia sum-target" id="grand_total_laia">0 €</td>
<td class="col-global sum-target" id="grand_total_global">0 €</td>
@@ -399,25 +397,24 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</table>
</div>
</div>
</div>
<div id="addCatModal" class="pf-modal" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(15, 23, 42, 0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center;">
<div class="pf-modal-content" style="background:white; width:95%; max-width:400px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;">Nouvelle Ligne de Budget</h3>
<button onclick="document.getElementById('addCatModal').style.display='none'" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">&times;</button>
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;"><?= tr('bud_prev_new_line_title') ?></h3>
<button onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">&times;</button>
</div>
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
<input type="hidden" name="action" value="add_category">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Nom (ex: Vacances)</label>
<label class="pf-label"><?= tr('bud_prev_label_name') ?></label>
<input type="text" name="name" class="pf-input" required>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Cible (Destination)</label>
<label class="pf-label"><?= tr('bud_prev_label_target') ?></label>
<select name="target" class="pf-input" required>
<option value="" disabled selected>-- Choisir --</option>
<option value="" disabled selected>-- <?= tr('bud_prev_choose') ?> --</option>
<option value="vers L.Pol">vers L.Pol</option>
<option value="vers L.Pep">vers L.Pep</option>
<option value="vers L.Perso">vers L.Perso</option>
@@ -425,17 +422,17 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</select>
</div>
<div class="form-group" style="margin-bottom:20px;">
<label class="pf-label" style="color:#8b5cf6;">🌴 Lier à un voyage (Optionnel)</label>
<label class="pf-label" style="color:#8b5cf6;">🌴 <?= tr('bud_prev_link_holiday') ?></label>
<select name="holiday_id" class="pf-input" style="border-color:#8b5cf6; background:#f5f3ff;">
<option value="">-- Ne pas associer --</option>
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
<?php foreach ($activeHolidays as $hol): ?>
<option value="<?= $hol['id'] ?>"><?= htmlspecialchars($hol['title']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="modal-footer">
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'" class="pf-btn btn-secondary">Annuler</button>
<button type="submit" class="pf-btn">Ajouter</button>
<button type="button" onclick="document.getElementById('addCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('bud_add_title') ?></button>
</div>
</form>
</div>
@@ -444,19 +441,19 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
<div id="editCatModal" class="pf-modal">
<div class="pf-modal-content" style="max-width:400px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;">Modifier la ligne</h3>
<button onclick="document.getElementById('editCatModal').style.display='none'" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">&times;</button>
<h3 class="pf-modal-title" style="margin:0; border:none; font-size:1.2rem;"><?= tr('bud_prev_edit_line_title') ?></h3>
<button onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">&times;</button>
</div>
<form action="/modules/budget/includes/api/save-budget.php" method="POST">
<input type="hidden" name="action" value="update_category">
<input type="hidden" name="cat_id" id="edit_cat_id">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Nom</label>
<label class="pf-label"><?= tr('bud_label_name') ?></label>
<input type="text" name="name" id="edit_cat_name" class="pf-input" required>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Cible (Destination)</label>
<label class="pf-label"><?= tr('bud_prev_label_target') ?></label>
<select name="target" id="edit_cat_target" class="pf-input" required>
<option value="vers L.Pol">vers L.Pol</option>
<option value="vers L.Pep">vers L.Pep</option>
@@ -465,17 +462,17 @@ $activeHolidays = $pdo->query("SELECT id, title FROM pf_holidays WHERE status IN
</select>
</div>
<div class="form-group" style="margin-bottom:20px;">
<label class="pf-label" style="color:#8b5cf6;">🌴 Lier à un voyage (Optionnel)</label>
<label class="pf-label" style="color:#8b5cf6;">🌴 <?= tr('bud_prev_link_holiday') ?></label>
<select name="holiday_id" id="edit_cat_holiday" class="pf-input" style="border-color:#8b5cf6; background:#f5f3ff;">
<option value="">-- Ne pas associer --</option>
<option value="">-- <?= tr('bud_prev_no_link') ?> --</option>
<?php foreach ($activeHolidays as $hol): ?>
<option value="<?= $hol['id'] ?>"><?= htmlspecialchars($hol['title']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="modal-footer">
<button type="button" onclick="document.getElementById('editCatModal').style.display='none'" class="pf-btn btn-secondary">Annuler</button>
<button type="submit" class="pf-btn">Enregistrer</button>
<button type="button" onclick="document.getElementById('editCatModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
@@ -564,15 +561,28 @@ body.sum-mode-active .sum-target {
}
</style>
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()" title="Activer l'addition rapide">
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()" title="<?= tr('bud_sav_sum_mode_title') ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="8" y1="12" x2="16" y2="12"></line><line x1="12" y1="8" x2="12" y2="16"></line></svg>
</button>
<div id="sumResultBar" class="pf-sum-bar">
<span class="pf-sum-label">lection :</span>
<span class="pf-sum-label"><?= tr('bud_sav_selection') ?></span>
<span id="sumResultValue" class="pf-sum-value">0 €</span>
<button onclick="toggleSumMode()" class="pf-sum-close" title="Fermer">&times;</button>
<button onclick="toggleSumMode()" class="pf-sum-close" title="<?= tr('btn_close') ?>">&times;</button>
</div>
<script>
window.I18N = {
...window.I18N,
'bud_prev_err_no_history': "<?= tr('bud_prev_err_no_history') ?>",
'bud_prev_confirm_copy': "<?= tr('bud_prev_confirm_copy') ?>",
'bud_prev_confirm_transfers': "<?= tr('bud_prev_confirm_transfers') ?>",
'bud_err_tech': "<?= tr('bud_err_tech') ?>"
};
const currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
</script>
<script>
function openEditModal(btn) {
const id = btn.getAttribute('data-id');
@@ -586,6 +596,7 @@ function openEditModal(btn) {
document.getElementById('edit_cat_holiday').value = holiday;
document.getElementById('editCatModal').style.display = 'flex';
document.body.classList.add('no-scroll');
}
const currentYear = <?= $currentYear ?>;
@@ -600,7 +611,7 @@ function updateSalary(person, input) {
const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0;
const restant = salary - (mens + frais + ecoP + ecoF);
document.getElementById('restant_' + person).innerText = Math.round(restant).toLocaleString('fr-FR') + ' €';
document.getElementById('restant_' + person).innerText = Math.round(restant).toLocaleString(currentLang) + ' €';
saveData('update_salary_config', { year: currentYear, person: person, field: input.dataset.field, value: input.value });
recalcAllAllocations();
@@ -611,29 +622,24 @@ function updateAlloc(month, catId, personField, input) {
recalcAllAllocations();
}
function getMonthName(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('fr-FR', { month: 'long' }).toUpperCase();
}
function duplicateMonth() {
const targetDateStr = months[0];
const sourceDateStr = months[1];
if (!sourceDateStr) {
alert("Impossible : pas d'historique disponible pour copier.");
alert(tr("bud_prev_err_no_history"));
return;
}
const formatMonth = (d) => {
let str = new Date(d).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
let str = new Date(d).toLocaleDateString(currentLang, { month: 'long', year: 'numeric' });
return str.charAt(0).toUpperCase() + str.slice(1);
};
const sourceName = formatMonth(sourceDateStr);
const targetName = formatMonth(targetDateStr);
const message = `Voulez-vous copier les données de ${sourceName} vers ${targetName} ?\n\n⚠️ Cela écrasera toutes les valeurs déjà présentes pour ${targetName}.`;
const message = tr("bud_prev_confirm_copy").replace('%s', sourceName).replace('%t', targetName);
if(!confirm(message)) return;
@@ -699,13 +705,13 @@ function recalcAllAllocations() {
const elRestLaia = document.getElementById('restant_alloc_laia_' + m);
elRestAlex.innerText = Math.round(restAlex) + ' €';
elRestAlex.className = 'val-' + (restAlex >= 0 ? 'ok' : 'ko') + ' sum-target'; // On maintient la classe sum-target !
elRestAlex.className = 'val-' + (restAlex >= 0 ? 'ok' : 'ko') + ' sum-target';
elRestLaia.innerText = Math.round(restLaia) + ' €';
elRestLaia.className = 'val-' + (restLaia >= 0 ? 'ok' : 'ko') + ' sum-target';
});
updateSummaryTable();
if(isSumModeActive) updateSumResult(); // Force recalcul si la calculatrice est ouverte
if(isSumModeActive) updateSumResult();
}
function updateSummaryTable() {
@@ -739,18 +745,18 @@ function updateSummaryTable() {
const laiaSum = dataByTarget[targetName] ? dataByTarget[targetName].laia : 0;
const globalSum = alexSum + laiaSum;
row.cells[1].innerText = Math.round(alexSum).toLocaleString('fr-FR') + ' €';
row.cells[2].innerText = Math.round(laiaSum).toLocaleString('fr-FR') + ' €';
row.cells[3].innerText = Math.round(globalSum).toLocaleString('fr-FR') + ' €';
row.cells[1].innerText = Math.round(alexSum).toLocaleString(currentLang) + ' €';
row.cells[2].innerText = Math.round(laiaSum).toLocaleString(currentLang) + ' €';
row.cells[3].innerText = Math.round(globalSum).toLocaleString(currentLang) + ' €';
grandTotalAlex += alexSum;
grandTotalLaia += laiaSum;
});
}
document.getElementById('grand_total_alex').innerText = Math.round(grandTotalAlex).toLocaleString('fr-FR') + ' €';
document.getElementById('grand_total_laia').innerText = Math.round(grandTotalLaia).toLocaleString('fr-FR') + ' €';
document.getElementById('grand_total_global').innerText = Math.round(grandTotalAlex + grandTotalLaia).toLocaleString('fr-FR') + ' €';
document.getElementById('grand_total_alex').innerText = Math.round(grandTotalAlex).toLocaleString(currentLang) + ' €';
document.getElementById('grand_total_laia').innerText = Math.round(grandTotalLaia).toLocaleString(currentLang) + ' €';
document.getElementById('grand_total_global').innerText = Math.round(grandTotalAlex + grandTotalLaia).toLocaleString(currentLang) + ' €';
}
function saveData(action, data) {
@@ -761,7 +767,8 @@ function saveData(action, data) {
}
function validateTransfers(person, month) {
if (!confirm(`Confirmer que ${person} a bien effectué tous ses virements pour ${month} ?\n\nCela mettra à jour l'Épargne automatiquement.`)) return;
const msg = tr("bud_prev_confirm_transfers").replace('%p', person).replace('%m', month);
if (!confirm(msg)) return;
const formData = new FormData();
formData.append('action', 'validate_transfers');
@@ -777,7 +784,7 @@ function validateTransfers(person, month) {
if(data.success) window.location.reload();
else alert("Erreur: " + data.error);
})
.catch(e => alert("Erreur technique"));
.catch(e => alert(tr("bud_err_tech")));
}
function saveGenericNote(noteType, refId, content) {
@@ -845,7 +852,6 @@ function toggleSumMode() {
function extractNumberFromText(text) {
if (!text) return 0;
// Remplace les virgules par des points et supprime tout ce qui n'est pas chiffre, point ou signe moins
const cleanText = text.replace(',', '.').replace(/[^\d.-]/g, '');
return parseFloat(cleanText) || 0;
}
@@ -862,13 +868,12 @@ function updateSumResult() {
total += val;
});
document.getElementById('sumResultValue').innerText = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total);
document.getElementById('sumResultValue').innerText = new Intl.NumberFormat(currentLang, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total);
}
document.addEventListener('click', function(e) {
if (!isSumModeActive) return;
// Cible les inputs type number OU les cellules (td/th/div) ayant la classe .sum-target
const targetElement = e.target.closest('input[type="number"], .sum-target');
if (targetElement) {
@@ -884,7 +889,7 @@ document.addEventListener('click', function(e) {
updateSumResult();
}
}, true); // Le 'true' intercepte le clic avant le focus normal
}, true);
document.addEventListener('DOMContentLoaded', recalcAllAllocations);
</script>
+79 -47
View File
@@ -13,6 +13,13 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
$cycleConfigs[$mKey] = json_decode($row['content'], true);
}
}
// Récupération sécurisée du nom des mois (utilise les clés globales existantes)
function getMonthName($dateString) {
$m = date('m', strtotime($dateString));
$y = date('Y', strtotime($dateString));
return tr('month_' . $m) . ' ' . $y;
}
?>
<div class="budget-view">
@@ -48,7 +55,7 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
$ownerTextClass = '';
if ($currentOwner === 'Alex') $ownerTextClass = 'txt-alex';
elseif ($currentOwner === 'Laia') $ownerTextClass = 'txt-laia';
else $ownerTextClass = 'txt-global'; // Pour Pol et Pep (ou autre)
else $ownerTextClass = 'txt-global'; // Pour Pol et Pep
?>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
@@ -65,11 +72,11 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<div style="display: flex; gap: 10px; align-items: center;">
<?php if (!empty($months)): ?>
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= $currentOwner ?>')" class="pf-btn btn-secondary">
🔁 +1 Mois
🔁 <?= tr('bud_sav_add_one_month') ?>
</button>
<?php endif; ?>
<button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn">
Saisir un mois
<?= tr('bud_sav_add_month') ?>
</button>
</div>
</div>
@@ -77,32 +84,32 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0;">
<?php if (empty($months)): ?>
<div style="padding: 30px; text-align: center; color: #64748b;">
<p>Aucune donnée pour <?= htmlspecialchars($currentOwner) ?>.</p>
<p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p>
</div>
<?php else: ?>
<table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
<thead>
<tr>
<th class="sticky-col" style="background:#f8fafc;">Poste / Mois</th>
<th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></th>
<?php foreach ($months as $month): ?>
<th>
<div class="month-header-container" style="display: flex; flex-direction: column; align-items: center; gap: 6px;">
<div style="display:flex; flex-direction:column; text-align:center;">
<span class="month-name"><?= date('M Y', strtotime($month)) ?></span>
<span class="month-name" style="text-transform:capitalize;"><?= getMonthName($month) ?></span>
<?php
if (isset($cycleConfigs[$month]) && !empty($cycleConfigs[$month]['start_date'])) {
$cStart = date('d/m', strtotime($cycleConfigs[$month]['start_date']));
echo "<span style='font-size:0.75rem; font-weight:normal; color:#64748b;'>Dès le $cStart</span>";
echo "<span style='font-size:0.75rem; font-weight:normal; color:#64748b;'>" . sprintf(tr('bud_sav_from_date'), $cStart) . "</span>";
}
?>
</div>
<div class="month-actions" style="justify-content: center; width: 100%;">
<button class="btn-icon-small" title="Modifier avec Modale"
<button class="btn-icon-small" title="<?= tr('bud_sav_edit_modal') ?>"
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", JSON.parse(this.getAttribute("data-json")))'>
✏️
</button>
<button class="btn-icon-small" title="Supprimer tout le mois"
<button class="btn-icon-small" title="<?= tr('bud_sav_delete_month') ?>"
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
🗑️
@@ -115,7 +122,7 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
</thead>
<tbody>
<tr class="row-total">
<td class="sticky-col"><strong>Total Banque</strong></td>
<td class="sticky-col"><strong><?= tr('bud_sav_total_bank') ?></strong></td>
<?php foreach ($months as $month):
$val = $data[$month]['TOTAL_BANQUE'] ?? 0;
?>
@@ -154,7 +161,7 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<?php endforeach; ?>
<tr class="row-extres">
<td class="sticky-col"><strong>Extra</strong></td>
<td class="sticky-col"><strong><?= tr('bud_sav_extra') ?></strong></td>
<?php foreach ($months as $month):
$total = $data[$month]['TOTAL_BANQUE'] ?? 0;
$sum = 0;
@@ -176,8 +183,8 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<div id="savingsModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 600px; width: 95%;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 id="savingsModalTitle" class="pf-modal-title" style="margin:0;">Saisir un mois</h3>
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
<h3 id="savingsModalTitle" class="pf-modal-title" style="margin:0;"><?= tr('bud_sav_modal_title_add') ?></h3>
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
</div>
<form action="/modules/budget/includes/api/save-savings.php" method="POST" id="savingsForm">
@@ -187,12 +194,12 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<div style="display:flex; gap:15px; margin-bottom:20px;">
<div class="form-group" style="flex:1; margin:0;">
<label class="pf-label">Mois concerné</label>
<label class="pf-label"><?= tr('bud_sav_month_concerned') ?></label>
<input type="month" id="sav_month" required class="pf-input">
</div>
<div class="form-group" style="flex:1; margin:0;">
<label class="pf-label">Total en Banque (€)</label>
<label class="pf-label"><?= tr('bud_sav_total_bank_eur') ?></label>
<input type="number" step="0.01" name="values[TOTAL_BANQUE]" id="sav_total" required class="pf-input no-spinners" style="font-weight:bold; color:#2563eb;">
</div>
</div>
@@ -201,17 +208,17 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px;">
<div>
<h4 style="margin:0; font-size:1rem; color:#1e293b;">Ventilation</h4>
<span style="font-size:0.8rem; color:#64748b;">Utilisez l'ajustement (+/-) pour recalculer automatiquement.</span>
<h4 style="margin:0; font-size:1rem; color:#1e293b;"><?= tr('bud_sav_ventilation') ?></h4>
<span style="font-size:0.8rem; color:#64748b;"><?= tr('bud_sav_adj_help') ?></span>
</div>
<button type="button" class="pf-btn btn-secondary" onclick="addCustomEpargneLine()" style="padding:4px 10px; height:auto; width:auto; font-size:0.9rem;"> Ligne</button>
<button type="button" class="pf-btn btn-secondary" onclick="addCustomEpargneLine()" style="padding:4px 10px; height:auto; width:auto; font-size:0.9rem;"> <?= tr('bud_sav_add_line') ?></button>
</div>
<div style="display:flex; gap:10px; padding:0 5px 5px 5px; font-size:0.8rem; color:#64748b; font-weight:600;">
<div style="flex:2;">Catégorie</div>
<div style="width:100px;">Actuel</div>
<div style="width:90px;">Ajust (+/-)</div>
<div style="width:100px;">Nouveau</div>
<div style="flex:2;"><?= tr('bud_category') ?></div>
<div style="width:100px;"><?= tr('bud_sav_current') ?></div>
<div style="width:90px;"><?= tr('bud_sav_adjust') ?></div>
<div style="width:100px;"><?= tr('bud_sav_new') ?></div>
<div style="width:28px;"></div>
</div>
@@ -219,22 +226,41 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
</div>
<div style="margin-top:25px; display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'" class="pf-btn btn-secondary" style="width:auto; margin:0;">Annuler</button>
<button type="submit" class="pf-btn" style="width:auto; margin:0;">Enregistrer</button>
<button type="button" onclick="document.getElementById('savingsModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary" style="width:auto; margin:0;"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn" style="width:auto; margin:0;"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
</div>
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()" title="Activer l'addition rapide">
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()" title="<?= tr('bud_sav_sum_mode_title') ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="8" y1="12" x2="16" y2="12"></line><line x1="12" y1="8" x2="12" y2="16"></line></svg>
</button>
<div id="sumResultBar" class="pf-sum-bar">
<span class="pf-sum-label">lection :</span>
<span class="pf-sum-label"><?= tr('bud_sav_selection') ?></span>
<span id="sumResultValue" class="pf-sum-value">0 €</span>
<button onclick="toggleSumMode()" class="pf-sum-close" title="Fermer">&times;</button>
<button onclick="toggleSumMode()" class="pf-sum-close" title="<?= tr('btn_close') ?>">&times;</button>
</div>
<script>
window.I18N = {
...window.I18N,
'bud_sav_ph_name' : "<?= tr('bud_sav_ph_name') ?>",
'bud_sav_modal_title_edit' : "<?= tr('bud_sav_modal_title_edit') ?>",
'bud_sav_modal_title_add' : "<?= tr('bud_sav_modal_title_add') ?>",
'bud_sav_saving' : "<?= tr('bud_sav_saving') ?>",
'bud_err_tech' : "<?= tr('bud_err_tech') ?>",
'bud_sav_confirm_delete_month' : "<?= tr('bud_sav_confirm_delete_month') ?>",
'bud_sav_prompt_duplicate' : "<?= tr('bud_sav_prompt_duplicate') ?>",
'bud_err_server' : "<?= tr('bud_err_server') ?>",
'bud_err_network_dup' : "<?= tr('bud_err_network_dup') ?>"
};
// Détection de la langue pour le JS
const currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
</script>
<script>
// ============================================================================
// 1. GESTION DE L'ÉDITION INVISIBLE EN DIRECT (Input Classique)
@@ -243,7 +269,6 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
function updateEpargneCell(month, category, owner, inputEl) {
const val = parseFloat(inputEl.value) || 0;
// 1. Sauvegarde silencieuse en Ajax
const formData = new FormData();
formData.append('action', 'update_single_entry');
formData.append('month_date', month);
@@ -254,9 +279,8 @@ function updateEpargneCell(month, category, owner, inputEl) {
fetch('/modules/budget/includes/api/save-savings.php', {
method: 'POST',
body: formData
}).catch(err => alert("Erreur lors de la sauvegarde"));
}).catch(err => alert(tr("bud_err_tech")));
// 2. Recalcul visuel de l'Extra instantané
const totalInput = document.querySelector(`.total-input-${owner}-${month}`);
const totalVal = parseFloat(totalInput.value) || 0;
@@ -269,11 +293,10 @@ function updateEpargneCell(month, category, owner, inputEl) {
const extraCell = document.getElementById(`extra_${owner}_${month}`);
if (extraCell) {
extraCell.innerText = Math.round(extra).toLocaleString('fr-FR') + ' €';
extraCell.innerText = Math.round(extra).toLocaleString(currentLang) + ' €';
extraCell.style.color = extra >= 0 ? '#10b981' : '#ef4444';
}
// Si la calculatrice est active, on met à jour son résultat aussi
if(isSumModeActive) updateSumResult();
}
@@ -290,7 +313,7 @@ function addCustomEpargneLine(catName = '', amount = '') {
const html = `
<div class="ventilation-line" style="display:flex; gap:10px; align-items:center; background:#f8fafc; padding:8px; border-radius:8px; border:1px solid #e2e8f0;">
<div style="flex:2;">
<input type="text" class="pf-input cat-name-input" value="${catName}" placeholder="Nom (ex: Vacances)" oninput="updateCustomFieldName(this)" style="padding:6px; font-size:0.9rem;" required>
<input type="text" class="pf-input cat-name-input" value="${catName}" placeholder="${tr("bud_sav_ph_name")}" oninput="updateCustomFieldName(this)" style="padding:6px; font-size:0.9rem;" required>
</div>
<div style="width:100px;">
@@ -337,8 +360,8 @@ function editCustomSavingsMonth(monthDate, owner, rowData) {
document.getElementById('sav_month').value = ym;
const dateObj = new Date(monthDate);
const monthName = dateObj.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
document.getElementById('savingsModalTitle').innerText = "Modifier : " + monthName + " (" + owner + ")";
const monthName = dateObj.toLocaleDateString(currentLang, { month: 'long', year: 'numeric' });
document.getElementById('savingsModalTitle').innerText = tr("bud_sav_modal_title_edit") + " " + monthName + " (" + owner + ")";
document.getElementById('sav_total').value = rowData['TOTAL_BANQUE'] || '';
@@ -352,6 +375,7 @@ function editCustomSavingsMonth(monthDate, owner, rowData) {
if (container.children.length === 0) addCustomEpargneLine();
document.getElementById('savingsModal').style.display = 'flex';
document.body.classList.add('no-scroll');
}
function openCustomSavingsModal(owner) {
@@ -359,18 +383,22 @@ function openCustomSavingsModal(owner) {
document.getElementById('sav_month').value = '';
document.getElementById('sav_total').value = '';
document.getElementById('savingsModalTitle').innerText = "Saisir un mois (" + owner + ")";
document.getElementById('savingsModalTitle').innerText = tr("bud_sav_modal_title_add") + " (" + owner + ")";
const container = document.getElementById('linesContainer');
container.innerHTML = '';
addCustomEpargneLine();
document.getElementById('savingsModal').style.display = 'flex';
document.body.classList.add('no-scroll');
}
window.onclick = function(event) {
const modal = document.getElementById('savingsModal');
if (event.target == modal) modal.style.display = 'none';
if (event.target == modal) {
modal.style.display = 'none';
document.body.classList.remove('no-scroll');
}
}
const savingsForm = document.getElementById('savingsForm');
@@ -385,7 +413,7 @@ if (savingsForm) {
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.innerText;
submitBtn.innerText = "Enregistrement...";
submitBtn.innerText = tr('bud_sav_saving');
submitBtn.disabled = true;
const formData = new FormData(this);
@@ -395,7 +423,7 @@ if (savingsForm) {
.then(text => { window.location.reload(); })
.catch(error => {
console.error("Erreur:", error);
alert("Une erreur technique est survenue.");
alert(tr("bud_err_tech"));
submitBtn.innerText = originalText;
submitBtn.disabled = false;
});
@@ -403,14 +431,16 @@ if (savingsForm) {
}
function deleteEntireMonth(monthDate, owner) {
if (!confirm(`Supprimer TOUT le mois de ${monthDate} pour ${owner} ?`)) return;
const msg = tr('bud_sav_confirm_delete_month').replace('%m', monthDate).replace('%o', owner);
if (!confirm(msg)) return;
const formData = new FormData();
formData.append("action", "delete_month_global");
formData.append("month_date", monthDate);
formData.append("owner", owner);
fetch("/modules/budget/includes/api/save-savings.php", { method: "POST", body: formData })
.then(() => window.location.reload())
.catch(err => alert("Erreur lors de la suppression."));
.catch(err => alert("<?= tr('bud_err_delete') ?>"));
}
function duplicateLastMonth(lastMonthDate, owner) {
@@ -421,7 +451,7 @@ function duplicateLastMonth(lastMonthDate, owner) {
let nextMonthStr = `${year}-${month}-01`;
const formatMonth = (d) => {
let str = new Date(d).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
let str = new Date(d).toLocaleDateString(currentLang, { month: 'long', year: 'numeric' });
return str.charAt(0).toUpperCase() + str.slice(1);
};
@@ -433,7 +463,10 @@ function duplicateLastMonth(lastMonthDate, owner) {
defaultTotal = cycleConfigs[nextMonthStr].start_balance;
}
const message = `Voulez-vous copier les données de ${sourceName} vers ${targetName} ?\n\nSaisissez le nouveau TOTAL en banque (€) pour ${targetName} :`;
const message = tr('bud_sav_prompt_duplicate')
.replace('%s', sourceName)
.replace('%t1', targetName)
.replace('%t2', targetName);
let newTotal = prompt(message, defaultTotal);
@@ -451,15 +484,14 @@ function duplicateLastMonth(lastMonthDate, owner) {
try {
const d = JSON.parse(text);
if (d.success) window.location.reload();
else alert("Erreur serveur : " + (d.error || "Inconnue"));
else alert(tr('bud_err_server') + (d.error || "Inconnue"));
} catch(e) {
console.log("Avertissement : La réponse serveur n'était pas du JSON. Rechargement...");
window.location.reload();
}
})
.catch(err => {
console.error(err);
alert("Erreur réseau lors de la duplication.");
alert(tr("bud_err_network_dup"));
});
}
}
@@ -509,7 +541,7 @@ function updateSumResult() {
total += val;
});
document.getElementById('sumResultValue').innerText = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total);
document.getElementById('sumResultValue').innerText = new Intl.NumberFormat(currentLang, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total);
}
document.addEventListener('click', function(e) {
+64 -54
View File
@@ -17,8 +17,12 @@ $currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEF
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth));
$viewMonthDate = "$currentYear-$currentMonth-01";
// Conservation du tableau d'origine pour la correspondance en base de données
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
$currentMonthName = $moisFr[(int)$currentMonth] . ' ' . $currentYear;
// Affichage dynamique traduit
$monthTranslationKey = 'month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT);
$currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear;
// 4. Récupération des Dépenses Réelles du mois (Basé sur le Mois de Gestion !)
$stmtExp = $pdo->prepare("SELECT amount, label, category, budget_item_id FROM pf_expenses WHERE gestion_month = ?");
@@ -31,20 +35,20 @@ $totalRevenus = 0;
<div class="budget-view">
<div class="view-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2 style="margin:0;">Récapitulatif Mensuel</h2>
<button onclick="openRecapModal('add')" class="pf-btn"> Ajouter un frais / revenu</button>
<h2 style="margin:0;"><?= tr('bud_recap_monthly_title') ?></h2>
<button onclick="openRecapModal('add')" class="pf-btn"> <?= tr('bud_recap_btn_add') ?></button>
</div>
<div class="table-responsive" style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0; overflow:hidden;">
<table class="pf-table" style="margin:0; box-shadow:none;">
<thead style="background:#f8fafc;">
<tr>
<th>Nom</th>
<th>Montant Prévu</th>
<th>Type</th>
<th>Jour</th>
<th>État du mois (<?= $currentMonthName ?>)</th>
<th style="text-align:right;">Actions</th>
<th><?= tr('bud_label_name') ?></th>
<th><?= tr('bud_planned_amount') ?></th>
<th><?= tr('bud_type') ?></th>
<th><?= tr('bud_day') ?></th>
<th><?= tr('bud_month_state') ?> (<?= $currentMonthName ?>)</th>
<th style="text-align:right;"><?= tr('actions') ?></th>
</tr>
</thead>
<tbody>
@@ -109,7 +113,7 @@ $totalRevenus = 0;
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid #f1f5f9;">
<td style="padding:15px;">
<strong><?= htmlspecialchars($item['name']) ?></strong>
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">(Est.)</small>' : '' ?>
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">('.tr('bud_est_short').')</small>' : '' ?>
<?php if(!empty($item['mapping_keywords'])): ?>
<span title="<?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
@@ -117,7 +121,7 @@ $totalRevenus = 0;
<?php if(!empty($item['reg_month'])): ?>
<div style="font-size:0.75rem; color:#94a3b8; font-style:italic; margin-top:2px;">
📅 Régul. prévue en <?= htmlspecialchars($item['reg_month']) ?>
📅 <?= sprintf(tr('bud_reg_planned_in'), tr('month_'.str_pad(array_search($item['reg_month'], $moisFr), 2, '0', STR_PAD_LEFT))) ?>
</div>
<?php endif; ?>
</td>
@@ -128,30 +132,29 @@ $totalRevenus = 0;
<?php if ($hasMatchingExpense): ?>
<?php
// Le "Gap" est la différence visuelle.
// S'il y a plus de $realAbs que prévu, c'est un dépassement ou un bonus.
$gap = $realAbs - $targetAbs;
if ($gap > 0.05): ?>
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">
<?= $item['category'] === 'income' ? 'Bonus :' : 'Dépassement :' ?> +<?= number_format($gap, 2, ',', ' ') ?> €
<?= $item['category'] === 'income' ? tr('bud_bonus') : tr('bud_overrun') ?> : +<?= number_format($gap, 2, ',', ' ') ?> €
</div>
<?php elseif ($gap < -0.05): ?>
<div style="font-size:0.75rem; color:#f59e0b; font-weight:normal;">
Reste : <?= number_format(abs($gap), 2, ',', ' ') ?> €
<?= tr('bud_remaining') ?> : <?= number_format(abs($gap), 2, ',', ' ') ?> €
</div>
<?php else: ?>
<div style="font-size:0.75rem; color:#10b981; font-weight:normal;">
Montant exact
<?= tr('bud_exact_amount') ?>
</div>
<?php endif; ?>
<?php elseif ($item['type'] === 'Annuel'): ?>
<div style="font-size:0.75rem; color:#94a3b8; font-weight:normal;">Soit <?= number_format($amountToAdd, 2, ',', ' ') ?>/mois</div>
<div style="font-size:0.75rem; color:#94a3b8; font-weight:normal;"><?= tr('bud_per_month_short') ?> <?= number_format($amountToAdd, 2, ',', ' ') ?>/<?= tr('bud_month_short') ?></div>
<?php endif; ?>
</td>
<td style="padding:15px;">
<span class="badge-type <?= strtolower($item['type']) ?>" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
<?= $item['type'] ?>
<?= tr('bud_freq_'.strtolower($item['type'])) ?>
</span>
</td>
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td>
@@ -159,23 +162,23 @@ $totalRevenus = 0;
<td style="padding:15px;">
<?php if ($isAutoChecked): ?>
<div style="display:inline-flex; align-items:center; gap:5px; background:#f0fdf4; color:#16a34a; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #bbf7d0;">
<span>✓</span> Validé
<span>✓</span> <?= tr('bud_state_validated') ?>
</div>
<?php elseif($hasMatchingExpense): ?>
<div style="display:inline-flex; align-items:center; gap:5px; background:#fffbeb; color:#d97706; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #fde68a;">
<span>⏳</span> Partiel
<span>⏳</span> <?= tr('bud_state_partial') ?>
</div>
<?php else: ?>
<div style="display:inline-flex; align-items:center; gap:5px; background:#f8fafc; color:#94a3b8; padding:4px 10px; border-radius:20px; font-size:0.85rem; border:1px solid #e2e8f0;">
<span>○</span> En attente
<span>○</span> <?= tr('bud_state_waiting') ?>
</div>
<?php endif; ?>
</td>
<td style="padding:15px; text-align:right;">
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
<button class="btn-icon" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">✏️</button>
<button class="btn-icon" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="Supprimer" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">🗑️</button>
<button class="btn-icon" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">✏️</button>
<button class="btn-icon" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>" style="background:none; border:none; cursor:pointer; font-size:1.1rem; filter:grayscale(1); transition:0.2s;">🗑️</button>
</div>
</td>
</tr>
@@ -183,18 +186,18 @@ $totalRevenus = 0;
</tbody>
<tfoot style="background:#f8fafc;">
<tr>
<td colspan="1" style="padding:15px;"><strong>Total Revenus (Lissés)</strong></td>
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_income_smoothed') ?></strong></td>
<td colspan="5" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenus, 2, ',', ' ') ?> €</strong></td>
</tr>
<tr>
<td colspan="1" style="padding:15px;"><strong>Totalpenses (Lissées)</strong></td>
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_expenses_smoothed') ?></strong></td>
<td colspan="5" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepenses, 2, ',', ' ') ?> €</strong></td>
</tr>
<tr style="border-top: 2px solid #e2e8f0; background: white;">
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong>Équilibre théorique</strong></td>
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong><?= tr('bud_theoretical_balance_recap') ?></strong></td>
<?php $balance = $totalRevenus - $totalDepenses; ?>
<td colspan="5" style="padding:15px; font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>">
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong>
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / <?= tr('bud_month_short') ?></strong>
</td>
</tr>
</tfoot>
@@ -202,7 +205,7 @@ $totalRevenus = 0;
</div>
<div class="budget-note" style="margin-top:15px; font-size:0.85rem; color:#64748b;">
<p>* L'état de paiement se met à jour automatiquement en fonction des opérations importées dans l'onglet "Suivi Mensuel".</p>
<p>* <?= tr('bud_recap_footer_note') ?></p>
</div>
</div>
@@ -210,7 +213,7 @@ $totalRevenus = 0;
<div class="pf-modal-content" style="background:white; width:95%; max-width:500px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px; position:relative;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;">Ajouter un élément</h3>
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;"><?= tr('bud_recap_modal_add') ?></h3>
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">&times;</button>
</div>
@@ -219,67 +222,70 @@ $totalRevenus = 0;
<input type="hidden" name="id" id="item_id">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Nom</label>
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="Ex: Loyer, Salaire..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_name') ?></label>
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="<?= tr('bud_ph_item_name') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Mots-clés (Détection Auto)</label>
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="Ex: NETFLIX, PRIME VIDEO (séparés par virgule)" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
<small style="color:#64748b; font-size:0.75rem;">Si une dépense du mois contient ce mot, la ligne passera en "Validé".</small>
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_keywords') ?></label>
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="<?= tr('bud_ph_keywords') ?>" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
<small style="color:#64748b; font-size:0.75rem;"><?= tr('bud_help_keywords') ?></small>
</div>
<div style="display:flex; gap:15px; margin-bottom:15px;">
<div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Montant (€)</label>
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_amount_eur') ?></label>
<input type="number" step="0.01" name="amount" id="item_amount" required class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
</div>
<div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Jour (1-31)</label>
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_day') ?></label>
<input type="number" min="1" max="31" name="payment_day" id="item_day" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px;">
</div>
</div>
<div style="display:flex; gap:15px; margin-bottom:15px;">
<div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Catégorie</label>
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_category') ?></label>
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
<option value="expense">Dépense (Frais)</option>
<option value="income">Revenu (Salaire)</option>
<option value="expense"><?= tr('bud_cat_expense') ?></option>
<option value="income"><?= tr('bud_cat_income') ?></option>
</select>
</div>
<div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Fréquence</label>
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_frequency') ?></label>
<select name="type" id="item_type" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
<option value="Mensuel">Mensuel</option>
<option value="Annuel">Annuel</option>
<option value="Mensuel"><?= tr('bud_freq_mensuel') ?></option>
<option value="Annuel"><?= tr('bud_freq_annuel') ?></option>
</select>
</div>
</div>
<div style="display:flex; gap:15px; margin-bottom:25px;">
<div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Type de montant</label>
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_amount_type') ?></label>
<select name="is_estimate" id="item_is_estimate" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
<option value="0">Fixe (Facture)</option>
<option value="1">Variable (Estimation)</option>
<option value="0"><?= tr('bud_type_fixed') ?></option>
<option value="1"><?= tr('bud_type_variable') ?></option>
</select>
</div>
<div style="flex:1;">
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">gularisation</label>
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_regularization') ?></label>
<select name="reg_month" id="item_reg_month" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
<option value="">Aucune</option>
<option value=""><?= tr('bud_reg_none') ?></option>
<?php
$mois = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
foreach($mois as $m) echo "<option value='$m'>$m</option>";
foreach($moisFr as $index => $m) {
if($index == 0) continue;
// On injecte la valeur en français (pour la DB) mais on affiche la version traduite
echo "<option value='$m'>" . tr('month_'.str_pad($index, 2, '0', STR_PAD_LEFT)) . "</option>";
}
?>
</select>
</div>
</div>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Annuler</button>
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;">Enregistrer</button>
<button type="button" onclick="closeRecapModal()" class="pf-btn btn-secondary" style="width:auto; margin:0; background:#f1f5f9; color:#475569; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn" style="width:auto; margin:0; background:#2563eb; color:white; border:none; padding:10px 20px; border-radius:8px; font-weight:600; cursor:pointer;"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
@@ -290,26 +296,29 @@ window.onclick = function(event) {
const modal = document.getElementById('budgetRecapModal');
if (event.target == modal) {
modal.style.display = 'none';
document.body.classList.remove('no-scroll');
}
}
function openRecapModal(mode) {
if (mode === "add") {
document.getElementById("recapModalTitle").innerText = "Ajouter un élément";
document.getElementById("recapModalTitle").innerText = "<?= tr('bud_recap_modal_add') ?>";
document.getElementById("item_id").value = "";
document.getElementById("recapForm").reset();
}
document.getElementById("budgetRecapModal").style.display = "flex";
document.body.classList.add('no-scroll');
}
function closeRecapModal() {
document.getElementById("budgetRecapModal").style.display = "none";
document.body.classList.remove('no-scroll');
}
function editRecapItem(item) {
const data = typeof item === "string" ? JSON.parse(item) : item;
document.getElementById("recapModalTitle").innerText = "Modifier : " + data.name;
document.getElementById("recapModalTitle").innerText = "<?= tr('bud_recap_modal_edit') ?> : " + data.name;
document.getElementById("item_id").value = data.id;
document.getElementById("item_name").value = data.name;
document.getElementById("item_keywords").value = data.mapping_keywords || '';
@@ -322,10 +331,11 @@ function editRecapItem(item) {
document.getElementById("item_is_estimate").value = data.is_estimate;
document.getElementById("budgetRecapModal").style.display = "flex";
document.body.classList.add('no-scroll');
}
function deleteRecapItem(id) {
if (confirm("Voulez-vous vraiment supprimer cet élément ?")) {
if (confirm("<?= tr('bud_recap_confirm_delete') ?>")) {
const formData = new FormData();
formData.append("action", "delete");
formData.append("id", id);
@@ -336,7 +346,7 @@ function deleteRecapItem(id) {
}).then(() => {
window.location.reload();
}).catch(err => {
alert("Erreur lors de la suppression.");
alert("<?= tr('bud_err_delete') ?>");
console.error(err);
});
}
+78 -83
View File
@@ -5,14 +5,12 @@
// 1. GESTION DU MOIS ACTIF ET DE LA NAVIGATION
// ============================================================================
// A. Déterminer quel est le mois "ouvert" par défaut
$stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1");
$defaultActiveMonth = $stmtActive->fetchColumn();
if (!$defaultActiveMonth) {
$defaultActiveMonth = date('Y-m-01');
}
// B. Mois affiché à l'écran
$viewM = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth));
$viewY = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth));
$viewMonthDate = "$viewY-$viewM-01";
@@ -168,21 +166,18 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
// 3. RECUPERATION DES DONNEES ET CALCULS
// ============================================================================
// A. Vérifier si le mois affiché est clôturé
$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
$stmtCheckClose->execute([$viewMonthDate]);
$closureJson = $stmtCheckClose->fetchColumn();
$monthState = $closureJson ? json_decode($closureJson, true) : null;
$isClosed = ($monthState && isset($monthState['is_closed']) && $monthState['is_closed'] === true);
// B. Récupérer le solde bancaire (Snapshot actuel)
$snapshot = ['date' => date('Y-m-d'), 'amount' => 0];
$snapStmt = $pdo->query("SELECT * FROM pf_bank_snapshots ORDER BY id DESC LIMIT 1");
if ($s = $snapStmt->fetch(PDO::FETCH_ASSOC)) {
$snapshot = ['date' => $s['snapshot_date'], 'amount' => (float)$s['amount']];
}
// C. Récupérer le solde final du mois PRÉCÉDENT pour avoir la base de ce mois-ci
$stmtPrevClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
$stmtPrevClose->execute([$prevDate]);
$prevClosureJson = $stmtPrevClose->fetchColumn();
@@ -193,7 +188,6 @@ if ($solde_initial === 0 && !$prevMonthState) {
$solde_initial = $snapshot['amount'];
}
// D. Les Dépenses assignées à ce MOIS DE GESTION
$stmt = $pdo->prepare("SELECT * FROM pf_expenses WHERE gestion_month = ? ORDER BY date_exp DESC");
$stmt->execute([$viewMonthDate]);
$allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
@@ -201,13 +195,11 @@ $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
$paidItemIds = array_column(array_filter($allExpenses, fn($e) => !empty($e['budget_item_id'])), 'budget_item_id');
$realExpensesLabels = array_column(array_filter($allExpenses, fn($e) => $e['amount'] < 0), 'label');
// E. Lecture du Budget
$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_income_prevu = 0;
$total_income = 0; $total_expenses_prevues = 0;
$reste_a_venir_calc = 0;
$fixedChargesList = []; $incomeList = []; $pending_charges = [];
// CORRECTION BUG N°2 : On supprime la vérification "is_checked" de la base, on ne se fie qu'aux dépenses réelles du mois.
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items ORDER BY name ASC");
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
$absAmount = abs((float)$item['amount']);
@@ -249,26 +241,25 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
$budget_autres = max(0, $total_income - $total_expenses_prevues);
// TRADUCTION DES CATÉGORIES
$categoriesConfig = [
'Income' => ['type'=>'credit', 'label'=>'Revenus', 'budget'=>$budget_income_prevu, 'color'=>'#10b981', 'suggestions'=>[]],
'FMCG' => ['type'=>'debit', 'label'=>'Courses (FMCG)', 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']],
'Essence' => ['type'=>'debit', 'label'=>'Essence', 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']],
'School' => ['type'=>'debit', 'label'=>'École / Garde', 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
'Frais' => ['type'=>'debit', 'label'=>'Charges Fixes', 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]],
'Autres' => ['type'=>'debit', 'label'=>'Autres / Imprévus', 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']],
'LivretA' => ['type'=>'debit', 'label'=>'Epargne', 'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
'Income' => ['type'=>'credit', 'label'=>tr('cat_income'), 'budget'=>$budget_income_prevu, 'color'=>'#10b981', 'suggestions'=>[]],
'FMCG' => ['type'=>'debit', 'label'=>tr('cat_fmcg'), 'budget'=>$budget_fmcg, 'color'=>'#3b82f6', 'suggestions'=>['Action', 'Carrefour', 'Lidl']],
'Essence' => ['type'=>'debit', 'label'=>tr('cat_fuel'), 'budget'=>$budget_essence, 'color'=>'#f59e0b', 'suggestions'=>['Audi', 'Polo']],
'School' => ['type'=>'debit', 'label'=>tr('cat_school'), 'budget'=>$budget_school, 'color'=>'#10b981', 'suggestions'=>[]],
'Frais' => ['type'=>'debit', 'label'=>tr('cat_fixed'), 'budget'=>$budget_frais, 'color'=>'#ef4444', 'suggestions'=>[]],
'Autres' => ['type'=>'debit', 'label'=>tr('cat_others'), 'budget'=>$budget_autres, 'color'=>'#64748b', 'suggestions'=>['Restaurant', 'Cadeau']],
'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
];
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
$total_rentrees = 0;
$depenses_reelles = 0;
foreach ($allExpenses as $exp) {
$cat = $exp['category'];
if (!isset($totals[$cat])) $cat = 'Autres';
$val = (float)$exp['amount'];
if ($cat === 'Income') { $totals[$cat] += $val; }
@@ -286,28 +277,26 @@ foreach ($allExpenses as $exp) {
}
}
// 1. École
// Reste à venir dynamiques
$ecole_depense = isset($totals['School']) ? $totals['School'] : 0;
$reste_ecole = max(0, $budget_school - $ecole_depense);
if ($reste_ecole > 0) {
$reste_a_venir_calc += $reste_ecole;
$pending_charges[] = ['name' => 'École (Reste estimé)', 'amount' => $reste_ecole];
$pending_charges[] = ['name' => tr('bud_rem_school'), 'amount' => $reste_ecole];
}
// 2. Courses (FMCG)
$fmcg_depense = isset($totals['FMCG']) ? $totals['FMCG'] : 0;
$reste_fmcg = max(0, $budget_fmcg - $fmcg_depense);
if ($reste_fmcg > 0) {
$reste_a_venir_calc += $reste_fmcg;
$pending_charges[] = ['name' => 'Courses (Reste estimé)', 'amount' => $reste_fmcg];
$pending_charges[] = ['name' => tr('bud_rem_fmcg'), 'amount' => $reste_fmcg];
}
// 3. Essence
$essence_depense = isset($totals['Essence']) ? $totals['Essence'] : 0;
$reste_essence = max(0, $budget_essence - $essence_depense);
if ($reste_essence > 0) {
$reste_a_venir_calc += $reste_essence;
$pending_charges[] = ['name' => 'Essence (Reste estimé)', 'amount' => $reste_essence];
$pending_charges[] = ['name' => tr('bud_rem_fuel'), 'amount' => $reste_essence];
}
// F. Calculs des KPIs finaux
@@ -315,7 +304,6 @@ $rentrees_salaires_reels = $totals['Income'] ?? 0;
$rentrees_autres = $total_rentrees - $rentrees_salaires_reels;
$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu);
// Capacité Max : Solde fin de mois précédent + Salaire retenu + Autres rentrées
$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres;
$solde_theorique_calc = ($solde_initial + $total_rentrees) - $depenses_reelles - $reste_a_venir_calc;
@@ -347,8 +335,13 @@ function getDisplayLogic($spent, $bg, $type) {
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
}
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
$monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
// Noms des mois traduits
$monthNames = [
1 => tr('month_01'), 2 => tr('month_02'), 3 => tr('month_03'), 4 => tr('month_04'),
5 => tr('month_05'), 6 => tr('month_06'), 7 => tr('month_07'), 8 => tr('month_08'),
9 => tr('month_09'), 10 => tr('month_10'), 11 => tr('month_11'), 12 => tr('month_12')
];
$monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
?>
<div class="budget-view">
@@ -357,37 +350,37 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<?php if ($isClosed): ?>
<div style="position:absolute; top:0; left:0; right:0; background:#f1f5f9; padding:5px; text-align:center; font-size:0.8rem; font-weight:bold; color:#64748b; border-radius:16px 16px 0 0; border-bottom:1px solid #e2e8f0;">
🔒 Ce mois est clôturé et archivé.
🔒 <?= tr('bud_closed_archived') ?>
</div>
<div style="height:20px;"></div>
<?php endif; ?>
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; margin-bottom: 20px;">
<div style="display:flex; align-items:center; gap:15px;">
<h2 style="margin:0; font-size:1.3rem; color:#0f172a; text-transform:capitalize;">Budget : <?= $monthName ?></h2>
<h2 style="margin:0; font-size:1.3rem; color:#0f172a; text-transform:capitalize;"><?= tr('bud_budget_of') ?> <?= $monthName ?></h2>
</div>
<div style="display:flex; gap:10px;">
<?php if (!$isClosed): ?>
<form method="POST" onsubmit="return confirm('Clôturer ce mois ? Cela figera les soldes actuels et basculera le suivi par défaut sur le mois prochain.');">
<form method="POST" onsubmit="return confirm('<?= tr('bud_confirm_close') ?>');">
<input type="hidden" name="action" value="close_month">
<input type="hidden" name="close_month_date" value="<?= $viewMonthDate ?>">
<input type="hidden" name="freeze_solde_actuel" value="<?= $solde_actuel ?>">
<input type="hidden" name="freeze_capacite_max" value="<?= $capacite_max ?>">
<input type="hidden" name="freeze_solde_theorique" value="<?= $solde_theorique ?>">
<input type="hidden" name="freeze_reste_a_venir" value="<?= $reste_a_venir ?>">
<button type="submit" class="pf-btn" style="background:linear-gradient(135deg, #10b981, #059669); padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔒 Clôturer</button>
<button type="submit" class="pf-btn" style="background:linear-gradient(135deg, #10b981, #059669); padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔒 <?= tr('bud_close_btn') ?></button>
</form>
<?php else: ?>
<form method="POST" onsubmit="return confirm('Rouvrir ce mois ? Ses soldes seront recalculés en direct.');">
<form method="POST" onsubmit="return confirm('<?= tr('bud_confirm_reopen') ?>');">
<input type="hidden" name="action" value="reopen_month">
<button type="submit" class="pf-btn btn-secondary" style="padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔓 Rouvrir</button>
<button type="submit" class="pf-btn btn-secondary" style="padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">🔓 <?= tr('bud_reopen_btn') ?></button>
</form>
<?php endif; ?>
<div class="suivi-nav-group">
<a href="<?= $prevLink ?>" class="suivi-btn-nav">◀</a>
<a href="<?= $defaultLink ?>" class="suivi-btn-nav">Mois Actif</a>
<a href="<?= $defaultLink ?>" class="suivi-btn-nav"><?= tr('bud_active_month_btn') ?></a>
<a href="<?= $nextLink ?>" class="suivi-btn-nav">▶</a>
</div>
</div>
@@ -395,17 +388,17 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:15px; margin-bottom:25px;">
<div style="padding:15px; background:#f8fafc; border-radius:12px; position:relative; border:1px solid #e2e8f0;">
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;">Solde bancaire <?= $isClosed ? "(Figé)" : "(Actuel)" ?></div>
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;"><?= tr('bud_bank_balance') ?> <?= $isClosed ? "(".tr('bud_frozen').")" : "(".tr('bud_current').")" ?></div>
<div style="font-size:1.4rem; font-weight:700; color:#0f172a;">
<?= number_format($solde_actuel, 2, ',', ' ') ?> €
</div>
<?php if (!$isClosed): ?>
<button onclick="openSuiviModal('snapshotModal')" style="position:absolute; top:12px; right:12px; background:white; border:1px solid #cbd5e1; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.8rem; color:#475569; transition:0.2s;" onmouseover="this.style.background='#f1f5f9'" onmouseout="this.style.background='white'">✏️ MàJ</button>
<button onclick="openSuiviModal('snapshotModal')" style="position:absolute; top:12px; right:12px; background:white; border:1px solid #cbd5e1; border-radius:6px; padding:4px 8px; cursor:pointer; font-size:0.8rem; color:#475569; transition:0.2s;" onmouseover="this.style.background='#f1f5f9'" onmouseout="this.style.background='white'">✏️ <?= tr('bud_update_btn') ?></button>
<?php endif; ?>
</div>
<div style="padding:15px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0;">
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;">Solde théorique fin de mois</div>
<div style="font-size:0.85rem; color:#64748b; margin-bottom:4px;"><?= tr('bud_theoretical_balance') ?></div>
<div style="font-size:1.4rem; font-weight:700; color:<?= $solde_theorique < 0 ? '#ef4444' : '#334155' ?>;">
<?= number_format($solde_theorique, 2, ',', ' ') ?> €
</div>
@@ -413,7 +406,7 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div style="padding:15px; background:#fef2f2; border-radius:12px; border:1px solid #fecaca; position: relative;">
<div style="font-size:0.85rem; color:#991b1b; margin-bottom:4px; display:flex; justify-content:space-between; align-items:center;">
<span>Charges à venir</span>
<span><?= tr('bud_upcoming_charges') ?></span>
<button onclick="toggleDiv('pendingDetailsList')" style="background:none; border:none; cursor:pointer; font-size:1rem; padding:0; filter: grayscale(1); opacity: 0.7; transition: 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7">👁️</button>
</div>
<div style="font-size:1.4rem; font-weight:700; color:#b91c1c;">
@@ -421,7 +414,7 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div id="pendingDetailsList" style="display:none; margin-top: 12px; padding-top: 12px; border-top: 1px dashed #fca5a5; font-size: 0.8rem; color: #7f1d1d;">
<?php if (empty($pending_charges)): ?>
<div style="text-align:center; font-style:italic; opacity:0.8;">Tout est payé !</div>
<div style="text-align:center; font-style:italic; opacity:0.8;"><?= tr('bud_all_paid') ?></div>
<?php else: ?>
<?php foreach($pending_charges as $pc): ?>
<div style="display:flex; justify-content:space-between; margin-bottom:6px;">
@@ -438,12 +431,12 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div style="position: absolute; top: 0px; left: 10px; font-size: 0.8rem; color: #64748b; font-weight: 700;">0 €</div>
<div style="position: absolute; top: 0px; right: 10px; font-size: 0.8rem; color: #64748b; font-weight: 700; text-align:right; cursor:help;"
title="Calcul : Report Mois Précédent (<?= number_format($solde_initial, 0) ?>€) + Salaires (<?= number_format($salaires_retenus, 0) ?>€) + Autres rentrées (<?= number_format($rentrees_autres, 0) ?>€)">
Capacité Max : <?= number_format($capacite_max, 0, ',', ' ') ?> €
title="<?= sprintf(tr('bud_capacity_tooltip'), number_format($solde_initial, 0), number_format($salaires_retenus, 0), number_format($rentrees_autres, 0)) ?>">
<?= tr('bud_max_capacity') ?> : <?= number_format($capacite_max, 0, ',', ' ') ?> €
</div>
<div style="position: absolute; top: 12px; left: <?= $pct_actuel ?>%; transform: translateX(-50%); text-align: center; z-index: 10;">
<div style="color: #334155; font-size: 0.75rem; font-weight: 800; white-space: nowrap; margin-bottom: 2px;">Actuel</div>
<div style="color: #334155; font-size: 0.75rem; font-weight: 800; white-space: nowrap; margin-bottom: 2px;"><?= tr('bud_actual') ?></div>
<div style="width: 3px; height: 18px; background: #334155; margin: 0 auto; border-radius: 2px;"></div>
</div>
<div style="height: 24px; background: #e2e8f0; border-radius: 12px; display: flex; overflow: hidden; position: relative; box-shadow: inset 0 2px 4px rgba(0,0,0,0.05);">
@@ -452,37 +445,37 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div style="position: absolute; top: 54px; left: <?= $pct_theorique ?>%; transform: translateX(-50%); text-align: center; z-index: 10;">
<div style="width: 3px; height: 18px; background: #8b5cf6; margin: 0 auto 2px auto; border-radius: 2px;"></div>
<div style="color: #8b5cf6; font-size: 0.75rem; font-weight: 800; white-space: nowrap;">Théorique</div>
<div style="color: #8b5cf6; font-size: 0.75rem; font-weight: 800; white-space: nowrap;"><?= tr('bud_theoretical') ?></div>
</div>
</div>
</div>
<?php if (!$isClosed): ?>
<div style="display:flex; gap:10px; margin-bottom:20px;">
<button class="pf-btn btn-secondary" onclick="toggleDiv('importCsvForm')">📂 Importer CSV</button>
<button class="pf-btn btn-secondary" onclick="toggleDiv('importCsvForm')">📂 <?= tr('bud_import_csv') ?></button>
</div>
<div id="importCsvForm" style="display:<?= $showPreview ? 'block' : 'none' ?>; background:#f8fafc; padding:20px; border-radius:12px; margin-bottom:20px;">
<?php if (!$showPreview): ?>
<form method="POST" enctype="multipart/form-data" style="display:flex; gap:10px; align-items:end; flex-wrap:wrap;">
<div style="flex:1; min-width:250px;">
<label class="pf-label">Fichier CSV (Point-virgule)</label>
<label class="pf-label"><?= tr('bud_csv_file') ?></label>
<input type="file" name="csv_file" accept=".csv" class="pf-input">
</div>
<button type="submit" class="pf-btn" style="width:auto;">Prévisualiser</button>
<button type="submit" class="pf-btn" style="width:auto;"><?= tr('bud_preview') ?></button>
</form>
<?php else: ?>
<form method="POST" id="formMapping">
<input type="hidden" name="action" value="save_import">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; flex-wrap:wrap; gap:10px;">
<h3 style="margin:0;">Valider l'importation</h3>
<h3 style="margin:0;"><?= tr('bud_validate_import') ?></h3>
<span id="missingCount" style="color:#ef4444; background:#fee2e2; padding:4px 10px; border-radius:12px; font-weight:bold; font-size:0.85rem; display:none;"></span>
</div>
<div style="max-height:350px; overflow-y:auto; background:white; border:1px solid #eee; border-radius:8px;">
<table class="pf-table" style="margin:0;">
<thead style="position:sticky; top:0; z-index:10;">
<tr><th><input type="checkbox" onclick="toggleAll(this)" checked></th><th>Imputer au mois</th><th>Date Opération</th><th>Libellé</th><th>Montant</th><th>Catégorie</th></tr>
<tr><th><input type="checkbox" onclick="toggleAll(this)" checked></th><th><?= tr('bud_assign_month') ?></th><th><?= tr('bud_op_date') ?></th><th><?= tr('bud_label') ?></th><th><?= tr('bud_amount') ?></th><th><?= tr('bud_category') ?></th></tr>
</thead>
<tbody>
<?php foreach ($csvData as $idx => $row):
@@ -507,17 +500,17 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<td>
<div style="display:flex; gap:5px;">
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="flex:1;">
<option value="">-- <?= $isCrd ? 'Ignorer' : 'À définir' ?> --</option>
<option value="">-- <?= $isCrd ? tr('bud_ignore') : tr('bud_to_define') ?> --</option>
<?php foreach ($categoriesConfig as $k => $c): ?>
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= $c['label'] ?></option>
<?php endforeach; ?>
</select>
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; flex:1;" disabled>
<option value="">-- Charge ? --</option>
<option value="">-- <?= tr('bud_is_charge') ?> --</option>
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
</select>
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-income" onchange="checkValidation()" style="display:none; flex:1;" disabled>
<option value="">-- Revenu ? --</option>
<option value="">-- <?= tr('bud_is_income') ?> --</option>
<?php foreach ($incomeList as $inc): ?><option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option><?php endforeach; ?>
</select>
</div>
@@ -528,8 +521,8 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</table>
</div>
<div style="margin-top:15px; display:flex; justify-content:flex-end; gap:10px;">
<a href="?tab=suivi" class="pf-btn btn-secondary" style="width:auto;">Annuler</a>
<button type="submit" id="btnImport" class="pf-btn" style="width:auto;">Importer</button>
<a href="?tab=suivi" class="pf-btn btn-secondary" style="width:auto;"><?= tr('btn_cancel') ?></a>
<button type="submit" id="btnImport" class="pf-btn" style="width:auto;"><?= tr('btn_import') ?></button>
</div>
</form>
<?php endif; ?>
@@ -558,7 +551,7 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div style="flex:1; max-height:300px; overflow-y:auto; padding:0;">
<?php if (empty($expensesByCategory[$key])): ?>
<div style="padding:20px; text-align:center; color:#cbd5e1; font-style:italic; font-size:0.85rem;">Aucune ligne.</div>
<div style="padding:20px; text-align:center; color:#cbd5e1; font-style:italic; font-size:0.85rem;"><?= tr('bud_no_lines') ?></div>
<?php else: ?>
<table style="width:100%; border-collapse:collapse; font-size:0.85rem;">
<?php foreach ($expensesByCategory[$key] as $exp): ?>
@@ -572,8 +565,8 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</td>
<?php if (!$isClosed): ?>
<td style="width:60px; padding-right:10px; text-align:right; white-space:nowrap;">
<button onclick='openEditModal(<?= json_encode($exp) ?>)' style="background:none; border:none; cursor:pointer; font-size:1rem; margin-right:8px;">✏️</button>
<a href="?tab=suivi&delete_expense=<?= $exp['id'] ?>" onclick="return confirm('Supprimer ?')" style="color:#ef4444; text-decoration:none; font-size:1.4rem;">&times;</a>
<button onclick='openEditModal(<?= json_encode($exp, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)' style="background:none; border:none; cursor:pointer; font-size:1rem; margin-right:8px;">✏️</button>
<a href="?tab=suivi&delete_expense=<?= $exp['id'] ?>" onclick="return confirm('<?= tr('bud_confirm_delete') ?>')" style="color:#ef4444; text-decoration:none; font-size:1.4rem;">&times;</a>
</td>
<?php endif; ?>
</tr>
@@ -589,25 +582,25 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div id="manualExpenseModal" class="pf-modal">
<div class="pf-modal-content" style="max-width:400px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 style="margin:0;" id="modalTitle">Nouvelle transaction</h3>
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" style="border:none; background:none; font-size:1.8rem; cursor:pointer;">&times;</button>
<h3 style="margin:0;" id="modalTitle"><?= tr('bud_new_transaction') ?></h3>
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" class="pf-modal-close">&times;</button>
</div>
<form method="POST">
<input type="hidden" name="action" value="save_expense_manual">
<input type="hidden" name="expense_id" id="modalExpenseId">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Imputer au mois de gestion :</label>
<label class="pf-label"><?= tr('bud_assign_to_month') ?> :</label>
<input type="month" name="gestion_month" id="modalGestionMonth" class="pf-input" style="font-weight:bold; color:#2563eb;" required>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Date réelle de l'opération</label>
<label class="pf-label"><?= tr('bud_real_date') ?></label>
<input type="date" name="date" id="modalDate" class="pf-input" required>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Catégorie</label>
<label class="pf-label"><?= tr('bud_category') ?></label>
<select name="category" id="modalCatSelect" class="pf-input" onchange="handleModalCatChange(this)">
<?php foreach($categoriesConfig as $key => $conf): ?>
<option value="<?= $key ?>"><?= $conf['label'] ?></option>
@@ -616,21 +609,21 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Type</label>
<label class="pf-label"><?= tr('bud_type') ?></label>
<select name="is_credit" id="modalIsCredit" class="pf-input">
<option value="0">Dépense (-)</option>
<option value="1">Revenu / Remboursement (+)</option>
<option value="0"><?= tr('bud_type_expense') ?> (-)</option>
<option value="1"><?= tr('bud_type_income') ?> (+)</option>
</select>
</div>
<div class="form-group" id="blockInputText" style="margin-bottom:15px;">
<label class="pf-label">Libellé</label>
<label class="pf-label"><?= tr('bud_label') ?></label>
<input type="text" name="label" class="pf-input" id="modalLabelInput" list="modalSuggestions" autocomplete="off">
<datalist id="modalSuggestions"></datalist>
</div>
<div class="form-group" id="blockInputSelect" style="margin-bottom:15px; display:none;">
<label class="pf-label">Bénéficiaire</label>
<label class="pf-label"><?= tr('bud_beneficiary') ?></label>
<select name="label_select" id="schoolSelect" class="pf-input">
<option value="Ecole Pol">Ecole Pol</option>
<option value="Carole">Carole</option>
@@ -638,7 +631,7 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;">
<label class="pf-label">Charge Fixe</label>
<label class="pf-label"><?= tr('bud_fixed_charge') ?></label>
<select name="budget_item_id" id="fraisSelect" class="pf-input" disabled>
<option value="">-- Sélectionner --</option>
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
@@ -646,7 +639,7 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div class="form-group" id="blockInputIncome" style="margin-bottom:15px; display:none;">
<label class="pf-label">Revenu Attendu</label>
<label class="pf-label"><?= tr('bud_expected_income') ?></label>
<select name="budget_item_id" id="incomeSelect" class="pf-input" disabled>
<option value="">-- Sélectionner --</option>
<?php foreach ($incomeList as $inc): ?><option value="<?= $inc['id'] ?>"><?= htmlspecialchars($inc['name']) ?></option><?php endforeach; ?>
@@ -654,13 +647,13 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Montant (€)</label>
<label class="pf-label"><?= tr('bud_amount_eur') ?></label>
<input type="number" step="0.01" name="amount" id="modalAmount" class="pf-input" placeholder="0.00" required>
</div>
<div style="margin-top:20px; display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" class="pf-btn btn-secondary" style="width:auto;">Annuler</button>
<button type="submit" class="pf-btn" style="width:auto;">Enregistrer</button>
<div class="modal-footer">
<button type="button" onclick="closeSuiviModal('manualExpenseModal')" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
@@ -668,14 +661,14 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<div id="snapshotModal" class="pf-modal">
<div class="pf-modal-content" style="max-width:350px;">
<h3 style="margin-top:0;">MàJ Solde Bancaire</h3>
<h3 style="margin-top:0;"><?= tr('bud_update_balance') ?></h3>
<form method="POST">
<input type="hidden" name="action" value="save_snapshot">
<input type="date" name="snapshot_date" class="pf-input" value="<?= date('Y-m-d') ?>" style="margin-bottom:10px;" required>
<input type="number" step="0.01" name="snapshot_amount" class="pf-input" placeholder="Solde (€)" style="margin-bottom:15px;" required>
<div style="display:flex; justify-content:flex-end; gap:10px;">
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn btn-secondary" style="width:auto;">Annuler</button>
<button type="submit" class="pf-btn" style="width:auto;">Sauvegarder</button>
<input type="number" step="0.01" name="snapshot_amount" class="pf-input" placeholder="<?= tr('bud_balance_eur') ?>" style="margin-bottom:15px;" required>
<div class="modal-footer">
<button type="button" onclick="closeSuiviModal('snapshotModal')" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
@@ -684,14 +677,13 @@ $monthName = $moisFr[(int)$viewM] . ' ' . $viewY;
<script>
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
function openSuiviModal(id) { document.getElementById(id).style.display = 'flex'; }
function closeSuiviModal(id) { document.getElementById(id).style.display = 'none'; }
function openSuiviModal(id) { document.getElementById(id).style.display = 'flex'; document.body.classList.add('no-scroll'); }
function closeSuiviModal(id) { document.getElementById(id).style.display = 'none'; document.body.classList.remove('no-scroll');}
const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>;
function handleModalCatChange(select) {
const catKey = select.value;
const catText = select.options[select.selectedIndex].text.toLowerCase();
document.getElementById('blockInputText').style.display = 'none';
document.getElementById('blockInputSelect').style.display = 'none';
@@ -716,10 +708,10 @@ function handleModalCatChange(select) {
function openAddModal(catKey, catLabel) {
openSuiviModal('manualExpenseModal');
document.getElementById('modalTitle').innerText = "Ajouter : " + catLabel;
document.getElementById('modalTitle').innerText = "<?= tr('bud_add_title') ?> : " + catLabel;
document.getElementById('modalExpenseId').value = "";
document.getElementById('modalDate').value = new Date().toISOString().split('T')[0];
document.getElementById('modalGestionMonth').value = activeViewMonth; // Force le mois de gestion actuel
document.getElementById('modalGestionMonth').value = activeViewMonth;
document.getElementById('modalLabelInput').value = "";
document.getElementById('modalAmount').value = "";
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey; handleModalCatChange(catSelect);
@@ -728,7 +720,7 @@ function openAddModal(catKey, catLabel) {
function openEditModal(e) {
openSuiviModal('manualExpenseModal');
document.getElementById('modalTitle').innerText = "Modifier la transaction";
document.getElementById('modalTitle').innerText = "<?= tr('bud_edit_title') ?>";
document.getElementById('modalExpenseId').value = e.id;
document.getElementById('modalDate').value = e.date_exp;
document.getElementById('modalGestionMonth').value = e.gestion_month ? e.gestion_month.substring(0, 7) : activeViewMonth;
@@ -748,7 +740,9 @@ function handleLineCatChange(select) {
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; }
checkValidation();
}
function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disabled])').forEach(c => c.checked = src.checked); checkValidation(); }
function checkValidation() {
let miss = 0;
document.querySelectorAll('.line-checkbox:checked').forEach(cb => {
@@ -757,8 +751,9 @@ function checkValidation() {
if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = '';
});
const btn = document.getElementById('btnImport'); const msg = document.getElementById('missingCount');
if(miss>0) { btn.disabled = true; btn.style.opacity=0.5; msg.style.display='inline'; msg.innerText=miss+' à définir'; }
if(miss>0) { btn.disabled = true; btn.style.opacity=0.5; msg.style.display='inline'; msg.innerText = miss + ' <?= tr('bud_to_define_js') ?>'; }
else { btn.disabled = false; btn.style.opacity=1; msg.style.display='none'; }
}
if(document.getElementById('formMapping')) { document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s)); checkValidation(); }
</script>
+43 -39
View File
@@ -105,7 +105,7 @@ document.addEventListener("DOMContentLoaded", () => {
setupModalUI() {
const headerH2 = document.querySelector(".fc-modal-header h2");
if (headerH2 && !document.getElementById("holidayYearSelect")) {
headerH2.innerHTML = `Vacances Scolaires (Zone C)
headerH2.innerHTML = `${tr("fc_modal_holidays_title")}
<select id="holidayYearSelect" style="margin-left:15px; font-size:1rem; padding:4px; border-radius:4px; border:1px solid #cbd5e1;">
<option value="${this.currentSchoolYearStart - 1}">${this.currentSchoolYearStart - 1} - ${this.currentSchoolYearStart}</option>
<option value="${this.currentSchoolYearStart}" selected>${this.currentSchoolYearStart} - ${this.currentSchoolYearStart + 1}</option>
@@ -273,20 +273,20 @@ document.addEventListener("DOMContentLoaded", () => {
(1000 * 60 * 60 * 24),
) + 1;
let name = "Vacances scolaires";
let name = tr("leg_school_holidays");
if (m === 10 || m === 11) {
name = "Vacances de la Toussaint";
name = tr("vac_toussaint");
} else if (m === 12 || m === 1) {
name = "Vacances de Noël";
name = tr("vac_noel");
} else if (m === 2 || m === 3) {
name = "Vacances d'Hiver";
name = tr("vac_hiver");
} else if (m === 4 || (m === 5 && durationDays > 6)) {
name = "Vacances de Printemps";
name = tr("vac_printemps");
} else if (m === 5 && durationDays <= 6) {
name = "Pont de Mai (Ascension)";
name = tr("vac_ascension");
} else if (m === 7 || m === 8) {
name = "Vacances d'Été";
name = tr("vac_ete");
}
html += `
@@ -690,22 +690,23 @@ document.addEventListener("DOMContentLoaded", () => {
const y = this.currentMonth.getFullYear();
const m = this.currentMonth.getMonth();
const lang = window.I18N_LANG || "fr-FR"; // Assure-toi d'avoir cette variable ou utilise une déduction
const titleEl = document.querySelector("#fc-current-month-year");
if (titleEl) {
if (this.viewMode === "year") {
titleEl.textContent = `Année scolaire ${this.currentSchoolYearStart} ${this.currentSchoolYearStart + 1}`;
titleEl.textContent = `${tr("fc_school_year")} ${this.currentSchoolYearStart} ${this.currentSchoolYearStart + 1}`;
} else if (this.viewMode === "2months") {
const nextM = new Date(y, m + 1, 1);
const m1 = new Intl.DateTimeFormat("fr-FR", { month: "long" }).format(
const m1 = new Intl.DateTimeFormat(lang, { month: "long" }).format(
this.currentMonth,
);
const m2 = new Intl.DateTimeFormat("fr-FR", {
const m2 = new Intl.DateTimeFormat(lang, {
month: "long",
year: "numeric",
}).format(nextM);
titleEl.textContent = `${m1} - ${m2}`;
} else {
titleEl.textContent = new Intl.DateTimeFormat("fr-FR", {
titleEl.textContent = new Intl.DateTimeFormat(lang, {
month: "long",
year: "numeric",
}).format(this.currentMonth);
@@ -1261,54 +1262,56 @@ document.addEventListener("DOMContentLoaded", () => {
if (!menu) return;
this._currentBulkInfo = { dates };
// Gestion de la date affichée (Singulier/Pluriel)
const dateLabel =
dates.length > 1
? `${dates.length} jours`
: new Date(dates[0]).toLocaleDateString("fr-FR");
? `${dates.length} ${tr("fc_unit_days")}`
: new Date(dates[0]).toLocaleDateString(window.I18N_LANG || "fr-FR");
// Uniformisation : On définit l'icône SVG une seule fois
// Icône SVG Poubelle (Uniformisée)
const trashSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>`;
// En-tête principal avec la date
// Fonction utilitaire interne pour les en-têtes de section avec suppression
const buildHeader = (title, action, cat) => `
<div class="fc-menu-header">
<strong>${title}</strong>
${action ? `<button class="fc-menu-clear-icon" title="${tr("fc_clear")}" data-action="${action}" ${cat ? `data-cat="${cat}"` : ""}>${trashSvg}</button>` : ""}
</div>
`;
// 1. En-tête du menu (Date ou nombre de jours)
let html = `<div class="fc-menu-section" style="border-bottom: none; padding-bottom: 0;">
<strong style="font-size:0.85rem; color:var(--text-main); margin-bottom: 4px;">${dateLabel}</strong>
</div>`;
// Fonction utilitaire pour créer une ligne de titre avec l'icône SVG
const buildHeader = (title, action, cat) => `
<div class="fc-menu-header">
<strong>${title}</strong>
${action ? `<button class="fc-menu-clear-icon" title="Effacer" data-action="${action}" ${cat ? `data-cat="${cat}"` : ""}>${trashSvg}</button>` : ""}
</div>
`;
// Section Carole
// 2. Section Carole (Congés)
html += `<div class="fc-menu-section">
${buildHeader("Congés Carole", "clear-type", "CONGE")}
${buildHeader(tr("fc_menu_carole"), "clear-type", "CONGE")}
<div class="fc-menu-grid">
<button class="fc-menu-btn" data-action="add" data-type="OFF_CAROLE" data-person="Carole">Off</button>
<button class="fc-menu-btn" data-action="add" data-type="EXTRA_OFF_CAROLE" data-person="Carole">Extra</button>
<button class="fc-menu-btn" data-action="add" data-type="OFF_CAROLE" data-person="Carole">${tr("btn_off")}</button>
<button class="fc-menu-btn" data-action="add" data-type="EXTRA_OFF_CAROLE" data-person="Carole">${tr("btn_extra")}</button>
</div>
</div>`;
// Section Garde
// 3. Section Garde (Centre / Avis)
html += `<div class="fc-menu-section">
${buildHeader("Garde", "clear-type", "GARDE")}
${buildHeader(tr("leg_centre"), "clear-type", "GARDE")}
<div class="fc-menu-grid">
<button class="fc-menu-btn" data-action="add" data-type="CENTRE">Centre</button>
<button class="fc-menu-btn" data-action="add" data-type="AVIS">Avis</button>
<button class="fc-menu-btn" data-action="add" data-type="CENTRE">${tr("leg_centre")}</button>
<button class="fc-menu-btn" data-action="add" data-type="AVIS">${tr("leg_avis")}</button>
</div>
</div>`;
// Section Pep
// 4. Section Pep (Maladie)
html += `<div class="fc-menu-section">
${buildHeader("Pep", "clear-type", "PEP")}
<button class="fc-menu-btn" data-action="add" data-type="PEP_SICK" style="width:100%">Pep Malade 🤒</button>
<button class="fc-menu-btn" data-action="add" data-type="PEP_SICK" style="width:100%">${tr("leg_pep_sick")} 🤒</button>
</div>`;
// --- SECTION ALEX & LAIA AVEC SUPPRESSION INDÉPENDANTE, UNIFORMISÉE ET ALIGNÉE À CÔTÉ (INLINE) ---
// 5. Section Enfants (Alex & Laia)
html += `<div class="fc-menu-section">
${buildHeader("Congés Enfants", null, null)}
${buildHeader(tr("fc_menu_kids_leaves"), null, null)}
<div class="fc-menu-leaves-table">
<table>
<thead>
@@ -1316,13 +1319,13 @@ document.addEventListener("DOMContentLoaded", () => {
<th>
<div class="fc-th-inline">
Alex
<button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="2" title="Effacer Alex">${trashSvg}</button>
<button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="2" title="${tr("fc_clear")} Alex">${trashSvg}</button>
</div>
</th>
<th>
<div class="fc-th-inline">
Laia
<button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="3" title="Effacer Laia">${trashSvg}</button>
<button class="fc-menu-clear-icon fc-menu-btn-th" data-action="clear-leaves-person" data-pid="3" title="${tr("fc_clear")} Laia">${trashSvg}</button>
</div>
</th>
</tr>
@@ -1335,6 +1338,7 @@ document.addEventListener("DOMContentLoaded", () => {
<td><button class="fc-menu-btn" data-action="add-leave" data-pid="3" data-type="${t}">${t}</button></td>
</tr>`;
});
html += ` </tbody>
</table>
</div>
@@ -1342,7 +1346,7 @@ document.addEventListener("DOMContentLoaded", () => {
menu.innerHTML = html;
// Positionnement intelligent du menu
// Positionnement (Gestion des bords d'écran)
const menuWidth = 240;
let left = x + 10;
if (left + menuWidth > window.innerWidth) left = x - menuWidth - 10;
+8
View File
@@ -1188,3 +1188,11 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
color: #64748b;
font-weight: 700;
}
.highlight-step {
transition:
box-shadow 0.3s,
transform 0.3s;
box-shadow: 0 0 0 3px var(--primary);
transform: scale(1.02);
}
+31 -36
View File
@@ -32,10 +32,12 @@ function openHolidayModal(mode) {
document.getElementById("list_activity").innerHTML = "";
if (mode === "add") {
document.getElementById("modalTitle").innerText = tr("modal_plan_trip");
document.getElementById("modalTitle").innerText = tr("hdl_modal_title");
btnDelete.style.display = "none";
} else {
document.getElementById("modalTitle").innerText = tr("modal_quick_edit");
document.getElementById("modalTitle").innerText = tr(
"hdl_quick_edit_title",
);
btnDelete.style.display = "block";
}
@@ -120,23 +122,14 @@ function addItem(category, name = "", amount = "", isPaid = 0) {
div.innerHTML = `
<input type="hidden" name="items[cat][]" value="${category}">
<input type="text" name="items[name][]" class="pf-input"
placeholder="${tr("placeholder_title")}" value="${name}"
style="flex: 2; padding: 8px; font-size:0.9rem;" required>
<input type="number" step="0.01" name="items[amount][]" class="pf-input"
placeholder="${tr("placeholder_price")}" value="${amount}"
style="width: 80px; text-align: right; padding: 8px; font-size:0.9rem;">
<label title="${tr("already_paid")}" style="display: flex; align-items: center; cursor: pointer; padding: 0 5px;">
<input type="text" name="items[name][]" class="pf-input" placeholder="${tr("hdl_js_ph_expense_name")}" value="${name}" style="flex: 2; padding: 8px; font-size:0.9rem;" required>
<input type="number" step="0.01" name="items[amount][]" class="pf-input" placeholder="0.00" value="${amount}" style="width: 80px; text-align: right; padding: 8px; font-size:0.9rem;">
<label title="${tr("hdl_paid")}" style="display: flex; align-items: center; cursor: pointer; padding: 0 5px;">
<input type="checkbox" ${checkedAttr} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="margin:0;">
<input type="hidden" name="items[paid][]" value="${isPaid}">
<span style="font-size:0.75rem; margin-left:4px; font-weight:bold; color:#64748b;">${tr("paid")}</span>
<span style="font-size:0.75rem; margin-left:4px; font-weight:bold; color:#64748b;">${tr("hdl_paid")}</span>
</label>
<button type="button" onclick="this.parentElement.remove()" title="${tr("remove_line")}"
style="width: 28px; height: 28px; border: none; background: #fee2e2; color: #ef4444; border-radius: 6px; cursor: pointer; font-weight: bold; display:flex; align-items:center; justify-content:center;">
<button type="button" onclick="this.parentElement.remove()" title="${tr("btn_delete")}" style="width: 28px; height: 28px; border: none; background: #fee2e2; color: #ef4444; border-radius: 6px; cursor: pointer; font-weight: bold; display:flex; align-items:center; justify-content:center;">
&times;
</button>
`;
@@ -145,7 +138,7 @@ function addItem(category, name = "", amount = "", isPaid = 0) {
}
function deleteHoliday() {
if (!confirm(tr("confirm_delete_trip"))) return;
if (!confirm(tr("hdl_js_confirm_del_trip"))) return;
const form = document.getElementById("holidayForm");
const input = document.createElement("input");
input.type = "hidden";
@@ -161,6 +154,7 @@ let map = null;
function toggleMap() {
const modal = document.getElementById("hol-map-modal");
if (!modal) return;
if (modal.style.display === "flex") {
modal.style.display = "none";
} else {
@@ -246,7 +240,7 @@ function initDetailMap() {
marker.bindPopup(`
<div style="text-align:center;">
<div style="font-size:0.75rem; color:#64748b; margin-bottom:2px; font-weight:bold;">${tr("step_number")} ${index + 1}</div>
<div style="font-size:0.75rem; color:#64748b; margin-bottom:2px; font-weight:bold;">${tr("hdl_js_step_label")} ${index + 1}</div>
<strong style="font-size:1rem; color:#0f172a;">${pt.location_name}</strong><br>
<span style="font-weight:bold; color:${color};">${parseFloat(pt.total_amount).toFixed(2)} €</span>
</div>
@@ -358,7 +352,6 @@ function openCheckpointModal(mode, data = null) {
container.innerHTML = "";
// Nettoyage des champs de date et de la barre de recherche
if (document.getElementById("cp_start_date"))
document.getElementById("cp_start_date").value = "";
if (document.getElementById("cp_end_date"))
@@ -366,11 +359,10 @@ function openCheckpointModal(mode, data = null) {
document.getElementById("searchPlaceInput").value = "";
document.getElementById("searchResults").innerHTML = "";
// MODIFICATION : La barre de recherche (et les favoris) est TOUJOURS visible !
searchBlock.style.display = "block";
if (mode === "add") {
document.getElementById("cpModalTitle").innerText = tr("place_new_step");
document.getElementById("cpModalTitle").innerText = tr("hdl_btn_add_step");
formBlock.style.display = "none";
btnDel.style.display = "none";
document.getElementById("cp_old_sort_order").value = "";
@@ -378,7 +370,7 @@ function openCheckpointModal(mode, data = null) {
addCpExpenseLine();
document.getElementById("cp_is_return").checked = false;
} else if (mode === "edit" && data) {
document.getElementById("cpModalTitle").innerText = tr("edit_step");
document.getElementById("cpModalTitle").innerText = tr("hdl_js_edit_step");
formBlock.style.display = "block";
btnDel.style.display = "block";
@@ -422,7 +414,7 @@ function searchPlace() {
if (q.length < 3) return;
const resultsDiv = document.getElementById("searchResults");
resultsDiv.innerHTML = `<span style="color:#64748b; font-size:0.85rem;">${tr("search_in_progress")}</span>`;
resultsDiv.innerHTML = `<span style="color:#64748b; font-size:0.85rem;">${tr("hdl_js_search_loading")}</span>`;
fetch(
"/modules/holidays/includes/api/geocode.php?limit=5&q=" +
@@ -432,7 +424,7 @@ function searchPlace() {
.then((data) => {
resultsDiv.innerHTML = "";
if (data.error || !data.results || data.results.length === 0) {
resultsDiv.innerHTML = `<span style="color:#ef4444; font-size:0.85rem;">${tr("no_result_found")}</span>`;
resultsDiv.innerHTML = `<span style="color:#ef4444; font-size:0.85rem;">${tr("hdl_js_no_result")}</span>`;
return;
}
data.results.forEach((place) => {
@@ -449,7 +441,7 @@ function searchPlace() {
});
})
.catch((err) => {
resultsDiv.innerHTML = `<span style="color:#ef4444; font-size:0.85rem;">${tr("network_error")}</span>`;
resultsDiv.innerHTML = `<span style="color:#ef4444; font-size:0.85rem;">${tr("hdl_js_network_error")}</span>`;
});
}
@@ -478,23 +470,23 @@ function addCpExpenseLine(
div.innerHTML = `
<div class="hol-form-inner">
<select name="items[cat][]" class="pf-input hol-form-select" title="${tr("category")}">
<select name="items[cat][]" class="pf-input hol-form-select">
<option value="accommodation" ${category === "accommodation" ? "selected" : ""}>🏨</option>
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
</select>
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="${tr("placeholder_label")}" value="${name}">
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="${tr("hdl_js_ph_expense_name")}" value="${name}">
<input type="number" step="0.01" name="items[amount][]" class="pf-input hol-form-number" placeholder="0.00" value="${amount}">
<label class="hol-form-paid-label" title="${tr("already_paid")}">
<label class="hol-form-paid-label" title="${tr("hdl_paid")}">
<input type="checkbox" ${isChecked} onchange="this.nextElementSibling.value = this.checked ? 1 : 0">
<input type="hidden" name="items[paid][]" value="${isPaid}">
<span class="hol-form-paid-text">${tr("paid")}</span>
<span class="hol-form-paid-text">${tr("hdl_paid")}</span>
</label>
<button type="button" class="btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="${tr("btn_delete")}">&times;</button>
</div>
<div class="hol-form-subrow">
<input type="text" name="items[notes][]" class="pf-input hol-form-notes-input hol-form-notes-full" placeholder="${tr("placeholder_notes_link")}" value="${notes}">
<input type="text" name="items[notes][]" class="pf-input hol-form-notes-input hol-form-notes-full" placeholder="${tr("hdl_ph_notes")}" value="${notes}">
</div>
<input type="hidden" name="items[id][]" value="${itemId}">
<input type="hidden" name="items[date][]" value="${itemDate}">
@@ -505,7 +497,7 @@ function addCpExpenseLine(
}
function deleteCheckpoint() {
if (!confirm(tr("confirm_delete_step"))) return;
if (!confirm(tr("hdl_js_confirm_del_step"))) return;
const form = document.getElementById("formCheckpoint");
const input = document.createElement("input");
input.type = "hidden";
@@ -592,16 +584,21 @@ document.addEventListener("DOMContentLoaded", () => {
// ============================================================================
let selectedItemIdForMove = null;
function closePlanningModal() {
document.getElementById("planningModal").style.display = "none";
document.body.classList.remove("no-scroll");
}
function openPlanningModal(step) {
document.getElementById("planningModalTitle").innerText =
tr("planning_of") + step.location_name;
tr("hdl_planning_title") + " : " + step.location_name;
const container = document.getElementById("planningContainer");
selectedItemIdForMove = null;
let validItems = step.items.filter((it) => it.name !== "PF_TECHNICAL_POINT");
if (!step.step_start_date || !step.step_end_date) {
container.innerHTML = `<div style="text-align:center; padding:40px;"><h3>${tr("missing_dates_title")}</h3><p style="color:#64748b;">${tr("missing_dates_msg")}</p></div>`;
container.innerHTML = `<div style="text-align:center; padding:40px;"><h3>${tr("hdl_js_missing_dates_title")}</h3><p style="color:#64748b;">${tr("hdl_js_missing_dates_msg")}</p></div>`;
document.getElementById("planningModal").style.display = "flex";
return;
}
@@ -619,15 +616,13 @@ function openPlanningModal(step) {
<div class="hol-unmapped-zone" id="unmapped-pool"
ondragover="allowDrop(event)" ondrop="handleDropEvent(event, '', '')"
onclick="handleZoneTap(event, '', '')">
<div class="hol-unmapped-title" style="width:100%;">📥 ${tr("to_place")}</div>
<div class="hol-unmapped-title" style="width:100%;">📥 ${tr("hdl_to_place")}</div>
</div>
<div class="hol-calendar-zone">
`;
datesToDisplay.forEach((dateStr) => {
const dObj = new Date(dateStr);
// NOUVEAU : On utilise la langue actuelle du navigateur pour traduire les jours !
const dayName = dObj.toLocaleDateString(currentLang, { weekday: "short" });
const dayNum = dObj.toLocaleDateString(currentLang, {
day: "numeric",
+90 -105
View File
@@ -3,32 +3,34 @@
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id === 0) { echo "<div class='pf-section'><p><p>".tr('error')."</p></p></div>"; exit; }
if ($id === 0) {
echo "<div class='pf-section'><p>".tr('err_invalid_holiday_id')."</p></div>";
exit;
}
// Récupération des données du voyage
$stmt = $pdo->prepare("
SELECT h.*,
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
(
(SELECT COALESCE(SUM(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) +
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1)
) as total_paid,
((SELECT COALESCE(SUM(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) + (SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1)) as total_paid,
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
FROM pf_holidays h WHERE h.id = ?
");
$stmt->execute([$id]);
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
// Récupération des items (étapes et frais généraux)
$stmtItems = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ? ORDER BY sort_order ASC, id ASC");
$stmtItems->execute([$id]);
$items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
// Favoris pour le géocodage
$stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'");
$favorites = json_decode($stmtFav->fetchColumn() ?: '[]', true);
$steps = [];
$generalItems = [];
// C'est ici qu'il manquait sûrement une accolade !
foreach ($items as $it) {
if ($it['location_name'] !== null) {
$orderKey = $it['sort_order'];
@@ -54,6 +56,7 @@ foreach ($items as $it) {
ksort($steps);
$mapPoints = array_values($steps);
// Affichage de la date
$dateDisplay = htmlspecialchars($holiday['period_hint'] ?? '');
if (empty($dateDisplay) && $holiday['start_date']) {
$dateDisplay = date('d/m/Y', strtotime($holiday['start_date']));
@@ -78,7 +81,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<a href="?tab=list" class="pf-btn btn-secondary pf-btn-small" style="width: fit-content; text-decoration: none;"><?= tr('btn_back') ?></a>
<div style="display: flex; align-items: center; gap: 15px;">
<h1 style="margin: 0; font-size: 1.8rem;"><?= htmlspecialchars($holiday['title']) ?></h1>
<span class="hol-badge-status"><?= strtoupper($holiday['status']) ?></span>
<span class="hol-badge-status"><?= tr('hdl_status_' . $holiday['status']) ?></span>
</div>
</div>
@@ -92,57 +95,56 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
</div>
<div class="hol-summary-card" style="width: 100%; display: block; clear: both;">
<div class="hol-summary-card">
<div class="hol-summary-grid">
<div class="hol-summary-item">
<div class="hol-summary-label"><?= tr('period') ?></div>
<div class="hol-summary-value"><?= $dateDisplay ?: tr('to_be_defined') ?></div>
<div class="hol-summary-label"><?= tr('hdl_label_period') ?></div>
<div class="hol-summary-value"><?= $dateDisplay ?: tr('hdl_dates_to_define') ?></div>
</div>
<div class="hol-summary-item">
<div class="hol-summary-label"><?= tr('budget_food') ?></div>
<div class="hol-summary-label"><?= tr('hdl_label_budget_food_extras') ?></div>
<div class="hol-summary-value">🍔 <?= number_format($holiday['budget_food'], 0) ?> € | 🎁 <?= number_format($holiday['budget_extra'], 0) ?> €</div>
</div>
<div class="hol-summary-item">
<div class="hol-summary-label"><?= tr('total_cost') ?></div>
<div class="hol-summary-label"><?= tr('hdl_total_budget') ?></div>
<div class="hol-summary-value total"><?= number_format($cost, 0, ',', ' ') ?> €</div>
</div>
</div>
<div class="hol-progress-bar">
<div class="hol-progress-paid" style="width:<?= $pctPaid ?>%;" title="<?= tr('paid') ?>"></div>
<div class="hol-progress-saved" style="width:<?= $pctSaved ?>%;" title="<?= tr('funded') ?>"></div>
<div class="hol-progress-paid" style="width:<?= $pctPaid ?>%;" title="<?= tr('hdl_paid') ?>"></div>
<div class="hol-progress-saved" style="width:<?= $pctSaved ?>%;" title="<?= tr('hdl_saved') ?>"></div>
</div>
<div class="hol-progress-labels">
<span class="hol-label-paid">✓ <?= tr('paid') ?> : <?= number_format($paid, 0, ',', ' ') ?> €</span>
<span class="hol-label-saved">💼 <?= tr('provisioned') ?> : <?= number_format($saved, 0, ',', ' ') ?> €</span>
<span class="hol-label-left">⏳ <?= tr('left_to_pay') ?> : <?= number_format($leftToPay, 0, ',', ' ') ?> €</span>
<span class="hol-label-paid">✓ <?= tr('hdl_paid') ?> : <?= number_format($paid, 0, ',', ' ') ?> €</span>
<span class="hol-label-saved">💼 <?= tr('hdl_saved') ?> : <?= number_format($saved, 0, ',', ' ') ?> €</span>
<span class="hol-label-left">⏳ <?= tr('hdl_left_to_pay') ?> : <?= number_format($leftToPay, 0, ',', ' ') ?> €</span>
</div>
</div>
<div class="hol-layout-grid">
<div class="hol-panel">
<div class="hol-panel-header" style="flex-wrap: wrap; gap: 10px;">
<div class="hol-panel-header-group">
<h3 style="margin:0;"><?= tr('map_itinerary') ?></h3>
<h3 style="margin:0;"><?= tr('hdl_map_itinerary') ?></h3>
<div class="hol-map-legend">
<span class="hol-legend-item" title="<?= tr('outbound_trip') ?>"><span class="hol-legend-color aller"></span> <?= tr('outbound_trip') ?></span>
<span class="hol-legend-item" title="<?= tr('return_trip') ?>"><span class="hol-legend-color retour"></span> <?= tr('return_trip') ?></span>
<span class="hol-legend-item" title="<?= tr('hdl_outbound') ?>"><span class="hol-legend-color aller"></span> <?= tr('hdl_outbound') ?></span>
<span class="hol-legend-item" title="<?= tr('hdl_return') ?>"><span class="hol-legend-color retour"></span> <?= tr('hdl_return') ?></span>
</div>
</div>
<button class="pf-btn pf-btn-small" onclick="openCheckpointModal('add')"><?= tr('place_step') ?></button>
<button class="pf-btn pf-btn-small" onclick="openCheckpointModal('add')"><?= tr('hdl_btn_add_step') ?></button>
</div>
<div id="tripMap" style="flex:1; width:100%; background:#f1f5f9;"></div>
</div>
<div class="hol-panel">
<div class="hol-panel-header">
<h3 style="margin:0;"><?= tr('step_details') ?></h3>
<h3 style="margin:0;"><?= tr('hdl_step_details') ?></h3>
</div>
<div class="hol-panel-body">
<?php if (empty($steps)): ?>
<p style="color:var(--text-muted); font-style:italic; text-align:center; margin-top:40px;"><?= tr('no_steps') ?></p>
<p style="color:var(--text-muted); font-style:italic; text-align:center; margin-top:40px;"><?= tr('hdl_no_steps') ?></p>
<?php else: ?>
<?php if (!empty($generalItems) || $holiday['budget_food'] > 0 || $holiday['budget_extra'] > 0): ?>
@@ -150,7 +152,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<div class="hol-cp-header" style="background: transparent;">
<div class="hol-cp-info-group">
<span style="font-size:1.4rem;">🌍</span>
<div class="hol-cp-title">Frais généraux & Budgets</div>
<div class="hol-cp-title"><?= tr('hdl_general_costs') ?></div>
</div>
<?php
$generalTotal = $holiday['budget_food'] + $holiday['budget_extra'];
@@ -158,20 +160,20 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
?>
<div class="hol-cp-actions-group">
<div class="hol-cp-total"><?= number_format($generalTotal, 2, ',', ' ') ?> €</div>
<button onclick='editHoliday(JSON.parse(document.getElementById("holidayDataJson").textContent))' class="btn-icon-small" title="<?= tr('edit') ?>">⚙️</button>
<button onclick='editHoliday(JSON.parse(document.getElementById("holidayDataJson").textContent))' class="btn-icon-small" title="<?= tr('btn_edit') ?>">⚙️</button>
</div>
</div>
<div class="hol-cp-body">
<?php if ($holiday['budget_food'] > 0): ?>
<div class="hol-expense-wrapper"><div class="hol-expense-main">
<span class="hol-expense-name" style="color:#64748b;">🍔 Food & Bev</span>
<span class="hol-expense-name" style="color:#64748b;">🍔 <?= tr('hdl_food_bev') ?></span>
<span><strong class="hol-expense-amount"><?= number_format($holiday['budget_food'], 2, ',', ' ') ?> €</strong><span class="status-pending">⏳</span></span>
</div></div>
<?php endif; ?>
<?php if ($holiday['budget_extra'] > 0): ?>
<div class="hol-expense-wrapper"><div class="hol-expense-main">
<span class="hol-expense-name" style="color:#64748b;">🎁 Extras</span>
<span class="hol-expense-name" style="color:#64748b;">🎁 <?= tr('hdl_extras') ?></span>
<span><strong class="hol-expense-amount"><?= number_format($holiday['budget_extra'], 2, ',', ' ') ?> €</strong><span class="status-pending">⏳</span></span>
</div></div>
<?php endif; ?>
@@ -190,6 +192,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
</div>
<?php endif; ?>
<?php foreach ($steps as $step): ?>
<div id="step-card-<?= $step['sort_order'] ?>" class="hol-checkpoint hol-checkpoint-draggable" draggable="true" data-location="<?= htmlspecialchars($step['location_name']) ?>">
<div class="hol-cp-header">
@@ -198,19 +201,19 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<div class="hol-cp-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)" title="<?= htmlspecialchars($step['location_name']) ?>">
📍 <?= htmlspecialchars($step['location_name']) ?>
<?php if (!empty($step['is_return'])): ?>
<span style="background: #fff7ed; color: #ea580c; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #ffedd5; vertical-align: middle;">🏁 <?= tr('return_trip') ?></span>
<span style="background: #fff7ed; color: #ea580c; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #ffedd5; vertical-align: middle;">🏁 <?= tr('hdl_return') ?></span>
<?php endif; ?>
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
<div style="font-size:0.75rem; color:#64748b; font-weight:normal; margin-top:2px;">
<?= tr('from_date') ?> <?= date('d/m', strtotime($step['step_start_date'])) ?> <?= tr('to_date') ?> <?= date('d/m', strtotime($step['step_end_date'])) ?>
<?= tr('hdl_from') ?> <?= date('d/m', strtotime($step['step_start_date'])) ?> <?= tr('hdl_to') ?> <?= date('d/m', strtotime($step['step_end_date'])) ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="hol-cp-actions-group">
<div class="hol-cp-total"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</div>
<button onclick='openPlanningModal(<?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('view_planning') ?>">📅</button>
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('edit') ?>">✏️</button>
<button onclick='openPlanningModal(<?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('hdl_view_planning') ?>">📅</button>
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('btn_edit') ?>">✏️</button>
</div>
</div>
@@ -227,14 +230,14 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<span class="hol-expense-name"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
<span>
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>" title="<?= $it['is_paid'] ? tr('paid') : tr('to_pay') ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>" title="<?= $it['is_paid'] ? tr('hdl_paid') : tr('hdl_to_pay') ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
</span>
</div>
</div>
<?php endforeach; ?>
<?php if ($visibleItemsCount === 0): ?>
<div class="hol-empty-step">📍 <?= tr('passing_point') ?></div>
<div class="hol-empty-step">📍 <?= tr('hdl_passing_point') ?></div>
<?php endif; ?>
</div>
</div>
@@ -242,12 +245,14 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<?php endif; ?>
</div>
</div>
</div> <?php if (!empty($holiday['notes'])): ?>
</div>
<?php if (!empty($holiday['notes'])): ?>
<div class="hol-summary-card" style="margin-top: 24px; padding: 25px; border-left: 5px solid #f59e0b;">
<h3 style="margin: 0 0 15px 0; font-size: 1.2rem; color: #0f172a; display: flex; align-items: center; gap: 8px;">
<?= tr('travel_notes') ?>
📝 <?= tr('hdl_label_notes') ?>
</h3>
<div style="font-size: 0.95rem; color: #334155; white-space: pre-wrap; line-height: 1.6; font-family: 'Segoe UI', system-ui, sans-serif;">
<div style="font-size: 0.95rem; color: #334155; white-space: pre-wrap; line-height: 1.6;">
<?= htmlspecialchars($holiday['notes']) ?>
</div>
</div>
@@ -255,30 +260,30 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
<div id="checkpointModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 600px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 id="cpModalTitle" style="margin:0;">📍 <?= tr('place_step') ?></h3>
<button type="button" onclick="document.getElementById('checkpointModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.8rem; cursor:pointer;">&times;</button> </div>
<h3 id="cpModalTitle" style="margin:0;">📍 <?= tr('hdl_btn_add_step') ?></h3>
<button type="button" onclick="closeCheckpointModal()" class="pf-modal-close">&times;</button>
</div>
<div id="cpSearchBlock" style="margin-bottom:20px;">
<?php if (!empty($favorites)): ?>
<div style="margin-bottom:15px; display:flex; gap:8px; flex-wrap:wrap;">
<?php foreach($favorites as $fav): ?>
<button type="button" class="pf-btn btn-secondary" style="padding:4px 10px; font-size:0.8rem; height:auto; width:auto; border-radius:20px;" onclick="selectPlace(<?= $fav['lat'] ?>, <?= $fav['lng'] ?>, '<?= htmlspecialchars(addslashes($fav['name'])) ?>')">
<button type="button" class="pf-btn btn-secondary" style="padding:4px 10px; font-size:0.8rem; border-radius:20px;" onclick="selectPlace(<?= $fav['lat'] ?>, <?= $fav['lng'] ?>, '<?= htmlspecialchars(addslashes($fav['name'])) ?>')">
⭐ <?= htmlspecialchars($fav['name']) ?>
</button>
<?php endforeach; ?>
</div>
<?php endif; ?>
<label class="pf-label"><?= tr('search_location') ?></label>
<label class="pf-label"><?= tr('hdl_search_location') ?></label>
<div style="display:flex; gap:10px;">
<input type="text" id="searchPlaceInput" class="pf-input" placeholder="<?= tr('search_placeholder') ?>" onkeypress="if(event.key === 'Enter') { searchPlace(); return false; }">
<button type="button" class="pf-btn btn-secondary" onclick="searchPlace()" style="width:auto; margin:0;">🔍</button>
<input type="text" id="searchPlaceInput" class="pf-input" placeholder="<?= tr('hdl_ph_search') ?>" onkeypress="if(event.key === 'Enter') { searchPlace(); return false; }">
<button type="button" class="pf-btn btn-secondary" onclick="searchPlace()">🔍</button>
</div>
<div id="searchResults" style="margin-top:10px; max-height:200px; overflow-y:auto; display:flex; flex-direction:column; gap:5px;"></div>
<div id="searchResults" style="margin-top:10px; max-height:200px; overflow-y:auto;"></div>
</div>
<form action="/modules/holidays/includes/api/save_checkpoint.php" method="POST" id="formCheckpoint" style="display:none; border-top:1px solid #e2e8f0; padding-top:20px;">
@@ -289,49 +294,46 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<input type="hidden" name="old_sort_order" id="cp_old_sort_order">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label"><?= tr('step_name') ?></label>
<label class="pf-label"><?= tr('hdl_label_step_name') ?></label>
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:var(--primary);" required>
</div>
<div style="display:flex; gap:15px; margin-bottom:15px; background:#f8fafc; padding:12px; border-radius:8px; border:1px solid #e2e8f0;">
<div class="form-group" style="flex:1; margin:0;">
<label class="pf-label"><?= tr('arrival_optional') ?></label>
<div style="display:flex; gap:15px; margin-bottom:15px; background:#f8fafc; padding:12px; border-radius:8px;">
<div class="form-group" style="flex:1;">
<label class="pf-label"><?= tr('hdl_label_arrival') ?></label>
<input type="date" name="step_start_date" id="cp_start_date" class="pf-input">
</div>
<div class="form-group" style="flex:1; margin:0;">
<label class="pf-label"><?= tr('departure_optional') ?></label>
<div class="form-group" style="flex:1;">
<label class="pf-label"><?= tr('hdl_label_departure') ?></label>
<input type="date" name="step_end_date" id="cp_end_date" class="pf-input">
</div>
<div style="margin-bottom: 15px; padding-left: 5px;">
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.9rem; color:#d97706; font-weight:600;">
<input type="checkbox" name="is_return" id="cp_is_return" value="1" style="margin-right:8px; width:16px; height:16px;">
🏁 <?= tr('return_trip') ?>
</label>
</div>
<div style="margin-bottom: 15px;">
<label style="display:flex; align-items:center; cursor:pointer; color:#ea580c; font-weight:600;">
<input type="checkbox" name="is_return" id="cp_is_return" value="1" style="margin-right:8px;">
🏁 <?= tr('hdl_return') ?>
</label>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<label class="pf-label" style="margin:0;"><?= tr('planned_expenses') ?></label>
<button type="button" class="pf-btn btn-secondary" onclick="addCpExpenseLine()" style="padding:4px 8px; font-size:0.8rem; height:auto; width:auto; margin:0;"><?= tr('add_expense') ?></button>
<label class="pf-label" style="margin:0;"><?= tr('hdl_planned_expenses') ?></label>
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="addCpExpenseLine()"><?= tr('hdl_btn_add_expense') ?></button>
</div>
<div id="cpExpensesContainer" style="margin-bottom:15px; display:flex; flex-direction:column; gap:10px; max-height:350px; overflow-y:auto; overflow-x:hidden;">
</div>
<div id="cpExpensesContainer" style="margin-bottom:15px;"></div>
<div style="margin-bottom: 20px; padding-left: 5px;">
<div style="margin-bottom: 20px;">
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.85rem; color:#475569;">
<input type="checkbox" name="save_favorite" value="1" style="margin-right:8px;">
⭐ <?= tr('save_favorite') ?>
⭐ <?= tr('hdl_save_fav') ?>
</label>
</div>
<div style="display:flex; justify-content:space-between; align-items:center; border-top:1px solid #e2e8f0; padding-top:15px;">
<div>
<button type="button" onclick="deleteCheckpoint()" id="btnDeleteCp" class="pf-btn btn-secondary" style="color:#ef4444; border-color:#fca5a5; display:none; width:auto; margin:0;">🗑️ <?= tr('btn_delete') ?></button>
</div>
<div style="display:flex; gap:10px;">
<button type="button" onclick="document.getElementById('checkpointModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary" style="width:auto; margin:0;"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn" style="width:auto; margin:0;"><?= tr('btn_save') ?></button>
</div>
<div class="modal-footer" style="padding-top:15px; border-top:1px solid #e2e8f0;">
<button type="button" onclick="deleteCheckpoint()" id="btnDeleteCp" class="pf-btn btn-secondary" style="color:#ef4444; border-color:#fca5a5; display:none;"><?= tr('btn_delete') ?></button>
<button type="button" onclick="closeCheckpointModal()" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div>
</form>
</div>
@@ -340,46 +342,29 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<div id="planningModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 550px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 id="planningModalTitle" style="margin:0; color:var(--primary);">📅 <?= tr('planning_title') ?></h3>
<button type="button" onclick="document.getElementById('planningModal').style.display='none'; document.body.classList.remove('no-scroll');" style="border:none; background:none; font-size:1.8rem; cursor:pointer;">&times;</button> </div>
<div id="planningContainer" style="width: 100%;">
<div style="margin-top:20px; display:flex; justify-content:flex-end;">
<button type="button" onclick="document.getElementById('planningModal').style.display='none'; document.body.classList.remove('no-scroll');" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
<h3 id="planningModalTitle" style="margin:0; color:var(--primary);">📅 <?= tr('hdl_planning_title') ?></h3>
<button type="button" onclick="closePlanningModal()" class="pf-modal-close">&times;</button>
</div>
<div id="planningContainer" style="width: 100%;"></div>
<div class="modal-footer">
<button type="button" onclick="closePlanningModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
</div>
</div>
</div>
<?php include __DIR__ . '/modal.php'; ?>
<script>
const MAP_POINTS = <?= json_encode($mapPoints) ?>;
// Le pont i18n pour Javascript
window.I18N = <?= json_encode([
'modal_plan_trip' => tr('modal_plan_trip'),
'modal_quick_edit' => tr('modal_quick_edit'),
'err_modal_missing' => tr('err_modal_missing'),
'placeholder_title' => tr('placeholder_title'),
'placeholder_price' => tr('placeholder_price'),
'already_paid' => tr('already_paid'),
'paid' => tr('paid'), // Clé déjà existante
'remove_line' => tr('remove_line'),
'confirm_delete_trip' => tr('confirm_delete_trip'),
'step_number' => tr('step_number'),
'place_new_step' => tr('place_new_step'),
'edit_step' => tr('edit_step'),
'search_in_progress' => tr('search_in_progress'),
'no_result_found' => tr('no_result_found'),
'network_error' => tr('network_error'),
'placeholder_label' => tr('placeholder_label'),
'btn_delete' => tr('btn_delete'), // Clé déjà existante
'placeholder_notes_link' => tr('placeholder_notes_link'),
'confirm_delete_step' => tr('confirm_delete_step'),
'planning_of' => tr('planning_of'),
'missing_dates_title' => tr('missing_dates_title'),
'missing_dates_msg' => tr('missing_dates_msg'),
'to_place' => tr('to_place')
]) ?>;
// Le pont i18n pour Javascript (Holidays specific)
window.I18N = {
...window.I18N,
'hdl_js_search_loading': "<?= tr('hdl_js_search_loading') ?>",
'hdl_js_no_result': "<?= tr('hdl_js_no_result') ?>",
'hdl_js_confirm_del_trip': "<?= tr('hdl_js_confirm_del_trip') ?>",
'hdl_js_confirm_del_step': "<?= tr('hdl_js_confirm_del_step') ?>",
'hdl_js_step_label': "<?= tr('hdl_js_step_label') ?>",
'hdl_js_ph_expense_name': "<?= tr('hdl_js_ph_expense_name') ?>",
'hdl_js_ph_amount': "0.00",
'hdl_js_delete_line': "<?= tr('btn_delete') ?>"
};
</script>
+27 -51
View File
@@ -4,19 +4,16 @@
// 1. Gestion du filtrage par année
$selectedYear = $_GET['y'] ?? date('Y');
// Récupérer les années disponibles dans la BDD pour le menu déroulant
$yearStmt = $pdo->query("SELECT DISTINCT YEAR(start_date) as yr FROM pf_holidays WHERE start_date IS NOT NULL ORDER BY yr DESC");
$availableYears = $yearStmt->fetchAll(PDO::FETCH_COLUMN);
if (!in_array(date('Y'), $availableYears)) {
$availableYears[] = date('Y');
rsort($availableYears); // Trie par ordre décroissant
rsort($availableYears);
}
// Construction de la condition de filtrage SQL
$whereSQL = "";
$params = [];
if ($selectedYear !== 'all') {
// On affiche les voyages de l'année OU les voyages sans date (pour ne pas perdre les brouillons)
$whereSQL = "WHERE YEAR(h.start_date) = ? OR h.start_date IS NULL";
$params[] = $selectedYear;
}
@@ -24,69 +21,53 @@ if ($selectedYear !== 'all') {
// 2. Récupération des voyages + Calculs
$sql = "
SELECT h.*,
(
COALESCE(h.budget_food, 0) +
COALESCE(h.budget_extra, 0) +
COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)
) as total_cost,
(
(SELECT COALESCE(SUM(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) +
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1)
) as total_paid,
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
((SELECT COALESCE(SUM(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) + (SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1)) as total_paid,
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
FROM pf_holidays h
$whereSQL
ORDER BY
COALESCE(start_date, '2999-12-31') ASC,
FIELD(status, 'booked', 'planned', 'draft', 'passed', 'archived')
ORDER BY COALESCE(start_date, '2999-12-31') ASC, FIELD(status, 'booked', 'planned', 'draft', 'passed', 'archived')
";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$holidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 3. Tri par statut
$active = array_filter($holidays, fn($h) => in_array($h['status'], ['draft', 'planned', 'booked']));
$history = array_filter($holidays, fn($h) => in_array($h['status'], ['passed', 'archived']));
// 4. Calcul du Reste à Payer Global (uniquement sur les voyages Actifs affichés)
$globalLeftToPay = 0;
foreach ($active as $h) {
$cost = (float)$h['total_cost'];
$paid = (float)$h['total_paid'];
$globalLeftToPay += max(0, $cost - $paid);
$globalLeftToPay += max(0, (float)$h['total_cost'] - (float)$h['total_paid']);
}
?>
<div class="pf-holidays">
<div class="pf-holidays__titlebar" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:15px; margin-bottom:30px;">
<div style="display:flex; align-items:center; gap:15px; flex-wrap:wrap;">
<h1 style="margin:0;">Mes Vacances ✈️</h1>
<h1 style="margin:0;"><?= tr('hdl_main_title') ?> ✈️</h1>
<select onchange="window.location.href='?tab=list&y='+this.value" class="pf-input" style="width:auto; padding:6px 12px; font-weight:bold; cursor:pointer; border-radius:8px; border:1px solid #cbd5e1; margin:0;">
<option value="all" <?= $selectedYear === 'all' ? 'selected' : '' ?>>Tout afficher</option>
<select onchange="window.location.href='?tab=list&y='+this.value" class="pf-input" style="width:auto; padding:6px 12px; font-weight:bold; cursor:pointer;">
<option value="all" <?= $selectedYear === 'all' ? 'selected' : '' ?>><?= tr('hdl_filter_all') ?></option>
<?php foreach($availableYears as $yr): ?>
<option value="<?= $yr ?>" <?= (string)$selectedYear === (string)$yr ? 'selected' : '' ?>>Année <?= $yr ?></option>
<option value="<?= $yr ?>" <?= (string)$selectedYear === (string)$yr ? 'selected' : '' ?>><?= tr('hdl_filter_year') ?> <?= $yr ?></option>
<?php endforeach; ?>
</select>
<div style="background:#fff1f2; color:#be123c; padding:6px 12px; border-radius:8px; font-weight:bold; border:1px solid #fecdd3; font-size:0.9rem; display:flex; align-items:center; gap:8px;">
<span>⏳ Reste à payer (<?= $selectedYear === 'all' ? 'Global' : $selectedYear ?>) :</span>
<span>⏳ <?= tr('hdl_left_to_pay_label') ?> (<?= $selectedYear === 'all' ? tr('hdl_filter_all') : $selectedYear ?>) :</span>
<span style="font-size:1rem;"><?= number_format($globalLeftToPay, 0, ',', ' ') ?> €</span>
</div>
</div>
<div>
<button class="pf-btn" onclick="openHolidayModal('add')" style="margin:0;">+ Créer un voyage</button>
<button class="pf-btn" onclick="openHolidayModal('add')" style="margin:0;">+ <?= tr('hdl_btn_create') ?></button>
</div>
</div>
<section class="pf-section">
<div class="hol-ideas-grid">
<?php if (empty($active)): ?>
<p style="color:var(--text-muted); font-style:italic;">Aucun voyage en cours pour cette période. Planifions quelque chose !</p>
<p style="color:var(--text-muted); font-style:italic;"><?= tr('hdl_no_active_trips') ?></p>
<?php endif; ?>
<?php foreach ($active as $h): ?>
@@ -97,7 +78,7 @@ foreach ($active as $h) {
<?php if (!empty($history)): ?>
<section class="pf-section" style="margin-top: 40px; border-top: 1px solid #e2e8f0; padding-top: 20px;">
<h3 style="color:var(--text-muted);">Historique</h3>
<h3 style="color:var(--text-muted);"><?= tr('hdl_history_title') ?></h3>
<div class="hol-ideas-grid" style="opacity: 0.7;">
<?php foreach ($history as $h): ?>
<?php renderHolidayCard($h, $pdo); ?>
@@ -105,13 +86,11 @@ foreach ($active as $h) {
</div>
</section>
<?php endif; ?>
</div>
<?php include __DIR__ . '/modal.php'; ?>
<?php
// FONCTION D'AFFICHAGE DE LA CARTE (Ne pas modifier, elle est déjà optimale)
function renderHolidayCard($h, $pdo) {
$stmt = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ?");
$stmt->execute([$h['id']]);
@@ -135,58 +114,55 @@ function renderHolidayCard($h, $pdo) {
$cost = (float)$h['total_cost'];
$paid = (float)$h['total_paid'];
$saved = (float)$h['total_saved'];
$leftToPay = max(0, $cost - $paid);
$pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0;
$pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
// Traduction dynamique du statut
$statusLabel = tr('hdl_status_' . $h['status']);
echo "
<div class='hol-idea-card' style='display: flex; flex-direction: column;'>
<div class='hol-idea-card__head' style='display:flex; justify-content:space-between; align-items:flex-start; margin-bottom: 10px;'>
<div style='flex:1;'>
<h3 style='margin:0; font-size:1.15rem; color:#0f172a;'>
<a href='?tab=holiday_detail&id={$h['id']}' style='text-decoration:none; color:inherit;' title='Ouvrir la page du voyage'>
<a href='?tab=holiday_detail&id={$h['id']}' style='text-decoration:none; color:inherit;'>
".htmlspecialchars($h['title'])."
</a>
</h3>
<span style='font-size:0.7rem; padding:3px 8px; border-radius:12px; font-weight:bold; display:inline-block; margin-top:6px;' class='$statusClass'>
".strtoupper($h['status'])."
".strtoupper($statusLabel)."
</span>
</div>
<div style='display:flex; gap:5px;'>
<button onclick='editHoliday($json)' class='pf-btn btn-secondary' style='padding:6px; height:auto; width:auto; line-height:1;' title='Modification rapide'>✏️</button>
<a href='?tab=holiday_detail&id={$h['id']}' class='pf-btn' style='padding:6px; height:auto; width:auto; line-height:1; text-decoration:none;' title='Gérer le voyage'>👁️</a>
<button onclick='editHoliday($json)' class='pf-btn btn-secondary' style='padding:6px;'>✏️</button>
<a href='?tab=holiday_detail&id={$h['id']}' class='pf-btn' style='padding:6px; text-decoration:none;'>👁️</a>
</div>
</div>
<div class='hol-idea-meta' style='margin-bottom:15px;'>
<span>🗓️ ".($dateDisplay ?: 'Dates à définir')."</span>
<span>🗓️ ".($dateDisplay ?: tr('hdl_dates_to_define'))."</span>
</div>
<div style='margin-top:auto; padding-top:15px; border-top:1px solid #f1f5f9;'>
<div style='display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;'>
<span style='font-size:0.85rem; color:#64748b;'>Budget Total</span>
<span style='font-size:0.85rem; color:#64748b;'>".tr('hdl_total_budget')."</span>
<span style='font-size:1.1rem; font-weight:bold; color:#1e293b;'>".number_format($cost, 0, ',', ' ')." €</span>
</div>
<div style='width:100%; height:8px; background:#e2e8f0; border-radius:4px; margin-bottom:12px; display:flex; overflow:hidden;'>
<div style='width:{$pctPaid}%; background:#10b981; transition:width 0.3s ease;' title='Payé'></div>
<div style='width:{$pctSaved}%; background:#3b82f6; transition:width 0.3s ease;' title='Financé (Provision)'></div>
<div style='width:{$pctPaid}%; background:#10b981; transition:width 0.3s ease;'></div>
<div style='width:{$pctSaved}%; background:#3b82f6; transition:width 0.3s ease;'></div>
</div>
<div style='display:grid; grid-template-columns: 1fr 1fr; gap:8px; font-size:0.8rem;'>
<div style='color:#10b981; font-weight:600;' title='Montant déjà dépensé'>✓ Payé : ".number_format($paid, 0, ',', ' ')." €</div>
<div style='color:#3b82f6; font-weight:600; text-align:right;' title='Montant épargné non dépensé'>💼 Financé : ".number_format($saved, 0, ',', ' ')." €</div>
<div style='color:#10b981; font-weight:600;'>✓ ".tr('hdl_paid')." : ".number_format($paid, 0, ',', ' ')." €</div>
<div style='color:#3b82f6; font-weight:600; text-align:right;'>💼 ".tr('hdl_saved')." : ".number_format($saved, 0, ',', ' ')." €</div>
<div style='color:#ef4444; font-weight:700; font-size:0.85rem; grid-column: span 2; padding-top: 4px; border-top: 1px dashed #fca5a5;'>
Reste à payer : ".number_format($leftToPay, 0, ',', ' ')." €
".tr('hdl_left_to_pay')." : ".number_format($leftToPay, 0, ',', ' ')." €
</div>
</div>
</div>
</div>
";
+28 -28
View File
@@ -1,6 +1,6 @@
<div id="holidayModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 800px; width: 95%;">
<h3 id="modalTitle" class="pf-modal-title">Planifier le voyage</h3>
<h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3>
<form action="/modules/holidays/includes/api/save_holiday.php" method="POST" id="holidayForm">
<input type="hidden" name="id" id="inp_id">
@@ -8,34 +8,34 @@
<div class="form-row">
<div style="flex: 2;">
<label class="pf-label">Nom du voyage</label>
<input type="text" name="title" id="inp_title" class="pf-input" placeholder="Ex: Octobre - Portugal" required>
<label class="pf-label"><?= tr('hdl_label_name') ?></label>
<input type="text" name="title" id="inp_title" class="pf-input" placeholder="<?= tr('hdl_ph_name') ?>" required>
</div>
<div style="flex: 1;">
<label class="pf-label">Statut</label>
<label class="pf-label"><?= tr('hdl_label_status') ?></label>
<select name="status" id="inp_status" class="pf-input">
<option value="draft">Brouillon ✏️</option>
<option value="planned">Planifié 📅</option>
<option value="booked">Réservé </option>
<option value="passed">Passé 👋</option>
<option value="archived">Archivé 🗄️</option>
<option value="draft"><?= tr('hdl_status_draft') ?> ✏️</option>
<option value="planned"><?= tr('hdl_status_planned') ?> 📅</option>
<option value="booked"><?= tr('hdl_status_booked') ?> ✅</option>
<option value="passed"><?= tr('hdl_status_passed') ?> 👋</option>
<option value="archived"><?= tr('hdl_status_archived') ?> 🗄️</option>
</select>
</div>
</div>
<div class="form-row">
<div>
<label class="pf-label">Période (Texte libre)</label>
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="Ex: Octobre 2026">
<label class="pf-label"><?= tr('hdl_label_period') ?></label>
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="<?= tr('hdl_ph_period') ?>">
</div>
<div style="display:flex; gap:10px;">
<div style="flex:1">
<label class="pf-label">Du</label>
<input type="date" name="start_date" id="inp_start" class="pf-input" lang="fr">
<label class="pf-label"><?= tr('hdl_label_from') ?></label>
<input type="date" name="start_date" id="inp_start" class="pf-input">
</div>
<div style="flex:1">
<label class="pf-label">Au</label>
<input type="date" name="end_date" id="inp_end" class="pf-input" lang="fr">
<label class="pf-label"><?= tr('hdl_label_to') ?></label>
<input type="date" name="end_date" id="inp_end" class="pf-input">
</div>
</div>
</div>
@@ -45,24 +45,24 @@
<div class="hol-columns-wrapper">
<div class="hol-col">
<div class="hol-col-header">
<h4 style="color:#2563eb;">🚗 Transport</h4>
<button type="button" class="btn-add-item" onclick="addItem('transport')" title="Ajouter un transport"></button>
<h4 style="color:#2563eb;">🚗 <?= tr('hdl_cat_transport') ?></h4>
<button type="button" class="btn-add-item" onclick="addItem('transport')" title="<?= tr('hdl_add_transport') ?>"></button>
</div>
<div id="list_transport" class="dynamic-list"></div>
</div>
<div class="hol-col">
<div class="hol-col-header">
<h4 style="color:#059669;">🏨 Hébergement</h4>
<button type="button" class="btn-add-item" onclick="addItem('accommodation')" title="Ajouter un hébergement"></button>
<h4 style="color:#059669;">🏨 <?= tr('hdl_cat_accommodation') ?></h4>
<button type="button" class="btn-add-item" onclick="addItem('accommodation')" title="<?= tr('hdl_add_accommodation') ?>"></button>
</div>
<div id="list_accommodation" class="dynamic-list"></div>
</div>
<div class="hol-col">
<div class="hol-col-header">
<h4 style="color:#d97706;">🎫 Activité</h4>
<button type="button" class="btn-add-item" onclick="addItem('activity')" title="Ajouter une activité"></button>
<h4 style="color:#d97706;">🎫 <?= tr('hdl_cat_activity') ?></h4>
<button type="button" class="btn-add-item" onclick="addItem('activity')" title="<?= tr('hdl_add_activity') ?>"></button>
</div>
<div id="list_activity" class="dynamic-list"></div>
</div>
@@ -72,24 +72,24 @@
<div class="form-row">
<div>
<label class="pf-label">🍔 Budget Food & Bev ()</label>
<label class="pf-label">🍔 <?= tr('hdl_label_budget_food') ?></label>
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
</div>
<div>
<label class="pf-label">🎁 Budget Extras ()</label>
<label class="pf-label">🎁 <?= tr('hdl_label_budget_extras') ?></label>
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
</div>
</div>
<div class="form-group">
<label class="pf-label">Notes</label>
<textarea name="notes" id="inp_notes" class="pf-input" rows="2" placeholder="Idées en vrac..."></textarea>
<label class="pf-label"><?= tr('hdl_label_notes') ?></label>
<textarea name="notes" id="inp_notes" class="pf-input" rows="2" placeholder="<?= tr('hdl_ph_notes') ?>"></textarea>
</div>
<div class="modal-footer">
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary" style="color:#ef4444; border-color:#fca5a5; margin-right:auto; display:none;">Supprimer</button>
<button type="button" onclick="closeHolidayModal()" class="pf-btn btn-secondary">Annuler</button>
<button type="submit" class="pf-btn">Enregistrer</button>
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary" style="color:#ef4444; border-color:#fca5a5; margin-right:auto; display:none;"><?= tr('btn_delete') ?></button>
<button type="button" onclick="closeHolidayModal()" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
</div>
</form>
</div>