This commit is contained in:
2026-04-17 16:51:05 +02:00
parent 0db6a71fd7
commit fe39eb8f2c
12 changed files with 360 additions and 97 deletions
+23
View File
@@ -196,5 +196,28 @@ require __DIR__ . '/header.php';
</section> </section>
</div> </div>
<script>
window.I18N = {
...(window.I18N || {}),
'fc_menu_carole': "<?= tr('fc_menu_carole') ?>",
'btn_off': "<?= tr('btn_off') ?>",
'btn_extra': "<?= tr('btn_extra') ?>",
'leg_centre': "<?= tr('leg_centre') ?>",
'leg_avis': "<?= tr('leg_avis') ?>",
'leg_pep_sick': "<?= tr('leg_pep_sick') ?>",
'fc_menu_kids_leaves': "<?= tr('fc_menu_kids_leaves') ?>",
'fc_clear': "<?= tr('fc_clear') ?>",
'fc_unit_days': "<?= tr('fc_unit_days') ?>",
'vac_toussaint': "<?= tr('vac_toussaint') ?>",
'vac_noel': "<?= tr('vac_noel') ?>",
'vac_hiver': "<?= tr('vac_hiver') ?>",
'vac_printemps': "<?= tr('vac_printemps') ?>",
'vac_ascension': "<?= tr('vac_ascension') ?>",
'vac_ete': "<?= tr('vac_ete') ?>",
'leg_school_holidays': "<?= tr('leg_school_holidays') ?>",
'fc_school_year': "<?= tr('fc_school_year') ?>",
'fc_modal_holidays_title': "<?= tr('fc_modal_holidays_title') ?>"
};
</script>
<script src="/modules/family-calendar/family-calendar.js"></script> <script src="/modules/family-calendar/family-calendar.js"></script>
<?php require __DIR__ . '/footer.php'; ?> <?php require __DIR__ . '/footer.php'; ?>
+17
View File
@@ -206,6 +206,20 @@ return [
'hdl_js_missing_dates_title'=> 'Dates mancants', 'hdl_js_missing_dates_title'=> 'Dates mancants',
'hdl_js_missing_dates_msg' => 'Cal definir una data darribada i sortida per organitzar la planificació.', 'hdl_js_missing_dates_msg' => 'Cal definir una data darribada i sortida per organitzar la planificació.',
'hdl_to_place' => 'Per situar', 'hdl_to_place' => 'Per situar',
'hdl_from' => 'Des del',
'hdl_to' => 'Fins al',
'hdl_btn_add_step' => 'Afegir una etapa',
'hdl_quick_edit_title' => 'Modificació ràpida',
'hdl_paid' => 'Pagat',
'weather_forecast' => 'Previsions meteorològiques',
'weather_not_available' => 'Temps no disponible',
'temp_max' => 'Temp. Màxima',
'weather_loading' => 'Carregant el temps...',
'weather_error' => 'Temps no disponible',
'weather_sunny' => 'Assolellat',
'weather_cloudy' => 'Ennuvolat',
'weather_rainy' => 'Plujós',
'weather_snowy' => 'Nevat',
// Anciennes clés Holidays non préfixées (conservades) // Anciennes clés Holidays non préfixées (conservades)
'outbound_trip' => 'Anada', 'outbound_trip' => 'Anada',
@@ -373,6 +387,9 @@ return [
'bud_reg_none' => 'Cap', 'bud_reg_none' => 'Cap',
'bud_recap_confirm_delete' => 'Segur que vols suprimir aquest element?', 'bud_recap_confirm_delete' => 'Segur que vols suprimir aquest element?',
'bud_err_delete' => 'Error en suprimir.', 'bud_err_delete' => 'Error en suprimir.',
'bud_sav_modal_title_add' => 'Introduir un mes',
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
'bud_prev_label_name' => 'Nom',
// --- BUDGET : EPARGNE --- // --- BUDGET : EPARGNE ---
'bud_sav_add_month' => 'Introduir un mes', 'bud_sav_add_month' => 'Introduir un mes',
+17
View File
@@ -206,6 +206,20 @@ return [
'hdl_js_missing_dates_title'=> 'Dates manquantes', '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_js_missing_dates_msg' => 'Veuillez définir une date darrivée et de départ pour organiser le planning.',
'hdl_to_place' => 'À placer', 'hdl_to_place' => 'À placer',
'hdl_from' => 'Du',
'hdl_to' => 'Au',
'hdl_btn_add_step' => 'Ajouter une étape',
'hdl_quick_edit_title' => 'Modification rapide',
'hdl_paid' => 'Payé',
'weather_forecast' => 'Prévisions météo',
'weather_not_available' => 'Météo indisponible',
'temp_max' => 'Temp. Max',
'weather_loading' => 'Chargement météo...',
'weather_error' => 'Météo indisponible',
'weather_sunny' => 'Ensoleillé',
'weather_cloudy' => 'Nuageux',
'weather_rainy' => 'Pluvieux',
'weather_snowy' => 'Neigeux',
// Anciennes clés Holidays non préfixées (conservées pour compatibilité si utilisées) // Anciennes clés Holidays non préfixées (conservées pour compatibilité si utilisées)
'outbound_trip' => 'Aller', 'outbound_trip' => 'Aller',
@@ -438,6 +452,9 @@ return [
'bud_prev_err_no_history' => 'Impossible : pas d\'historique disponible pour copier.', '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_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.", 'bud_prev_confirm_transfers' => "Confirmer que %p a bien effectué tous ses virements pour %m ?\n\nCela mettra à jour l'Épargne automatiquement.",
'bud_sav_modal_title_add' => 'Saisir un mois',
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
'bud_prev_label_name' => 'Nom',
// ========================================== // ==========================================
// CADEAUX (GIFTS) // CADEAUX (GIFTS)
+18 -20
View File
@@ -571,19 +571,22 @@ body.sum-mode-active .sum-target {
<button onclick="toggleSumMode()" class="pf-sum-close" title="<?= tr('btn_close') ?>">&times;</button> <button onclick="toggleSumMode()" class="pf-sum-close" title="<?= tr('btn_close') ?>">&times;</button>
</div> </div>
<script> <script>
window.I18N = { // --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
...window.I18N, window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
window.I18N = {
...(window.I18N || {}),
'bud_prev_label_name': "<?= tr('bud_prev_label_name') ?>",
'bud_prev_err_no_history': "<?= tr('bud_prev_err_no_history') ?>", 'bud_prev_err_no_history': "<?= tr('bud_prev_err_no_history') ?>",
'bud_prev_confirm_copy': "<?= tr('bud_prev_confirm_copy') ?>", 'bud_prev_confirm_copy': "<?= tr('bud_prev_confirm_copy') ?>",
'bud_prev_confirm_transfers': "<?= tr('bud_prev_confirm_transfers') ?>", 'bud_prev_confirm_transfers': "<?= tr('bud_prev_confirm_transfers') ?>",
'bud_err_tech': "<?= tr('bud_err_tech') ?>" 'bud_err_tech': "<?= tr('bud_err_tech') ?>"
}; };
const currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR"; const currentYear = <?= $currentYear ?? 'new Date().getFullYear()' ?>;
</script> const months = <?= json_encode($months ?? []) ?>;
<script>
function openEditModal(btn) { function openEditModal(btn) {
const id = btn.getAttribute('data-id'); const id = btn.getAttribute('data-id');
const name = btn.getAttribute('data-name'); const name = btn.getAttribute('data-name');
@@ -599,9 +602,6 @@ function openEditModal(btn) {
document.body.classList.add('no-scroll'); document.body.classList.add('no-scroll');
} }
const currentYear = <?= $currentYear ?>;
const months = <?= json_encode($months) ?>;
function updateSalary(person, input) { function updateSalary(person, input) {
const row = input.closest('tr'); const row = input.closest('tr');
const salary = parseFloat(row.querySelector('[data-field="salary"]').value) || 0; const salary = parseFloat(row.querySelector('[data-field="salary"]').value) || 0;
@@ -611,7 +611,7 @@ function updateSalary(person, input) {
const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0; const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0;
const restant = salary - (mens + frais + ecoP + ecoF); const restant = salary - (mens + frais + ecoP + ecoF);
document.getElementById('restant_' + person).innerText = Math.round(restant).toLocaleString(currentLang) + ' €'; document.getElementById('restant_' + person).innerText = Math.round(restant).toLocaleString(window.appLang) + ' €';
saveData('update_salary_config', { year: currentYear, person: person, field: input.dataset.field, value: input.value }); saveData('update_salary_config', { year: currentYear, person: person, field: input.dataset.field, value: input.value });
recalcAllAllocations(); recalcAllAllocations();
@@ -632,7 +632,7 @@ function duplicateMonth() {
} }
const formatMonth = (d) => { const formatMonth = (d) => {
let str = new Date(d).toLocaleDateString(currentLang, { month: 'long', year: 'numeric' }); let str = new Date(d).toLocaleDateString(window.appLang, { month: 'long', year: 'numeric' });
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
}; };
@@ -716,7 +716,6 @@ function recalcAllAllocations() {
function updateSummaryTable() { function updateSummaryTable() {
const focusMonth = months[0]; const focusMonth = months[0];
const sums = {};
let grandTotalAlex = 0; let grandTotalAlex = 0;
let grandTotalLaia = 0; let grandTotalLaia = 0;
const dataByTarget = {}; const dataByTarget = {};
@@ -745,18 +744,18 @@ function updateSummaryTable() {
const laiaSum = dataByTarget[targetName] ? dataByTarget[targetName].laia : 0; const laiaSum = dataByTarget[targetName] ? dataByTarget[targetName].laia : 0;
const globalSum = alexSum + laiaSum; const globalSum = alexSum + laiaSum;
row.cells[1].innerText = Math.round(alexSum).toLocaleString(currentLang) + ' €'; row.cells[1].innerText = Math.round(alexSum).toLocaleString(window.appLang) + ' €';
row.cells[2].innerText = Math.round(laiaSum).toLocaleString(currentLang) + ' €'; row.cells[2].innerText = Math.round(laiaSum).toLocaleString(window.appLang) + ' €';
row.cells[3].innerText = Math.round(globalSum).toLocaleString(currentLang) + ' €'; row.cells[3].innerText = Math.round(globalSum).toLocaleString(window.appLang) + ' €';
grandTotalAlex += alexSum; grandTotalAlex += alexSum;
grandTotalLaia += laiaSum; grandTotalLaia += laiaSum;
}); });
} }
document.getElementById('grand_total_alex').innerText = Math.round(grandTotalAlex).toLocaleString(currentLang) + ' €'; document.getElementById('grand_total_alex').innerText = Math.round(grandTotalAlex).toLocaleString(window.appLang) + ' €';
document.getElementById('grand_total_laia').innerText = Math.round(grandTotalLaia).toLocaleString(currentLang) + ' €'; document.getElementById('grand_total_laia').innerText = Math.round(grandTotalLaia).toLocaleString(window.appLang) + ' €';
document.getElementById('grand_total_global').innerText = Math.round(grandTotalAlex + grandTotalLaia).toLocaleString(currentLang) + ' €'; document.getElementById('grand_total_global').innerText = Math.round(grandTotalAlex + grandTotalLaia).toLocaleString(window.appLang) + ' €';
} }
function saveData(action, data) { function saveData(action, data) {
@@ -825,7 +824,6 @@ function saveGenericNote(noteType, refId, content) {
}); });
} }
// LOGIQUE POUR LA CALCULATRICE RAPIDE
let isSumModeActive = false; let isSumModeActive = false;
let selectedElementsForSum = new Set(); let selectedElementsForSum = new Set();
@@ -868,7 +866,7 @@ function updateSumResult() {
total += val; total += val;
}); });
document.getElementById('sumResultValue').innerText = new Intl.NumberFormat(currentLang, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total); document.getElementById('sumResultValue').innerText = new Intl.NumberFormat(window.appLang, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total);
} }
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
+25 -42
View File
@@ -244,31 +244,27 @@ function getMonthName($dateString) {
</div> </div>
<script> <script>
window.I18N = { // --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
...window.I18N, window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
'bud_sav_ph_name' : "<?= tr('bud_sav_ph_name') ?>", window.I18N = {
'bud_sav_modal_title_edit' : "<?= tr('bud_sav_modal_title_edit') ?>", ...(window.I18N || {}),
'bud_sav_modal_title_add' : "<?= tr('bud_sav_modal_title_add') ?>", 'bud_sav_modal_title_add': "<?= tr('bud_sav_modal_title_add') ?>",
'bud_sav_saving' : "<?= tr('bud_sav_saving') ?>", 'bud_sav_modal_title_edit': "<?= tr('bud_sav_modal_title_edit') ?>",
'bud_err_tech' : "<?= tr('bud_err_tech') ?>", 'bud_sav_ph_name': "<?= tr('bud_sav_ph_name') ?>",
'bud_sav_confirm_delete_month' : "<?= tr('bud_sav_confirm_delete_month') ?>", 'bud_sav_confirm_delete_month': "<?= tr('bud_sav_confirm_delete_month') ?>",
'bud_sav_prompt_duplicate' : "<?= tr('bud_sav_prompt_duplicate') ?>", 'bud_sav_prompt_duplicate': "<?= tr('bud_sav_prompt_duplicate') ?>",
'bud_err_server' : "<?= tr('bud_err_server') ?>", 'bud_err_tech': "<?= tr('bud_err_tech') ?>",
'bud_err_network_dup' : "<?= tr('bud_err_network_dup') ?>" 'bud_err_server': "<?= tr('bud_err_server') ?>",
}; 'bud_err_network_dup': "<?= tr('bud_err_network_dup') ?>",
'bud_sav_saving': "<?= tr('bud_sav_saving') ?>",
'bud_err_delete': "<?= tr('bud_err_delete') ?>"
};
// Détection de la langue pour le JS // --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT ---
const currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR"; const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>;
</script>
<script>
// ============================================================================
// 1. GESTION DE L'ÉDITION INVISIBLE EN DIRECT (Input Classique)
// ============================================================================
function updateEpargneCell(month, category, owner, inputEl) { function updateEpargneCell(month, category, owner, inputEl) {
const val = parseFloat(inputEl.value) || 0; const val = parseFloat(inputEl.value) || 0;
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'update_single_entry'); formData.append('action', 'update_single_entry');
formData.append('month_date', month); formData.append('month_date', month);
@@ -282,7 +278,7 @@ function updateEpargneCell(month, category, owner, inputEl) {
}).catch(err => alert(tr("bud_err_tech"))); }).catch(err => alert(tr("bud_err_tech")));
const totalInput = document.querySelector(`.total-input-${owner}-${month}`); const totalInput = document.querySelector(`.total-input-${owner}-${month}`);
const totalVal = parseFloat(totalInput.value) || 0; const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 0;
let sumCats = 0; let sumCats = 0;
document.querySelectorAll(`.cat-input-${owner}-${month}`).forEach(inp => { document.querySelectorAll(`.cat-input-${owner}-${month}`).forEach(inp => {
@@ -293,18 +289,13 @@ function updateEpargneCell(month, category, owner, inputEl) {
const extraCell = document.getElementById(`extra_${owner}_${month}`); const extraCell = document.getElementById(`extra_${owner}_${month}`);
if (extraCell) { if (extraCell) {
extraCell.innerText = Math.round(extra).toLocaleString(currentLang) + ' €'; extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €';
extraCell.style.color = extra >= 0 ? '#10b981' : '#ef4444'; extraCell.style.color = extra >= 0 ? '#10b981' : '#ef4444';
} }
if(isSumModeActive) updateSumResult(); if(isSumModeActive) updateSumResult();
} }
// ============================================================================
// 2. MODALE D'ÉDITION CLASSIQUE / AJOUT
// ============================================================================
const cycleConfigs = <?= json_encode($cycleConfigs) ?>;
function addCustomEpargneLine(catName = '', amount = '') { function addCustomEpargneLine(catName = '', amount = '') {
const container = document.getElementById('linesContainer'); const container = document.getElementById('linesContainer');
const baseAmount = (amount !== '' && amount !== null) ? parseFloat(amount).toFixed(2) : '0.00'; const baseAmount = (amount !== '' && amount !== null) ? parseFloat(amount).toFixed(2) : '0.00';
@@ -315,19 +306,15 @@ function addCustomEpargneLine(catName = '', amount = '') {
<div style="flex:2;"> <div style="flex:2;">
<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> <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>
<div style="width:100px;"> <div style="width:100px;">
<input type="number" step="0.01" class="pf-input base-amount no-spinners" value="${baseAmount}" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; background:#fff;"> <input type="number" step="0.01" class="pf-input base-amount no-spinners" value="${baseAmount}" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; background:#fff;">
</div> </div>
<div style="width:90px;"> <div style="width:90px;">
<input type="number" step="0.01" class="pf-input adjustment-amount no-spinners" placeholder="+ / -" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; color:#f59e0b; font-weight:bold;"> <input type="number" step="0.01" class="pf-input adjustment-amount no-spinners" placeholder="+ / -" oninput="recalculateCustomLine(this)" style="padding:6px; font-size:0.9rem; color:#f59e0b; font-weight:bold;">
</div> </div>
<div style="width:100px;"> <div style="width:100px;">
<input type="number" step="0.01" name="${inputName}" class="pf-input final-amount no-spinners" value="${baseAmount}" style="padding:6px; font-size:0.9rem; font-weight:bold; background:#e0f2fe; border-color:#bae6fd; color:#0369a1;" readonly> <input type="number" step="0.01" name="${inputName}" class="pf-input final-amount no-spinners" value="${baseAmount}" style="padding:6px; font-size:0.9rem; font-weight:bold; background:#e0f2fe; border-color:#bae6fd; color:#0369a1;" readonly>
</div> </div>
<button type="button" onclick="this.parentElement.remove()" style="width:28px; height:28px; border:none; background:#fee2e2; color:#ef4444; border-radius:4px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-weight:bold;">&times;</button> <button type="button" onclick="this.parentElement.remove()" style="width:28px; height:28px; border:none; background:#fee2e2; color:#ef4444; border-radius:4px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-weight:bold;">&times;</button>
</div> </div>
`; `;
@@ -360,7 +347,7 @@ function editCustomSavingsMonth(monthDate, owner, rowData) {
document.getElementById('sav_month').value = ym; document.getElementById('sav_month').value = ym;
const dateObj = new Date(monthDate); const dateObj = new Date(monthDate);
const monthName = dateObj.toLocaleDateString(currentLang, { month: 'long', year: 'numeric' }); const monthName = dateObj.toLocaleDateString(window.appLang, { month: 'long', year: 'numeric' });
document.getElementById('savingsModalTitle').innerText = tr("bud_sav_modal_title_edit") + " " + monthName + " (" + owner + ")"; document.getElementById('savingsModalTitle').innerText = tr("bud_sav_modal_title_edit") + " " + monthName + " (" + owner + ")";
document.getElementById('sav_total').value = rowData['TOTAL_BANQUE'] || ''; document.getElementById('sav_total').value = rowData['TOTAL_BANQUE'] || '';
@@ -440,7 +427,7 @@ function deleteEntireMonth(monthDate, owner) {
formData.append("owner", owner); formData.append("owner", owner);
fetch("/modules/budget/includes/api/save-savings.php", { method: "POST", body: formData }) fetch("/modules/budget/includes/api/save-savings.php", { method: "POST", body: formData })
.then(() => window.location.reload()) .then(() => window.location.reload())
.catch(err => alert("<?= tr('bud_err_delete') ?>")); .catch(err => alert(tr('bud_err_delete')));
} }
function duplicateLastMonth(lastMonthDate, owner) { function duplicateLastMonth(lastMonthDate, owner) {
@@ -451,7 +438,7 @@ function duplicateLastMonth(lastMonthDate, owner) {
let nextMonthStr = `${year}-${month}-01`; let nextMonthStr = `${year}-${month}-01`;
const formatMonth = (d) => { const formatMonth = (d) => {
let str = new Date(d).toLocaleDateString(currentLang, { month: 'long', year: 'numeric' }); let str = new Date(d).toLocaleDateString(window.appLang, { month: 'long', year: 'numeric' });
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
}; };
@@ -496,15 +483,12 @@ function duplicateLastMonth(lastMonthDate, owner) {
} }
} }
// ============================================================================ // --- CALCULATRICE ---
// 3. CALCULATRICE (SOMME RAPIDE)
// ============================================================================
let isSumModeActive = false; let isSumModeActive = false;
let selectedElementsForSum = new Set(); let selectedElementsForSum = new Set();
function toggleSumMode() { function toggleSumMode() {
isSumModeActive = !isSumModeActive; isSumModeActive = !isSumModeActive;
const fab = document.getElementById('fabSumMode'); const fab = document.getElementById('fabSumMode');
const resultBar = document.getElementById('sumResultBar'); const resultBar = document.getElementById('sumResultBar');
@@ -541,13 +525,12 @@ function updateSumResult() {
total += val; total += val;
}); });
document.getElementById('sumResultValue').innerText = new Intl.NumberFormat(currentLang, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total); document.getElementById('sumResultValue').innerText = new Intl.NumberFormat(window.appLang, { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(total);
} }
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
if (!isSumModeActive) return; 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'); const targetElement = e.target.closest('input[type="number"], .sum-target');
if (targetElement) { if (targetElement) {
@@ -563,5 +546,5 @@ document.addEventListener('click', function(e) {
updateSumResult(); updateSumResult();
} }
}, true); // Le 'true' intercepte le clic avant le focus normal }, true);
</script> </script>
+16 -4
View File
@@ -168,7 +168,6 @@
min-width: 1000px; min-width: 1000px;
border-collapse: separate; border-collapse: separate;
border-spacing: 0; border-spacing: 0;
table-layout: fixed;
} }
/* Sticky Header */ /* Sticky Header */
@@ -303,12 +302,25 @@
width: auto !important; width: auto !important;
border-right: 2px solid var(--border-color) !important; border-right: 2px solid var(--border-color) !important;
} }
.col-total, th.col-total,
.col-alex-sub, td.col-total,
.col-laia-sub { th.col-alex-sub,
td.col-alex-sub,
th.col-laia-sub,
td.col-laia-sub {
width: 32px !important; width: 32px !important;
min-width: 32px !important; min-width: 32px !important;
max-width: 32px !important; max-width: 32px !important;
box-sizing: border-box;
padding: 0 2px !important; /* On réduit le padding pour que les chiffres respirent */
overflow: hidden;
}
th.col-day,
td.col-day {
width: auto !important;
min-width: 35px !important;
border-right: 2px solid var(--border-color) !important;
} }
.rotated-text span { .rotated-text span {
+4 -4
View File
@@ -944,10 +944,10 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
div.innerHTML = ` div.innerHTML = `
<div class="fc-summary-item"><span class="fc-summary-label">Off Carole</span><span class="fc-summary-value">${parseFloat(stats.off.toFixed(1))} jours</span></div> <div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_off_carole")}</span><span class="fc-summary-value">${parseFloat(stats.off.toFixed(1))} ${tr("fc_unit_days")}</span></div>
<div class="fc-summary-item"><span class="fc-summary-label">Extra Off Carole</span><span class="fc-summary-value">${parseFloat(stats.extra.toFixed(1))} jours</span></div> <div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_extra_off")}</span><span class="fc-summary-value">${parseFloat(stats.extra.toFixed(1))} ${tr("fc_unit_days")}</span></div>
<div class="fc-summary-item"><span class="fc-summary-label">Pep Malade</span><span class="fc-summary-value">${parseFloat(stats.sick.toFixed(1))} jours</span></div> <div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_pep_sick")}</span><span class="fc-summary-value">${parseFloat(stats.sick.toFixed(1))} ${tr("fc_unit_days")}</span></div>
<div class="fc-summary-item"><span class="fc-summary-label">Présence Pep</span><span class="fc-summary-value">${parseFloat(stats.pep.toFixed(1))} jours</span></div> <div class="fc-summary-item"><span class="fc-summary-label">${tr("leg_presence")} Pep</span><span class="fc-summary-value">${parseFloat(stats.pep.toFixed(1))} ${tr("fc_unit_days")}</span></div>
`; `;
} }
+23
View File
@@ -1196,3 +1196,26 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
box-shadow: 0 0 0 3px var(--primary); box-shadow: 0 0 0 3px var(--primary);
transform: scale(1.02); transform: scale(1.02);
} }
/* Style du badge météo injecté par le JS */
.hol-weather-info {
display: inline-flex;
}
.pf-weather-badge {
display: flex;
align-items: center;
gap: 4px;
background: #f8fafc; /* Slate 50 */
border: 1px solid #e2e8f0; /* Slate 200 */
padding: 1px 6px;
border-radius: 6px;
color: #475569; /* Slate 600 */
font-weight: 600;
font-size: 0.7rem;
line-height: 1;
}
.pf-weather-icon {
font-size: 0.9rem;
}
+66 -5
View File
@@ -5,8 +5,60 @@ function tr(key) {
return window.I18N && window.I18N[key] ? window.I18N[key] : key; return window.I18N && window.I18N[key] ? window.I18N[key] : key;
} }
// On détecte la langue de la page (définie dans la balise <html lang="..."> du header) // On utilise 'var' au lieu de 'const/let' pour éviter les crashs si le fichier est lu 2 fois
const currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR"; var currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
var selectedItemIdForMove = null; // Déplacé ici pour plus de clarté
// ============================================================================
// UTILITAIRES MÉTÉO
// ============================================================================
function getWeatherInfo(code) {
// Transformation en conditions pour regrouper les codes WMO
if (code === 0) return { icon: "☀️", label: tr("weather_sunny") };
if ([1, 2].includes(code)) return { icon: "🌤️", label: tr("weather_sunny") };
if ([3, 45, 48].includes(code))
return { icon: "☁️", label: tr("weather_cloudy") };
// Les codes 51 à 67 et 80 à 82 couvrent toutes les formes de pluie et bruine
if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].includes(code))
return { icon: "🌧️", label: tr("weather_rainy") };
// Les codes neigeux
if ([71, 73, 75, 77, 85, 86].includes(code))
return { icon: "❄️", label: tr("weather_snowy") };
// Orages
if ([95, 96, 99].includes(code))
return { icon: "⛈️", label: tr("weather_rainy") };
return { icon: "🌡️", label: tr("weather_forecast") };
}
async function loadWeatherForStep(pt) {
if (!pt.step_start_date || !pt.lat || !pt.lng) return;
const container = document.querySelector(
`#step-card-${pt.sort_order} .hol-weather-info`,
);
if (!container) return;
try {
const resp = await fetch(
`/modules/holidays/includes/api/get_weather.php?lat=${pt.lat}&lng=${pt.lng}&date=${pt.step_start_date}`,
);
const res = await resp.json();
console.log(`Météo pour ${pt.location_name} :`, res);
if (res.success) {
const info = getWeatherInfo(res.data.code);
container.innerHTML = `
<div class="pf-weather-badge" title="${info.label}">
<span class="pf-weather-icon">${info.icon}</span>
<span>${Math.round(res.data.temp_min)}° / ${Math.round(res.data.temp_max)}°C</span>
</div>`;
}
} catch (e) {
console.error("Weather error", e);
}
}
// ============================================================================ // ============================================================================
// FERMETURE UNIVERSELLE DES MODALES // FERMETURE UNIVERSELLE DES MODALES
@@ -150,7 +202,7 @@ function deleteHoliday() {
// --- 3. GESTION DE LA CARTE --- // --- 3. GESTION DE LA CARTE ---
let map = null; var map = null;
function toggleMap() { function toggleMap() {
const modal = document.getElementById("hol-map-modal"); const modal = document.getElementById("hol-map-modal");
@@ -197,7 +249,7 @@ function initMap() {
// 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET GÉOCODAGE // 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET GÉOCODAGE
// ============================================================================ // ============================================================================
let detailMap = null; var detailMap = null;
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
if (document.getElementById("tripMap")) { if (document.getElementById("tripMap")) {
@@ -208,6 +260,11 @@ document.addEventListener("DOMContentLoaded", () => {
function initDetailMap() { function initDetailMap() {
if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return; if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return;
const mapContainer = L.DomUtil.get("tripMap");
if (mapContainer !== null && mapContainer._leaflet_id) {
mapContainer._leaflet_id = null;
}
detailMap = L.map("tripMap"); detailMap = L.map("tripMap");
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19, maxZoom: 19,
@@ -323,6 +380,11 @@ function initDetailMap() {
} }
}); });
}); });
detailMap.fitBounds(bounds, { padding: [50, 50] });
// Lancement de la météo pour chaque étape
if (typeof MAP_POINTS !== "undefined") {
MAP_POINTS.forEach((pt) => loadWeatherForStep(pt));
}
} }
function drawFallbackLine(coords, color, weight) { function drawFallbackLine(coords, color, weight) {
@@ -582,7 +644,6 @@ document.addEventListener("DOMContentLoaded", () => {
// ============================================================================ // ============================================================================
// MOTEUR DRAG & DROP DU PLANNING // MOTEUR DRAG & DROP DU PLANNING
// ============================================================================ // ============================================================================
let selectedItemIdForMove = null;
function closePlanningModal() { function closePlanningModal() {
document.getElementById("planningModal").style.display = "none"; document.getElementById("planningModal").style.display = "none";
@@ -0,0 +1,63 @@
<?php
// modules/holidays/includes/api/get_weather.php
// 1. Désactiver les erreurs HTML pour ne pas corrompre le JSON
ini_set('display_errors', 0);
error_reporting(0);
// 2. Inclusion de tes fichiers vitaux (C'est auth.php qui sécurise l'accès !)
$basePath = '../../../../';
require_once $basePath . 'includes/db.php';
require_once $basePath . 'includes/auth.php';
// Si on arrive à cette ligne, c'est que auth.php a validé la session.
header('Content-Type: application/json');
// 3. Récupération des paramètres GPS et Date
$lat = filter_input(INPUT_GET, 'lat', FILTER_VALIDATE_FLOAT);
$lng = filter_input(INPUT_GET, 'lng', FILTER_VALIDATE_FLOAT);
$date = $_GET['date'] ?? null;
if (!$lat || !$lng || !$date) {
echo json_encode(['success' => false, 'message' => 'Paramètres manquants']);
exit;
}
$date = substr($date, 0, 10);
$today = new DateTime();
$targetDate = new DateTime($date);
$interval = $today->diff($targetDate);
$daysDiff = (int)$interval->format('%R%a');
// 4. Choix du bon modèle météo (Prévisions vs Historique)
if ($daysDiff < -2) {
$baseUrl = "https://archive-api.open-meteo.com/v1/archive";
} else {
$baseUrl = "https://api.open-meteo.com/v1/forecast";
}
$url = "$baseUrl?latitude=$lat&longitude=$lng&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto&start_date=$date&end_date=$date";
// 5. Interrogation d'Open-Meteo
$res = @file_get_contents($url);
if (!$res) {
echo json_encode(['success' => false, 'message' => 'Erreur API Open-Meteo']);
exit;
}
$data = json_decode($res, true);
// 6. Formatage et envoi au Javascript
if (isset($data['daily']['weather_code'][0])) {
echo json_encode([
'success' => true,
'data' => [
'code' => $data['daily']['weather_code'][0],
'temp_max' => $data['daily']['temperature_2m_max'][0],
'temp_min' => $data['daily']['temperature_2m_min'][0]
]
]);
} else {
echo json_encode(['success' => false, 'message' => 'Aucune donnée pour cette date']);
}
+45 -9
View File
@@ -204,8 +204,11 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<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> <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 endif; ?>
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?> <?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;"> <div style="font-size:0.75rem; color:#64748b; font-weight:normal; margin-top:2px; display: flex; align-items: center; gap: 10px;">
<span>
<?= tr('hdl_from') ?> <?= date('d/m', strtotime($step['step_start_date'])) ?> <?= tr('hdl_to') ?> <?= 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'])) ?>
</span>
<div class="hol-weather-info"></div>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
@@ -352,19 +355,52 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div> </div>
</div> </div>
<script> <?php include __DIR__ . '/modal.php'; ?>
const MAP_POINTS = <?= json_encode($mapPoints) ?>;
// Le pont i18n pour Javascript (Holidays specific) <script>
window.I18N = { // --- 1. SÉCURISATION TRADUCTIONS ET VARIABLES ---
...window.I18N, window.MAP_POINTS = <?= json_encode($mapPoints ?? []) ?>;
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
window.I18N = {
...(window.I18N || {}),
'hdl_js_search_loading': "<?= tr('hdl_js_search_loading') ?>", 'hdl_js_search_loading': "<?= tr('hdl_js_search_loading') ?>",
'hdl_js_no_result': "<?= tr('hdl_js_no_result') ?>", 'hdl_js_no_result': "<?= tr('hdl_js_no_result') ?>",
'hdl_js_confirm_del_trip': "<?= tr('hdl_js_confirm_del_trip') ?>", 'hdl_js_confirm_del_trip': "<?= tr('hdl_js_confirm_del_trip') ?>",
'hdl_js_confirm_del_step': "<?= tr('hdl_js_confirm_del_step') ?>", 'hdl_js_confirm_del_step': "<?= tr('hdl_js_confirm_del_step') ?>",
'hdl_js_step_label': "<?= tr('hdl_js_step_label') ?>", 'hdl_js_step_label': "<?= tr('hdl_js_step_label') ?>",
'hdl_js_ph_expense_name': "<?= tr('hdl_js_ph_expense_name') ?>", 'hdl_js_ph_expense_name': "<?= tr('hdl_js_ph_expense_name') ?>",
'hdl_js_ph_amount': "0.00", 'hdl_js_delete_line': "<?= tr('btn_delete') ?>",
'hdl_js_delete_line': "<?= tr('btn_delete') ?>" 'hdl_planning_title': "<?= tr('hdl_planning_title') ?>",
}; 'hdl_to_place': "<?= tr('hdl_to_place') ?>",
'hdl_js_missing_dates_title': "<?= tr('hdl_js_missing_dates_title') ?>",
'hdl_js_missing_dates_msg': "<?= tr('hdl_js_missing_dates_msg') ?>",
'hdl_modal_title': "<?= tr('hdl_modal_title') ?>",
'hdl_js_edit_step': "<?= tr('hdl_js_edit_step') ?>",
'hdl_ph_notes': "<?= tr('hdl_ph_notes') ?>",
'hdl_btn_add_step': "<?= tr('hdl_btn_add_step') ?>",
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>",
// --- NOUVELLES CLÉS MÉTÉO ICI ---
'weather_sunny': "<?= tr('weather_sunny') ?>",
'weather_cloudy': "<?= tr('weather_cloudy') ?>",
'weather_rainy': "<?= tr('weather_rainy') ?>",
'weather_snowy': "<?= tr('weather_snowy') ?>",
'weather_forecast': "<?= tr('weather_forecast') ?>"
};
// Fallback de sécurité pour s'assurer que les modales peuvent toujours se fermer
window.closeCheckpointModal = window.closeCheckpointModal || function() {
const modal = document.getElementById('checkpointModal');
if(modal) modal.style.display = 'none';
document.body.classList.remove('no-scroll');
};
window.closePlanningModal = window.closePlanningModal || function() {
const modal = document.getElementById('planningModal');
if(modal) modal.style.display = 'none';
document.body.classList.remove('no-scroll');
};
</script> </script>
<script src="/modules/holidays/holidays.js"></script>
+31 -1
View File
@@ -90,6 +90,37 @@ foreach ($active as $h) {
<?php include __DIR__ . '/modal.php'; ?> <?php include __DIR__ . '/modal.php'; ?>
<script>
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
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_delete_line': "<?= tr('btn_delete') ?>",
'hdl_planning_title': "<?= tr('hdl_planning_title') ?>",
'hdl_to_place': "<?= tr('hdl_to_place') ?>",
'hdl_js_missing_dates_title': "<?= tr('hdl_js_missing_dates_title') ?>",
'hdl_js_missing_dates_msg': "<?= tr('hdl_js_missing_dates_msg') ?>",
'hdl_modal_title': "<?= tr('hdl_modal_title') ?>",
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>"
};
// Fallback de sécurité pour s'assurer que la modale peut toujours se fermer
window.closeHolidayModal = window.closeHolidayModal || function() {
const modal = document.getElementById('holidayModal');
if(modal) modal.style.display = 'none';
document.body.classList.remove('no-scroll');
};
</script>
<script src="/modules/holidays/holidays.js"></script>
<?php <?php
function renderHolidayCard($h, $pdo) { function renderHolidayCard($h, $pdo) {
$stmt = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ?"); $stmt = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ?");
@@ -119,7 +150,6 @@ function renderHolidayCard($h, $pdo) {
$pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0; $pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0;
$pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0; $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
// Traduction dynamique du statut
$statusLabel = tr('hdl_status_' . $h['status']); $statusLabel = tr('hdl_status_' . $h['status']);
echo " echo "