+281
@@ -80,6 +80,7 @@ require __DIR__ . '/header.php';
|
|||||||
<button class="bs-tab-btn" onclick="switchBsTab('categories', this)"><?= tr('bs_tab_categories') ?></button>
|
<button class="bs-tab-btn" onclick="switchBsTab('categories', this)"><?= tr('bs_tab_categories') ?></button>
|
||||||
<button class="bs-tab-btn" onclick="switchBsTab('rules', this)"><?= tr('bs_tab_rules') ?></button>
|
<button class="bs-tab-btn" onclick="switchBsTab('rules', this)"><?= tr('bs_tab_rules') ?></button>
|
||||||
<button class="bs-tab-btn" onclick="switchBsTab('salaries', this)"><?= tr('bs_tab_salaries') ?></button>
|
<button class="bs-tab-btn" onclick="switchBsTab('salaries', this)"><?= tr('bs_tab_salaries') ?></button>
|
||||||
|
<button class="bs-tab-btn" onclick="switchBsTab('csv', this)"><?= tr('bs_tab_csv') ?></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bs-content">
|
<div class="bs-content">
|
||||||
@@ -187,6 +188,89 @@ require __DIR__ . '/header.php';
|
|||||||
<div id="bs-list-salaries">⏳ <?= tr('loading') ?></div>
|
<div id="bs-list-salaries">⏳ <?= tr('loading') ?></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="pane-csv" class="bs-pane">
|
||||||
|
<h4 class="bs-section-title"><?= tr('bs_csv_title') ?></h4>
|
||||||
|
<p class="pf-muted-tiny" style="margin-top:0; margin-bottom:15px;"><?= tr('bs_csv_desc') ?></p>
|
||||||
|
|
||||||
|
<div id="csv-drop-zone" style="border: 2px dashed var(--border-light, #ccc); border-radius: var(--radius); padding: 30px; text-align: center; cursor: pointer; margin-bottom: 25px; transition: all 0.2s;">
|
||||||
|
<div style="font-size: 2rem; margin-bottom: 10px;">📥</div>
|
||||||
|
<h5 style="margin: 0 0 5px 0;"><?= tr('bs_csv_drop_title') ?></h5>
|
||||||
|
<p class="pf-muted-tiny" style="margin: 0;"><?= tr('bs_csv_drop_desc') ?></p>
|
||||||
|
<input type="file" id="csv_file_input" accept=".csv" style="display: none;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="csv-preview-container" style="display:none; background: var(--bg-soft, #f8fafc); padding: 15px; border-radius: var(--radius); border: 1px dashed var(--primary); margin-bottom: 25px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 style="margin: 0 0 15px 0; border-bottom: 1px solid var(--border-light); padding-bottom: 5px;">
|
||||||
|
<?= tr('bs_csv_settings_title') ?>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<form id="form-csv-mapping" onsubmit="saveCsvMapping(event)">
|
||||||
|
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_delimiter') ?></label>
|
||||||
|
<select name="csv_delimiter" id="csv_delimiter" class="pf-input">
|
||||||
|
<option value=";"><?= tr('bs_csv_delim_semi') ?></option>
|
||||||
|
<option value=","><?= tr('bs_csv_delim_comma') ?></option>
|
||||||
|
<option value="\t"><?= tr('bs_csv_delim_tab') ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_date_format') ?></label>
|
||||||
|
<select name="csv_date_format" id="csv_date_format" class="pf-input">
|
||||||
|
<option value="d/m/Y">JJ/MM/AAAA</option>
|
||||||
|
<option value="Y-m-d">AAAA-MM-JJ</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 style="margin: 15px 0 10px; font-size: 0.95rem;">
|
||||||
|
<?= tr('bs_csv_col_index') ?>
|
||||||
|
<small style="color:var(--text-muted); font-weight:normal;"><?= tr('bs_csv_col_index_help') ?></small>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_col_date') ?></label>
|
||||||
|
<input type="number" min="0" name="csv_col_date" id="csv_col_date" class="pf-input" value="0" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_col_label') ?></label>
|
||||||
|
<input type="number" min="0" name="csv_col_label" id="csv_col_label" class="pf-input" value="1" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: var(--bg-panel); padding: 10px; border-radius: var(--radius); border: 1px solid var(--border-light); margin-bottom: 15px;">
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_amount_mgmt') ?></label>
|
||||||
|
<select name="csv_amount_type" id="csv_amount_type" class="pf-input" style="margin-bottom: 10px;" onchange="toggleCsvAmountCols(this.value)">
|
||||||
|
<option value="single"><?= tr('bs_csv_amount_single') ?></option>
|
||||||
|
<option value="split"><?= tr('bs_csv_amount_split') ?></option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
||||||
|
<div>
|
||||||
|
<label class="pf-label" id="lbl_col_debit"><?= tr('bs_csv_col_amount') ?></label>
|
||||||
|
<input type="number" min="0" name="csv_col_debit" id="csv_col_debit" class="pf-input" value="8" required>
|
||||||
|
</div>
|
||||||
|
<div id="wrapper_col_credit" style="display:none;">
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_col_credit') ?></label>
|
||||||
|
<input type="number" min="0" name="csv_col_credit" id="csv_col_credit" class="pf-input" value="9">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 20px;">
|
||||||
|
<label class="pf-label"><?= tr('bs_csv_col_ref') ?></label>
|
||||||
|
<input type="number" min="0" name="csv_col_ref" id="csv_col_ref" class="pf-input" value="3">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<button type="submit" class="btn btn-secondary">💾 <?= tr('btn_save_format') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -719,5 +803,202 @@ async function updateBudgetCurrency(selectEl) {
|
|||||||
alert("Erreur : " + err.message);
|
alert("Erreur : " + err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- GESTION DU MAPPING CSV ---
|
||||||
|
window.I18N = {
|
||||||
|
...(window.I18N || {}),
|
||||||
|
'bs_csv_col_amount': <?= json_encode(tr('bs_csv_col_amount')) ?>,
|
||||||
|
'bs_csv_col_debit': <?= json_encode(tr('bs_csv_col_debit')) ?>,
|
||||||
|
'bs_csv_saved': <?= json_encode(tr('bs_csv_saved')) ?>,
|
||||||
|
'error_occured': <?= json_encode(tr('error_occured')) ?>
|
||||||
|
};
|
||||||
|
|
||||||
|
function toggleCsvAmountCols(type) {
|
||||||
|
if (type === 'split') {
|
||||||
|
document.getElementById('wrapper_col_credit').style.display = 'block';
|
||||||
|
document.getElementById('lbl_col_debit').innerText = window.I18N['bs_csv_col_debit'];
|
||||||
|
} else {
|
||||||
|
document.getElementById('wrapper_col_credit').style.display = 'none';
|
||||||
|
document.getElementById('lbl_col_debit').innerText = window.I18N['bs_csv_col_amount'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCsvMapping(mapping) {
|
||||||
|
if (!mapping) return;
|
||||||
|
document.getElementById('csv_delimiter').value = mapping.delimiter || ';';
|
||||||
|
document.getElementById('csv_date_format').value = mapping.date_format || 'd/m/Y';
|
||||||
|
document.getElementById('csv_col_date').value = mapping.col_date ?? 0;
|
||||||
|
document.getElementById('csv_col_label').value = mapping.col_label ?? 1;
|
||||||
|
document.getElementById('csv_col_ref').value = mapping.col_ref ?? 3;
|
||||||
|
|
||||||
|
const amountType = mapping.amount_type || 'split';
|
||||||
|
document.getElementById('csv_amount_type').value = amountType;
|
||||||
|
document.getElementById('csv_col_debit').value = mapping.col_debit ?? 8;
|
||||||
|
document.getElementById('csv_col_credit').value = mapping.col_credit ?? 9;
|
||||||
|
|
||||||
|
toggleCsvAmountCols(amountType);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCsvMapping(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const btn = form.querySelector('button[type="submit"]');
|
||||||
|
const oldText = btn.innerText;
|
||||||
|
btn.innerText = '⏳...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
formData.append('action', 'save_csv_mapping');
|
||||||
|
|
||||||
|
const response = await pachaFetch('/modules/budget/includes/api/settings.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.success) throw new Error(response.error || window.I18N['error_occured']);
|
||||||
|
|
||||||
|
if (typeof showToast === 'function') showToast(window.I18N['bs_csv_saved']);
|
||||||
|
else alert(window.I18N['bs_csv_saved']);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
alert(window.I18N['error_occured'] + " : " + err.message);
|
||||||
|
} finally {
|
||||||
|
btn.innerText = oldText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTION DE LA DROP ZONE CSV ET FILEREADER ---
|
||||||
|
|
||||||
|
// Ajout des nouvelles clés I18N magiques
|
||||||
|
window.I18N = {
|
||||||
|
...(window.I18N || {}),
|
||||||
|
'bs_csv_err_type': <?= json_encode(tr('bs_csv_err_type')) ?>,
|
||||||
|
'bs_csv_file_ok': <?= json_encode(tr('bs_csv_file_ok')) ?>,
|
||||||
|
'bs_csv_preview_title': <?= json_encode(tr('bs_csv_preview_title')) ?>,
|
||||||
|
'bs_csv_empty_col': <?= json_encode(tr('bs_csv_empty_col')) ?>,
|
||||||
|
'bs_csv_no_data': <?= json_encode(tr('bs_csv_no_data')) ?>
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropZone = document.getElementById('csv-drop-zone');
|
||||||
|
const fileInput = document.getElementById('csv_file_input');
|
||||||
|
|
||||||
|
if (dropZone && fileInput) {
|
||||||
|
dropZone.addEventListener('click', () => fileInput.click());
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.style.backgroundColor = 'rgba(0, 123, 255, 0.05)';
|
||||||
|
dropZone.style.borderColor = 'var(--primary-color, #007bff)';
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.style.backgroundColor = '';
|
||||||
|
dropZone.style.borderColor = 'var(--border-light, #ccc)';
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.style.backgroundColor = '';
|
||||||
|
dropZone.style.borderColor = 'var(--border-light, #ccc)';
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length) {
|
||||||
|
fileInput.files = e.dataTransfer.files;
|
||||||
|
handleCsvUpload(e.dataTransfer.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', function() {
|
||||||
|
if (this.files.length) {
|
||||||
|
handleCsvUpload(this.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCsvUpload(file) {
|
||||||
|
if (!file.name.endsWith('.csv')) {
|
||||||
|
alert(window.I18N['bs_csv_err_type']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(window.I18N['bs_csv_file_ok'] + ' : ' + file.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function(e) {
|
||||||
|
const text = e.target.result;
|
||||||
|
const delimiter = document.getElementById('csv_delimiter').value || ';';
|
||||||
|
|
||||||
|
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
|
||||||
|
|
||||||
|
if (lines.length > 0) {
|
||||||
|
// Extraction des en-têtes (Ligne 0)
|
||||||
|
const headers = lines[0].split(delimiter).map(h => h.replace(/^"|"$/g, '').trim());
|
||||||
|
|
||||||
|
// Extraction de max 2 lignes de données (Ligne 1 et 2)
|
||||||
|
const sampleRows = [];
|
||||||
|
for (let i = 1; i < Math.min(lines.length, 3); i++) {
|
||||||
|
const rowData = lines[i].split(delimiter).map(d => d.replace(/^"|"$/g, '').trim());
|
||||||
|
sampleRows.push(rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCsvTablePreview(headers, sampleRows);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file, 'ISO-8859-1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCsvTablePreview(headers, sampleRows) {
|
||||||
|
const container = document.getElementById('csv-preview-container');
|
||||||
|
|
||||||
|
let html = `<h5 style="margin: 0 0 10px 0; color: var(--primary); font-size: 0.95rem;">👁️ ${window.I18N['bs_csv_preview_title']}</h5>`;
|
||||||
|
|
||||||
|
// Conteneur avec scroll horizontal si le tableau est trop large
|
||||||
|
html += `<div style="overflow-x: auto; max-width: 100%; border: 1px solid var(--border-light); border-radius: 8px; background: white;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.85rem; text-align: left; white-space: nowrap;">
|
||||||
|
<thead style="background: var(--bg-page, #f1f5f9); border-bottom: 2px solid var(--border-light);">
|
||||||
|
<tr>`;
|
||||||
|
|
||||||
|
// Rendu des en-têtes de colonnes (avec le numéro)
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
const cleanHeader = header || `(${window.I18N['bs_csv_empty_col']})`;
|
||||||
|
html += `<th style="padding: 10px 15px; font-weight: 600; color: var(--text-main); border-right: 1px solid var(--border-light);">
|
||||||
|
<div style="font-size: 0.75rem; color: var(--primary); margin-bottom: 2px;">N° ${index}</div>
|
||||||
|
<div>${cleanHeader}</div>
|
||||||
|
</th>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += ` </tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>`;
|
||||||
|
|
||||||
|
// Rendu des données
|
||||||
|
if (sampleRows.length === 0) {
|
||||||
|
html += `<tr><td colspan="${headers.length}" style="padding: 15px; text-align: center; color: var(--text-muted); font-style: italic;">${window.I18N['bs_csv_no_data']}</td></tr>`;
|
||||||
|
} else {
|
||||||
|
sampleRows.forEach(row => {
|
||||||
|
html += `<tr style="border-bottom: 1px solid #e2e8f0;">`;
|
||||||
|
// On boucle sur la taille des headers pour s'assurer que l'affichage ne casse pas si la ligne de données est incomplète
|
||||||
|
for (let i = 0; i < headers.length; i++) {
|
||||||
|
const cellData = row[i] !== undefined ? row[i] : '';
|
||||||
|
// Raccourcir la donnée si elle est vraiment très longue (ex: libellé de 100 caractères)
|
||||||
|
const displayData = cellData.length > 40 ? cellData.substring(0, 40) + '...' : cellData;
|
||||||
|
|
||||||
|
html += `<td style="padding: 10px 15px; color: var(--text-muted); border-right: 1px dashed #e2e8f0;">${displayData}</td>`;
|
||||||
|
}
|
||||||
|
html += `</tr>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `</tbody></table></div>`;
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
container.style.display = 'block';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
@@ -12,6 +12,29 @@ CREATE TABLE IF NOT EXISTS pf_foyer_settings (
|
|||||||
|
|
||||||
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
INSERT IGNORE INTO pf_foyer_settings (id, currency, zone_scolaire) VALUES (1, '€', 'C');
|
||||||
|
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
-- Paramètres dynamiques des Modules (Key-Value)
|
||||||
|
-- ────────────────────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS `pf_settings` (
|
||||||
|
`setting_key` varchar(50) NOT NULL,
|
||||||
|
`setting_value` text DEFAULT NULL,
|
||||||
|
`module` varchar(50) NOT NULL,
|
||||||
|
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`setting_key`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `pf_settings` (`setting_key`, `setting_value`, `module`) VALUES
|
||||||
|
('calendar_default_view', 'month', 'calendar'),
|
||||||
|
('calendar_first_day', '1', 'calendar'),
|
||||||
|
('calendar_working_hours', '08:00-19:00', 'calendar'),
|
||||||
|
('budget_start_day', '1', 'budget'),
|
||||||
|
('budget_default_tab', 'dépenses', 'budget'),
|
||||||
|
('travel_default_transport', 'car', 'voyage'),
|
||||||
|
('travel_default_fuel_price', '1.85', 'voyage'),
|
||||||
|
('gifts_hide_purchased', '0', 'cadeaux'),
|
||||||
|
('gifts_default_sort', 'person', 'cadeaux'),
|
||||||
|
('gifts_budget_alert', '500', 'cadeaux');
|
||||||
|
|
||||||
-- ─── Utilisateurs (Legacy) ────────────────────────────────────────────────────
|
-- ─── Utilisateurs (Legacy) ────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_users (
|
CREATE TABLE IF NOT EXISTS pf_users (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -178,6 +201,25 @@ CREATE TABLE IF NOT EXISTS `pf_advances` (
|
|||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ─── Budget Catégories (Dynamiques) ─────────────────────────────────────────────
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_budget_categories (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
label VARCHAR(100) NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
color VARCHAR(20) DEFAULT '#ccc',
|
||||||
|
icon VARCHAR(20) DEFAULT '?'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- Insertion des catégories de base pour toute nouvelle famille
|
||||||
|
INSERT IGNORE INTO pf_budget_categories (code, label, type, color, icon) VALUES
|
||||||
|
('INCOME', 'Revenus', 'Income', '#10b981', '💵'),
|
||||||
|
('FMCG', 'Alimentation & Courses', 'Expense', '#3b82f6', '🛒'),
|
||||||
|
('FUEL', 'Carburant', 'Expense', '#f59e0b', '⛽'),
|
||||||
|
('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢'),
|
||||||
|
('SCHOOL', 'École & Garde', 'Expense', '#a855f7', '🎒'),
|
||||||
|
('SAVINGS', 'Épargne', 'Expense', '#8b5cf6', '🐷');
|
||||||
|
|
||||||
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_notes (
|
CREATE TABLE IF NOT EXISTS pf_notes (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -287,6 +329,7 @@ CREATE TABLE IF NOT EXISTS pf_vehicles (
|
|||||||
license_plate VARCHAR(50) DEFAULT NULL,
|
license_plate VARCHAR(50) DEFAULT NULL,
|
||||||
vin VARCHAR(100) DEFAULT NULL,
|
vin VARCHAR(100) DEFAULT NULL,
|
||||||
fuel_type VARCHAR(50) DEFAULT 'Essence',
|
fuel_type VARCHAR(50) DEFAULT 'Essence',
|
||||||
|
consumption decimal(4,2) DEFAULT NULL COMMENT 'Consommation L/100km',
|
||||||
color VARCHAR(50) DEFAULT NULL,
|
color VARCHAR(50) DEFAULT NULL,
|
||||||
purchase_date DATE DEFAULT NULL,
|
purchase_date DATE DEFAULT NULL,
|
||||||
purchase_price DECIMAL(10,2) DEFAULT NULL,
|
purchase_price DECIMAL(10,2) DEFAULT NULL,
|
||||||
|
|||||||
+121
-149
@@ -8,50 +8,42 @@ require_login();
|
|||||||
require __DIR__ . '/includes/db.php';
|
require __DIR__ . '/includes/db.php';
|
||||||
require_once __DIR__ . '/includes/i18n.php';
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
|
|
||||||
// 1. Récupération de toutes les personnes avec leurs modes de garde
|
// 1. Récupération de toutes les personnes
|
||||||
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, care_modes FROM pf_people WHERE is_active = 1 ORDER BY id ASC");
|
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color, care_modes FROM pf_people WHERE is_active = 1 ORDER BY id ASC");
|
||||||
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$parents = [];
|
$parents = [];
|
||||||
$kids = [];
|
$kids = [];
|
||||||
|
$helpers = [];
|
||||||
// Liste de tous les modes de garde actifs dans le foyer
|
|
||||||
$activeCareModes = [];
|
|
||||||
|
|
||||||
foreach ($familyPeople as $p) {
|
foreach ($familyPeople as $p) {
|
||||||
$role = strtolower($p['role'] ?? '');
|
$role = strtolower($p['role'] ?? '');
|
||||||
if ($role === 'parent') {
|
if ($role === 'parent') {
|
||||||
// --- 🟢 NOUVEAU : Récupération dynamique des types de congés du parent ---
|
|
||||||
$stmtLT = $pdo->prepare("
|
|
||||||
SELECT DISTINCT leave_type FROM (
|
|
||||||
SELECT leave_type FROM pf_leave_balances WHERE person_id = ?
|
|
||||||
UNION
|
|
||||||
SELECT leave_type FROM pf_leave_snapshots WHERE person_id = ?
|
|
||||||
) as t ORDER BY leave_type = 'CP' DESC, leave_type = 'JRA' DESC, leave_type = 'JA' DESC, leave_type ASC
|
|
||||||
");
|
|
||||||
$stmtLT->execute([$p['id'], $p['id']]);
|
|
||||||
$lTypes = $stmtLT->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
|
|
||||||
// Fallback de sécurité si aucun compteur n'est encore paramétré
|
|
||||||
if (empty($lTypes)) {
|
|
||||||
$lTypes = ['CP', 'JRA', 'JA'];
|
|
||||||
}
|
|
||||||
$p['leave_types'] = $lTypes;
|
|
||||||
|
|
||||||
$parents[] = $p;
|
$parents[] = $p;
|
||||||
} elseif ($role === 'enfant') {
|
} elseif ($role === 'enfant' || $role === 'child') {
|
||||||
// Décodage sécurisé des modes de garde
|
$p['modes'] = json_decode($p['care_modes'] ?? '[]', true) ?: [];
|
||||||
$modes = json_decode($p['care_modes'] ?? '[]', true);
|
|
||||||
if (!is_array($modes)) $modes = [];
|
|
||||||
$p['modes'] = $modes;
|
|
||||||
$kids[] = $p;
|
$kids[] = $p;
|
||||||
|
} elseif ($role === 'helper' || $role === 'nounou') {
|
||||||
foreach ($modes as $m) {
|
$helpers[] = $p; // 🟢 On stocke les intervenants
|
||||||
$activeCareModes[$m] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$activeCareModes = array_keys($activeCareModes); // On récupère uniquement les noms uniques
|
|
||||||
|
// 2. Modes de garde (globaux du foyer, pour synchroniser avec le JS)
|
||||||
|
$stmtFoyer = $pdo->query("SELECT care_modes FROM pf_foyer_settings LIMIT 1");
|
||||||
|
$foyerData = $stmtFoyer->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$activeCareModes = json_decode($foyerData['care_modes'] ?? '[]', true);
|
||||||
|
if (!is_array($activeCareModes)) $activeCareModes = [];
|
||||||
|
|
||||||
|
// 3. Matrice de congés dynamique (pour les en-têtes et la modale)
|
||||||
|
$stmtLeaves = $pdo->query("SELECT person_id, leave_type FROM pf_person_leave_meta ORDER BY id ASC");
|
||||||
|
$dbLeaves = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$leaveMatrix = [];
|
||||||
|
$allLeaveTypes = [];
|
||||||
|
foreach ($dbLeaves as $l) {
|
||||||
|
$leaveMatrix[$l['person_id']][] = $l['leave_type'];
|
||||||
|
$allLeaveTypes[$l['leave_type']] = true;
|
||||||
|
}
|
||||||
|
$allLeaveTypes = array_keys($allLeaveTypes);
|
||||||
|
|
||||||
$pageTitle = tr('fc_page_title');
|
$pageTitle = tr('fc_page_title');
|
||||||
$activePage = "family-calendar";
|
$activePage = "family-calendar";
|
||||||
@@ -67,9 +59,9 @@ require __DIR__ . '/header.php';
|
|||||||
<!-- Bloc Titre + Roue crantée -->
|
<!-- Bloc Titre + Roue crantée -->
|
||||||
<div style="display: flex; align-items: center; gap: 12px;">
|
<div style="display: flex; align-items: center; gap: 12px;">
|
||||||
<h1 style="margin: 0;"><?= tr('fc_main_header') ?></h1>
|
<h1 style="margin: 0;"><?= tr('fc_main_header') ?></h1>
|
||||||
<button id="btnOpenCalendarSettings" class="btn-settings-gear" title="<?= tr('fc_btn_settings') ?>">
|
<button id="btnOpenCalendarSettings" class="btn-settings-gear" title="Paramètres du calendrier" style="font-size: 1.4rem; padding: 4px;">
|
||||||
⚙️
|
⚙️
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Boutons d'actions -->
|
<!-- Boutons d'actions -->
|
||||||
@@ -124,7 +116,7 @@ require __DIR__ . '/header.php';
|
|||||||
<label class="pf-label"><?= tr('fc_label_person') ?></label>
|
<label class="pf-label"><?= tr('fc_label_person') ?></label>
|
||||||
<select id="snapPerson" class="pf-input" required>
|
<select id="snapPerson" class="pf-input" required>
|
||||||
<option value="" disabled selected>-- <?= tr('fc_choose_person') ?> --</option>
|
<option value="" disabled selected>-- <?= tr('fc_choose_person') ?> --</option>
|
||||||
<?php foreach ($familyPeople as $person): ?>
|
<?php foreach ($parents as $person): ?>
|
||||||
<option value="<?= htmlspecialchars($person['id']) ?>">
|
<option value="<?= htmlspecialchars($person['id']) ?>">
|
||||||
<?= htmlspecialchars($person['name']) ?>
|
<?= htmlspecialchars($person['name']) ?>
|
||||||
</option>
|
</option>
|
||||||
@@ -134,9 +126,9 @@ require __DIR__ . '/header.php';
|
|||||||
<div class="pf-form-group">
|
<div class="pf-form-group">
|
||||||
<label class="pf-label"><?= tr('fc_label_leave_type') ?></label>
|
<label class="pf-label"><?= tr('fc_label_leave_type') ?></label>
|
||||||
<select id="snapType" class="pf-input" required>
|
<select id="snapType" class="pf-input" required>
|
||||||
<option value="CP">CP</option>
|
<?php foreach ($allLeaveTypes as $type): ?>
|
||||||
<option value="JRA">JRA</option>
|
<option value="<?= htmlspecialchars($type) ?>"><?= htmlspecialchars($type) ?></option>
|
||||||
<option value="JA">JA</option>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,87 +154,58 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3. MODALE : PARAMÈTRES AVANCÉS DU CALENDRIER -->
|
|
||||||
<div id="modalCalendarSettings" class="pf-modal">
|
<div id="modalCalendarSettings" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 750px; width: 95%; padding: 0; overflow: hidden; display: flex; flex-direction: column;">
|
<div class="pf-modal-content" style="max-width: 600px; width: 90%;">
|
||||||
|
<div class="pf-modal-header">
|
||||||
<!-- HEADER -->
|
<h3 class="pf-modal-title">⚙️ <?= tr('fc_modal_settings_title') ?></h3>
|
||||||
<div class="pf-modal-header" style="padding: 1.2rem 1.5rem; border-bottom: 1px solid var(--border-light); background: var(--bg-panel);">
|
<button type="button" class="pf-modal-close" onclick="closeCalendarSettings()">×</button>
|
||||||
<h3 class="pf-modal-title" style="display: flex; align-items: center; gap: 8px; margin: 0;">
|
|
||||||
<span style="color: var(--text-muted); font-size: 1.2rem;">⚙️</span> <?= tr('fc_modal_settings_title') ?>
|
|
||||||
</h3>
|
|
||||||
<button type="button" class="pf-modal-close" style="margin-right: 1.5rem;" onclick="closeCalendarSettings()">×</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CORPS SCINDÉ EN DEUX (SIDEBAR / CONTENU) -->
|
<div style="display: flex; gap: 4px; background: var(--bg-page); padding: 4px; border-radius: 8px; margin: 1rem;">
|
||||||
<div style="display: flex; flex-direction: row; height: 60vh; min-height: 400px; background: var(--bg-panel);">
|
<button type="button" id="tab-btn-foyer" class="bs-tab-btn active" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('foyer')">🏡 Foyer</button>
|
||||||
|
<button type="button" id="tab-btn-membres" class="bs-tab-btn" style="flex: 1; padding: 8px 12px; font-weight: 600;" onclick="switchCalendarTab('membres')">👥 Membres & Droits</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- SIDEBAR GAUCHE -->
|
<div class="pf-modal-body" style="max-height: 60vh; overflow-y: auto; padding-top: 0;">
|
||||||
<div style="width: 220px; border-right: 1px solid var(--border-light); padding: 1rem 0; display: flex; flex-direction: column; gap: 4px;">
|
|
||||||
<button type="button" id="tab-btn-foyer" class="fc-sidebar-tab bs-tab-btn active" onclick="switchCalendarTab('foyer')">
|
<div id="cal-pane-foyer" class="cal-settings-pane">
|
||||||
<?= tr('fc_tab_foyer') ?>
|
<form id="formCalFoyer" onsubmit="submitCalFoyer(event)">
|
||||||
</button>
|
<div class="pf-form-group">
|
||||||
<button type="button" id="tab-btn-membres" class="fc-sidebar-tab bs-tab-btn" onclick="switchCalendarTab('membres')">
|
<label class="pf-label"><?= tr('fc_zone_label') ?></label>
|
||||||
<?= tr('fc_tab_members') ?>
|
<select id="setZoneScolaire" name="zone_scolaire" class="pf-input" required style="width: 100%;">
|
||||||
</button>
|
<option value="A">Zone A</option>
|
||||||
|
<option value="B">Zone B</option>
|
||||||
|
<option value="C">Zone C</option>
|
||||||
|
<option value="Autre">Autre / Hors France</option>
|
||||||
|
</select>
|
||||||
|
<small class="pf-muted-note"><?= tr('fc_zone_desc') ?></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group" style="margin-top: 1.5rem;">
|
||||||
|
<label class="pf-label"><?= tr('fc_care_modes_label') ?></label>
|
||||||
|
<div id="careModesContainer" style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; min-height: 38px; padding: 8px; background: var(--bg-page); border-radius: var(--radius); border: 1px solid var(--border-light);"></div>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<input type="text" id="inputNewCareMode" class="pf-input" placeholder="<?= tr('fc_add_mode_placeholder') ?>" style="flex: 1;">
|
||||||
|
<button type="button" class="btn btn-secondary" style="padding: 0 16px; border-radius: 6px;" onclick="addCareModeTag()">+</button>
|
||||||
|
</div>
|
||||||
|
<small class="pf-muted-note"><?= tr('fc_care_modes_desc') ?></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 2rem; display: flex; justify-content: flex-end; gap: 8px;">
|
||||||
|
<button type="button" class="pf-btn pf-btn-secondary" onclick="closeCalendarSettings()"><?= tr('btn_cancel') ?></button>
|
||||||
|
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('save') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CONTENU DROITE -->
|
<div id="cal-pane-membres" class="cal-settings-pane" style="display: none;">
|
||||||
<div class="pf-modal-body" style="flex: 1; padding: 1.5rem; overflow-y: auto;">
|
<div style="margin-bottom: 1.2rem;">
|
||||||
|
<label class="pf-label">Sélectionner un membre de la famille :</label>
|
||||||
<!-- PANE A : CONFIGURATION GÉNÉRALE DU FOYER -->
|
<select id="selectCalMember" class="pf-input" style="width: 100%; font-weight: 600;" onchange="loadMemberConfigView()"></select>
|
||||||
<div id="cal-pane-foyer" class="cal-settings-pane">
|
|
||||||
<div style="display: flex; align-items: center; margin-bottom: 1.5rem; border-bottom: 1px solid var(--border-light); padding-bottom: 0.8rem;">
|
|
||||||
<h4 style="margin: 0; font-size: 1.1rem; color: var(--text-main);"><?= tr('fc_tab_foyer') ?></h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="formCalFoyer" onsubmit="submitCalFoyer(event)">
|
|
||||||
<div class="pf-form-group" style="margin-bottom: 1.5rem;">
|
|
||||||
<label class="pf-label" style="font-weight: 600; margin-bottom: 6px;"><?= tr('fc_zone_label') ?></label>
|
|
||||||
<select id="setZoneScolaire" name="zone_scolaire" class="pf-input" required style="width: 100%; border-radius: 8px; background-color: var(--bg-page);">
|
|
||||||
<option value="A">Zone A</option>
|
|
||||||
<option value="B">Zone B</option>
|
|
||||||
<option value="C">Zone C</option>
|
|
||||||
<option value="Autre">Autre / Hors France</option>
|
|
||||||
</select>
|
|
||||||
<small class="pf-muted-note" style="margin-top: 4px; display: block;"><?= tr('fc_zone_desc') ?></small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pf-form-group">
|
|
||||||
<label class="pf-label" style="font-weight: 600; margin-bottom: 6px;"><?= tr('fc_care_modes_label') ?></label>
|
|
||||||
<div style="background: var(--bg-page); border: 1px solid var(--border-light); border-radius: 8px; padding: 8px;">
|
|
||||||
<div id="careModesContainer" style="display: flex; flex-wrap: wrap; gap: 6px; min-height: 32px; margin-bottom: 8px;"></div>
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
<input type="text" id="inputNewCareMode" class="pf-input" placeholder="<?= tr('fc_add_mode_placeholder') ?>" style="flex: 1; background: var(--bg-soft); border: none; border-radius: 6px; padding: 6px 12px;">
|
|
||||||
<button type="button" class="pf-btn pf-btn-secondary" style="padding: 0 14px; border-radius: 6px;" onclick="addCareModeTag()">➕</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small class="pf-muted-note" style="margin-top: 6px; display: block;"><?= tr('fc_care_modes_desc') ?></small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 2rem; display: flex; justify-content: flex-end; gap: 12px;">
|
|
||||||
<button type="button" class="pf-btn pf-btn-secondary" onclick="closeCalendarSettings()"><?= tr('btn_cancel') ?></button>
|
|
||||||
<button type="submit" class="pf-btn pf-btn-primary"><?= tr('save') ?></button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PANE B : CONFIGURATION DE LA MATRICE COMPTEURS -->
|
<div id="memberConfigZone" style="background: var(--bg-page); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); min-height: 100px;">
|
||||||
<div id="cal-pane-membres" class="cal-settings-pane" style="display: none;">
|
|
||||||
<div style="display: flex; align-items: center; margin-bottom: 1.5rem; border-bottom: 1px solid var(--border-light); padding-bottom: 0.8rem;">
|
|
||||||
<h4 style="margin: 0; font-size: 1.1rem; color: var(--text-main);"><?= tr('fc_tab_members') ?></h4>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 1.5rem;">
|
|
||||||
<label class="pf-label" style="font-weight: 600; margin-bottom: 6px;"><?= tr('fc_select_member') ?></label>
|
|
||||||
<select id="selectCalMember" class="pf-input" style="width: 100%; font-weight: 600; border-radius: 8px; background-color: var(--bg-page);" onchange="loadMemberConfigView()"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="memberConfigZone" style="background: var(--bg-page); padding: 16px; border-radius: 12px; border: 1px solid var(--border-light); min-height: 120px;">
|
|
||||||
<!-- Injecté à la volée en fonction du membre sélectionné -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -307,32 +270,36 @@ require __DIR__ . '/header.php';
|
|||||||
<th rowspan="3" class="col-total rotated-text"><span>Maladie <?= htmlspecialchars($kid['name']) ?></span></th>
|
<th rowspan="3" class="col-total rotated-text"><span>Maladie <?= htmlspecialchars($kid['name']) ?></span></th>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<!-- 🟢 NOUVEAU : Colspan dynamique selon le nombre de compteurs de chaque parent -->
|
|
||||||
<?php foreach ($parents as $index => $parent):
|
<?php foreach ($parents as $index => $parent):
|
||||||
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia'; // Couleurs alternées
|
||||||
$colspan = count($parent['leave_types']) * 2;
|
$pLeaves = $leaveMatrix[$parent['id']] ?? [];
|
||||||
|
$colspan = count($pLeaves) * 2;
|
||||||
?>
|
?>
|
||||||
|
<?php if ($colspan > 0): ?>
|
||||||
<th colspan="<?= $colspan ?>" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th>
|
<th colspan="<?= $colspan ?>" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th>
|
||||||
|
<?php endif; ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<!-- 🟢 NOUVEAU : Noms des compteurs dynamiques -->
|
|
||||||
<?php foreach ($parents as $index => $parent):
|
<?php foreach ($parents as $index => $parent):
|
||||||
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
||||||
foreach ($parent['leave_types'] as $lt):
|
$pLeaves = $leaveMatrix[$parent['id']] ?? [];
|
||||||
?>
|
?>
|
||||||
<th colspan="2" class="<?= $parentClass ?>-sub"><?= htmlspecialchars(strtoupper($lt)) ?></th>
|
<?php foreach ($pLeaves as $type): ?>
|
||||||
<?php endforeach; endforeach; ?>
|
<th colspan="2" class="<?= $parentClass ?>-sub"><?= htmlspecialchars($type) ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<!-- 🟢 NOUVEAU : Sous-colonnes Av./Use dynamiques -->
|
|
||||||
<?php foreach ($parents as $index => $parent):
|
<?php foreach ($parents as $index => $parent):
|
||||||
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
||||||
foreach ($parent['leave_types'] as $lt):
|
$pLeaves = $leaveMatrix[$parent['id']] ?? [];
|
||||||
?>
|
?>
|
||||||
|
<?php foreach ($pLeaves as $type): ?>
|
||||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
|
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-av"><?= tr('fc_col_av') ?></th>
|
||||||
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
|
<th class="<?= $parentClass ?>-sub <?= $parentClass ?>-use"><?= tr('fc_col_use') ?></th>
|
||||||
<?php endforeach; endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="planningBody"></tbody>
|
<tbody id="planningBody"></tbody>
|
||||||
@@ -342,49 +309,54 @@ require __DIR__ . '/header.php';
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="pf-section pf-section--bottom-panels" style="margin-top: 1.5rem;">
|
<section class="pf-section pf-section--bottom-panels" style="margin-top: 1.5rem;">
|
||||||
<div class="fc-bottom-grid">
|
<div class="pf-card">
|
||||||
<div class="pf-card">
|
|
||||||
<div class="pf-card-title fc-summary-header">
|
|
||||||
<span><?= tr('fc_summary_title') ?></span>
|
|
||||||
<div class="fc-summary-controls">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pf-card">
|
|
||||||
<h2 class="pf-card-title"><?= tr('fc_legend_title') ?></h2>
|
<h2 class="pf-card-title"><?= tr('fc_legend_title') ?></h2>
|
||||||
<div class="pf-card-body">
|
<div class="pf-card-body">
|
||||||
<div class="pf-legend-grid">
|
<div class="pf-legend-grid">
|
||||||
|
<!-- Fériés & Vacances (Fixes) -->
|
||||||
<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-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-public-holiday"></div><span><?= tr('leg_public_holiday') ?></span></div>
|
||||||
|
|
||||||
<?php foreach ($activeCareModes as $index => $mode):
|
<!-- Intervenants (Dynamique) -->
|
||||||
$hue = ($index * 137) % 360;
|
<?php foreach ($helpers as $helper): ?>
|
||||||
?>
|
<div class="pf-legend-item"><div class="pf-legend-color" style="background: var(--warning);"></div><span>Off <?= htmlspecialchars($helper['name']) ?></span></div>
|
||||||
<div class="pf-legend-item">
|
<div class="pf-legend-item"><div class="pf-legend-color" style="background: var(--danger);"></div><span>Extra <?= htmlspecialchars($helper['name']) ?></span></div>
|
||||||
<div class="pf-legend-color" style="background: hsl(<?= $hue ?>, 70%, 50%);"></div>
|
|
||||||
<span><?= htmlspecialchars($mode) ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<?php foreach ($kids as $kid): ?>
|
<!-- Modes de garde (Dynamique avec icônes) -->
|
||||||
|
<?php foreach ($activeCareModes as $index => $mode):
|
||||||
|
$modeLower = strtolower($mode);
|
||||||
|
if ($modeLower === 'avis'): ?>
|
||||||
|
<div class="pf-legend-item">
|
||||||
|
<img src="/modules/family-calendar/assets/img/avis.svg" style="width:16px;height:16px;object-fit:contain;margin-right:6px;" alt="Avis">
|
||||||
|
<span><?= htmlspecialchars($mode) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($modeLower === 'centre'): ?>
|
||||||
|
<div class="pf-legend-item">
|
||||||
|
<span style="font-size:1.1rem; line-height:1; margin-right:6px;">🏫</span>
|
||||||
|
<span><?= htmlspecialchars($mode) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php else:
|
||||||
|
$hue = ($index * 137) % 360; ?>
|
||||||
|
<div class="pf-legend-item">
|
||||||
|
<div class="pf-legend-color" style="background: hsl(<?= $hue ?>, 70%, 50%);"></div>
|
||||||
|
<span><?= htmlspecialchars($mode) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif;
|
||||||
|
endforeach; ?>
|
||||||
|
|
||||||
|
<!-- Enfants Malades (Dynamique avec couleur BDD) -->
|
||||||
|
<?php foreach ($kids as $kid):
|
||||||
|
$color = !empty($kid['color']) ? $kid['color'] : 'var(--danger)';
|
||||||
|
?>
|
||||||
<div class="pf-legend-item">
|
<div class="pf-legend-item">
|
||||||
<div class="pf-legend-color" style="background: var(--pf-danger, #ef4444); opacity: 0.8;"></div>
|
<div class="pf-legend-color" style="background: <?= htmlspecialchars($color) ?>; opacity: 0.8;"></div>
|
||||||
<span>Maladie <?= htmlspecialchars($kid['name']) ?></span>
|
<span>Maladie <?= htmlspecialchars($kid['name']) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+107
-2
@@ -216,8 +216,8 @@ return [
|
|||||||
'fc_tab_foyer' => '🏡 Llar',
|
'fc_tab_foyer' => '🏡 Llar',
|
||||||
'fc_tab_members' => '👥 Membres i Drets',
|
'fc_tab_members' => '👥 Membres i Drets',
|
||||||
'fc_select_member' => 'Selecciona un membre de la família:',
|
'fc_select_member' => 'Selecciona un membre de la família:',
|
||||||
'fc_role_adult' => '👨👩👦 Adult',
|
'fc_role_adult' => 'Adult',
|
||||||
'fc_role_child' => '👶 Nen',
|
'fc_role_child' => 'Nen',
|
||||||
'fc_care_modes_title' => '👶 Modes de cura associats',
|
'fc_care_modes_title' => '👶 Modes de cura associats',
|
||||||
'fc_care_modes_desc' => 'Marca els modes de cura que s\'apliquen a aquest nen.',
|
'fc_care_modes_desc' => 'Marca els modes de cura que s\'apliquen a aquest nen.',
|
||||||
'fc_no_care_mode' => 'Cap mode configurat.',
|
'fc_no_care_mode' => 'Cap mode configurat.',
|
||||||
@@ -237,6 +237,48 @@ return [
|
|||||||
'fc_care_modes_desc' => 'Gestiona la llista global de modes de cura possibles per a la família.',
|
'fc_care_modes_desc' => 'Gestiona la llista global de modes de cura possibles per a la família.',
|
||||||
'fc_add_mode_placeholder' => 'Ex: Mainadera, Casal, Avis...',
|
'fc_add_mode_placeholder' => 'Ex: Mainadera, Casal, Avis...',
|
||||||
'fc_settings_updated' => 'Configuració actualitzada correctament!',
|
'fc_settings_updated' => 'Configuració actualitzada correctament!',
|
||||||
|
'fc_first_day' => 'Primer dia de la setmana',
|
||||||
|
// --- CALENDARI : PARÀMETRES (MEMBRES I VISUALITZACIÓ) ---
|
||||||
|
'fc_tab_display' => '🖥️ Visualització',
|
||||||
|
'fc_display_title' => 'Ajustos de visualització',
|
||||||
|
'fc_display_desc' => 'Personalitza l\'aparença i el comportament predeterminat del teu calendari.',
|
||||||
|
'fc_default_view' => 'Vista per defecte',
|
||||||
|
'fc_view_month' => '1 mes',
|
||||||
|
'fc_view_2months' => '2 mesos',
|
||||||
|
'fc_view_3months' => '3 mesos',
|
||||||
|
'fc_working_hours' => 'Horari de cura (Per defecte)',
|
||||||
|
'fc_working_hours_desc' => 'Exemple: 08:30-18:00',
|
||||||
|
|
||||||
|
'fc_role_unknown' => 'Desconegut',
|
||||||
|
'fc_role_adult' => 'Adult',
|
||||||
|
'fc_role_child' => 'Nen/a',
|
||||||
|
'fc_role_helper' => 'Ajudant',
|
||||||
|
'fc_error' => 'Error: ',
|
||||||
|
|
||||||
|
'fc_care_modes_title' => 'Modes de Cura Predeterminats',
|
||||||
|
'fc_care_modes_desc' => 'Quins modes de cura s\'apliquen a aquest nen/a?',
|
||||||
|
'fc_no_care_mode' => 'No hi ha modes de cura configurats a la llar.',
|
||||||
|
'fc_child_saved' => 'Desat!',
|
||||||
|
|
||||||
|
'fc_matrix_title' => 'Matriu de Permisos',
|
||||||
|
'fc_matrix_desc' => 'Defineix les quotes i dates de renovació.',
|
||||||
|
'fc_matrix_empty' => 'No hi ha comptadors definits.',
|
||||||
|
'fc_matrix_saved' => 'Matriu desada!',
|
||||||
|
|
||||||
|
'fc_leave_obsolete' => 'Obsolet',
|
||||||
|
'fc_leave_code_placeholder' => 'Codi...',
|
||||||
|
'fc_leave_method_fixed' => 'Fix',
|
||||||
|
'fc_leave_method_accumulated' => 'Acumulat',
|
||||||
|
|
||||||
|
'fc_date_format_placeholder' => 'DD/MM',
|
||||||
|
'fc_date_format_title' => 'Format DD/MM',
|
||||||
|
|
||||||
|
'fc_table_code' => 'Codi',
|
||||||
|
'fc_table_method' => 'Mètode',
|
||||||
|
'fc_table_quota' => 'Quota',
|
||||||
|
'fc_table_renewal' => 'Renov.',
|
||||||
|
|
||||||
|
'btn_save_rights' => 'Guardar',
|
||||||
|
|
||||||
'leg_presence' => 'Presència Pep',
|
'leg_presence' => 'Presència Pep',
|
||||||
'leg_school_holidays' => 'Vacances Escolars',
|
'leg_school_holidays' => 'Vacances Escolars',
|
||||||
@@ -461,6 +503,36 @@ return [
|
|||||||
'bs_currency_label' => 'Divisa de la llar',
|
'bs_currency_label' => 'Divisa de la llar',
|
||||||
'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.',
|
'bs_currency_desc' => 'Divisa principal utilitzada per mostrar els vostres comptes i pressupostos.',
|
||||||
'bs_currency_updated' => 'Divisa actualitzada correctament!',
|
'bs_currency_updated' => 'Divisa actualitzada correctament!',
|
||||||
|
// ONGLET CSV MAPPING
|
||||||
|
'bs_csv_title' => 'Format de la importació CSV',
|
||||||
|
'bs_csv_desc' => 'Configureu aquí les columnes que corresponen a l\'exportació del vostre banc.',
|
||||||
|
'bs_csv_delimiter' => 'Separador',
|
||||||
|
'bs_csv_delim_semi' => 'Punt i coma (;)',
|
||||||
|
'bs_csv_delim_comma' => 'Coma (,)',
|
||||||
|
'bs_csv_delim_tab' => 'Tabulació',
|
||||||
|
'bs_csv_date_format' => 'Format de la Data',
|
||||||
|
'bs_csv_col_index' => 'Índex de les columnes',
|
||||||
|
'bs_csv_col_index_help' => '(Atenció: la 1a columna és la Número 0)',
|
||||||
|
'bs_csv_col_date' => 'Núm Col: Data',
|
||||||
|
'bs_csv_col_label' => 'Núm Col: Concepte / Motiu',
|
||||||
|
'bs_csv_amount_mgmt' => 'Gestió dels imports',
|
||||||
|
'bs_csv_amount_single' => 'Una sola columna (Positiu = Ingrés, Negatiu = Despesa)',
|
||||||
|
'bs_csv_amount_split' => 'Dues columnes separades (Dèbit / Crèdit)',
|
||||||
|
'bs_csv_col_amount' => 'Núm Col: Import',
|
||||||
|
'bs_csv_col_debit' => 'Núm Col: Dèbit',
|
||||||
|
'bs_csv_col_credit' => 'Núm Col: Crèdit',
|
||||||
|
'bs_csv_col_ref' => 'Núm Col: Referència única (Opcional)',
|
||||||
|
'btn_save_format' => 'Desar el format',
|
||||||
|
'bs_csv_saved' => 'Format CSV desat',
|
||||||
|
'bs_tab_csv' => '📥 Importació CSV',
|
||||||
|
'bs_csv_settings_title' => 'Configuració del format',
|
||||||
|
'bs_csv_drop_title' => 'Arrossegueu el vostre fitxer CSV aquí',
|
||||||
|
'bs_csv_drop_desc' => 'o feu clic per cercar fitxers',
|
||||||
|
'bs_csv_err_type' => 'Error: Només s\'admeten fitxers .csv.',
|
||||||
|
'bs_csv_file_ok' => 'Fitxer carregat',
|
||||||
|
'bs_csv_preview_title' => 'Previsualització de les vostres columnes',
|
||||||
|
'bs_csv_empty_col' => 'Buit',
|
||||||
|
'bs_csv_no_data' => 'No s\'ha trobat cap línia de transacció en aquest fitxer.',
|
||||||
|
|
||||||
// --- BUDGET : SUIVI ---
|
// --- BUDGET : SUIVI ---
|
||||||
'bud_rem_school' => 'Escola (Resta estimada)',
|
'bud_rem_school' => 'Escola (Resta estimada)',
|
||||||
@@ -605,6 +677,39 @@ return [
|
|||||||
'bud_sav_prompt_duplicate' => "Vols copiar les dades de %s a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:",
|
'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_server' => 'Error del servidor: ',
|
||||||
'bud_err_network_dup' => 'Error de xarxa en duplicar.',
|
'bud_err_network_dup' => 'Error de xarxa en duplicar.',
|
||||||
|
'budget_tab_kids' => 'Infants 👶',
|
||||||
|
'bud_sav_add_one_month' => '+1 Mes',
|
||||||
|
'bud_sav_add_month' => 'Afegir un mes',
|
||||||
|
'bud_sav_no_data' => 'Cap dada d\'estalvi per a %s.',
|
||||||
|
'bud_sav_post_month' => 'Concepte / Mes',
|
||||||
|
'bud_sav_from_date' => 'Des del %s',
|
||||||
|
'bud_sav_edit_modal' => 'Editar aquest mes',
|
||||||
|
'bud_sav_delete_month' => 'Suprimir aquest mes',
|
||||||
|
'bud_sav_total_bank' => 'TOTAL BANC',
|
||||||
|
'bud_sav_extra' => 'NO ASSIGNAT (EXTRES)',
|
||||||
|
'bud_sav_modal_title_add' => 'Afegir un mes',
|
||||||
|
'bud_sav_modal_title_edit' => 'Editar',
|
||||||
|
'bud_sav_month_concerned' => 'Mes afectat',
|
||||||
|
'bud_sav_total_bank_eur' => 'Total al Banc (€)',
|
||||||
|
'bud_sav_ventilation' => 'Desglossament de comptes',
|
||||||
|
'bud_sav_adj_help' => 'Utilitzeu l\'ajust (+/-) per recalcular automàticament.',
|
||||||
|
'bud_sav_add_line' => 'Afegir una línia',
|
||||||
|
'bud_category' => 'Categoria',
|
||||||
|
'bud_sav_current' => 'Actual',
|
||||||
|
'bud_sav_adjust' => 'Ajust',
|
||||||
|
'bud_sav_new' => 'Nou',
|
||||||
|
'bud_sav_sum_mode_title' => 'Activar/Desactivar la calculadora',
|
||||||
|
'bud_sav_selection' => 'Selecció:',
|
||||||
|
'bud_sav_ph_name' => 'Nom del compte',
|
||||||
|
|
||||||
|
// JS & Errors
|
||||||
|
'bud_sav_confirm_delete_month' => 'Estàs segur que vols suprimir totes les dades de %m per a %o?',
|
||||||
|
'bud_sav_prompt_duplicate' => "Vols duplicar les dades de %s cap a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:",
|
||||||
|
'bud_err_tech' => 'S\'ha produït un error tècnic.',
|
||||||
|
'bud_err_server' => 'Error del servidor: ',
|
||||||
|
'bud_err_network_dup' => 'Error de xarxa en duplicar.',
|
||||||
|
'bud_sav_saving' => 'Desant...',
|
||||||
|
'bud_err_delete' => 'Error en suprimir.',
|
||||||
|
|
||||||
// --- BUDGET : PREVISIONNEL ---
|
// --- BUDGET : PREVISIONNEL ---
|
||||||
'bud_prev_incomes' => 'Ingressos',
|
'bud_prev_incomes' => 'Ingressos',
|
||||||
|
|||||||
+108
-3
@@ -217,9 +217,9 @@ return [
|
|||||||
'fc_tab_foyer' => '🏡 Household',
|
'fc_tab_foyer' => '🏡 Household',
|
||||||
'fc_tab_members' => '👥 Members & Rights',
|
'fc_tab_members' => '👥 Members & Rights',
|
||||||
'fc_select_member' => 'Select a family member:',
|
'fc_select_member' => 'Select a family member:',
|
||||||
'fc_role_adult' => '👨👩👦 Adult',
|
'fc_role_adult' => 'Adult',
|
||||||
'fc_role_child' => '👶 Child',
|
'fc_role_child' => 'Child',
|
||||||
'fc_care_modes_title' => '👶 Associated Care Modes',
|
'fc_care_modes_title' => 'Associated Care Modes',
|
||||||
'fc_care_modes_desc' => 'Check the care modes that apply to this child.',
|
'fc_care_modes_desc' => 'Check the care modes that apply to this child.',
|
||||||
'fc_no_care_mode' => 'No care mode configured.',
|
'fc_no_care_mode' => 'No care mode configured.',
|
||||||
'fc_matrix_title' => '👨👩👦 Leave Rights Matrix',
|
'fc_matrix_title' => '👨👩👦 Leave Rights Matrix',
|
||||||
@@ -238,6 +238,48 @@ return [
|
|||||||
'fc_care_modes_desc' => 'Manage the global list of possible care modes for the family.',
|
'fc_care_modes_desc' => 'Manage the global list of possible care modes for the family.',
|
||||||
'fc_add_mode_placeholder' => 'E.g., Nanny, Center, Grandparents...',
|
'fc_add_mode_placeholder' => 'E.g., Nanny, Center, Grandparents...',
|
||||||
'fc_settings_updated' => 'Settings updated successfully!',
|
'fc_settings_updated' => 'Settings updated successfully!',
|
||||||
|
'fc_first_day' => 'First day of the week',
|
||||||
|
// --- CALENDAR: SETTINGS (MEMBERS & DISPLAY) ---
|
||||||
|
'fc_tab_display' => '🖥️ Display',
|
||||||
|
'fc_display_title' => 'Display Settings',
|
||||||
|
'fc_display_desc' => 'Customize the default appearance and behavior of your calendar.',
|
||||||
|
'fc_default_view' => 'Default View',
|
||||||
|
'fc_view_month' => '1 month',
|
||||||
|
'fc_view_2months' => '2 months',
|
||||||
|
'fc_view_3months' => '3 months',
|
||||||
|
'fc_working_hours' => 'Default Care Hours',
|
||||||
|
'fc_working_hours_desc' => 'Example: 08:30-18:00',
|
||||||
|
|
||||||
|
'fc_role_unknown' => 'Unknown',
|
||||||
|
'fc_role_adult' => 'Adult',
|
||||||
|
'fc_role_child' => 'Child',
|
||||||
|
'fc_role_helper' => 'Helper',
|
||||||
|
'fc_error' => 'Error: ',
|
||||||
|
|
||||||
|
'fc_care_modes_title' => 'Default Care Modes',
|
||||||
|
'fc_care_modes_desc' => 'Which care modes apply to this child?',
|
||||||
|
'fc_no_care_mode' => 'No care mode configured for this household.',
|
||||||
|
'fc_child_saved' => 'Saved!',
|
||||||
|
|
||||||
|
'fc_matrix_title' => 'Leave Matrix',
|
||||||
|
'fc_matrix_desc' => 'Define quotas and renewal dates.',
|
||||||
|
'fc_matrix_empty' => 'No counters defined.',
|
||||||
|
"fc_matrix_saved" => "Matrix saved!",
|
||||||
|
|
||||||
|
'fc_leave_obsolete' => 'Obsolete',
|
||||||
|
'fc_leave_code_placeholder' => 'Code...',
|
||||||
|
'fc_leave_method_fixed' => 'Fixed',
|
||||||
|
'fc_leave_method_accumulated' => 'Accrued',
|
||||||
|
|
||||||
|
'fc_date_format_placeholder' => 'DD/MM',
|
||||||
|
'fc_date_format_title' => 'DD/MM Format',
|
||||||
|
|
||||||
|
'fc_table_code' => 'Code',
|
||||||
|
'fc_table_method' => 'Method',
|
||||||
|
'fc_table_quota' => 'Quota',
|
||||||
|
'fc_table_renewal' => 'Renewal',
|
||||||
|
|
||||||
|
'btn_save_rights' => 'Save',
|
||||||
|
|
||||||
'leg_presence' => 'Pep presence',
|
'leg_presence' => 'Pep presence',
|
||||||
'leg_school_holidays' => 'School holidays',
|
'leg_school_holidays' => 'School holidays',
|
||||||
@@ -459,6 +501,36 @@ return [
|
|||||||
'bs_currency_label' => 'Household Currency',
|
'bs_currency_label' => 'Household Currency',
|
||||||
'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.',
|
'bs_currency_desc' => 'Main currency used for displaying your accounts and budgets.',
|
||||||
'bs_currency_updated' => 'Currency updated successfully!',
|
'bs_currency_updated' => 'Currency updated successfully!',
|
||||||
|
// ONGLET CSV MAPPING
|
||||||
|
'bs_csv_title' => 'CSV Import Format',
|
||||||
|
'bs_csv_desc' => 'Configure the columns that match your bank\'s export file.',
|
||||||
|
'bs_csv_delimiter' => 'Delimiter',
|
||||||
|
'bs_csv_delim_semi' => 'Semicolon (;)',
|
||||||
|
'bs_csv_delim_comma' => 'Comma (,)',
|
||||||
|
'bs_csv_delim_tab' => 'Tab',
|
||||||
|
'bs_csv_date_format' => 'Date Format',
|
||||||
|
'bs_csv_col_index' => 'Column Indexes',
|
||||||
|
'bs_csv_col_index_help' => '(Note: the 1st column is Number 0)',
|
||||||
|
'bs_csv_col_date' => 'Col #: Date',
|
||||||
|
'bs_csv_col_label' => 'Col #: Label / Description',
|
||||||
|
'bs_csv_amount_mgmt' => 'Amount Management',
|
||||||
|
'bs_csv_amount_single' => 'Single column (Positive = Income, Negative = Expense)',
|
||||||
|
'bs_csv_amount_split' => 'Two separate columns (Debit / Credit)',
|
||||||
|
'bs_csv_col_amount' => 'Col #: Amount',
|
||||||
|
'bs_csv_col_debit' => 'Col #: Debit',
|
||||||
|
'bs_csv_col_credit' => 'Col #: Credit',
|
||||||
|
'bs_csv_col_ref' => 'Col #: Unique Reference (Optional)',
|
||||||
|
'btn_save_format' => 'Save format',
|
||||||
|
'bs_csv_saved' => 'CSV format saved',
|
||||||
|
'bs_tab_csv' => '📥 CSV Import',
|
||||||
|
'bs_csv_settings_title' => 'Format Configuration',
|
||||||
|
'bs_csv_drop_title' => 'Drop your CSV file here',
|
||||||
|
'bs_csv_drop_desc' => 'or click to browse your files',
|
||||||
|
'bs_csv_err_type' => 'Error: Only .csv files are allowed.',
|
||||||
|
'bs_csv_file_ok' => 'File loaded',
|
||||||
|
'bs_csv_preview_title' => 'Columns Preview',
|
||||||
|
'bs_csv_empty_col' => 'Empty',
|
||||||
|
'bs_csv_no_data' => 'No transaction row found in this file.',
|
||||||
|
|
||||||
// --- BUDGET: TRACKING ---
|
// --- BUDGET: TRACKING ---
|
||||||
'bud_rem_school' => 'School (estimated remaining)',
|
'bud_rem_school' => 'School (estimated remaining)',
|
||||||
@@ -588,6 +660,39 @@ return [
|
|||||||
'bud_sav_prompt_duplicate' => "Do you want to copy data from %s to %t1?\n\nEnter the new TOTAL bank balance (€) for %t2:",
|
'bud_sav_prompt_duplicate' => "Do you want to copy data from %s to %t1?\n\nEnter the new TOTAL bank balance (€) for %t2:",
|
||||||
'bud_err_server' => 'Server error: ',
|
'bud_err_server' => 'Server error: ',
|
||||||
'bud_err_network_dup' => 'Network error during duplication.',
|
'bud_err_network_dup' => 'Network error during duplication.',
|
||||||
|
'budget_tab_kids' => 'Kids 👶',
|
||||||
|
'bud_sav_add_one_month' => '+1 Month',
|
||||||
|
'bud_sav_add_month' => 'Add a month',
|
||||||
|
'bud_sav_no_data' => 'No savings data for %s.',
|
||||||
|
'bud_sav_post_month' => 'Item / Month',
|
||||||
|
'bud_sav_from_date' => 'From %s',
|
||||||
|
'bud_sav_edit_modal' => 'Edit this month',
|
||||||
|
'bud_sav_delete_month' => 'Delete this month',
|
||||||
|
'bud_sav_total_bank' => 'TOTAL BANK',
|
||||||
|
'bud_sav_extra' => 'UNALLOCATED (EXTRAS)',
|
||||||
|
'bud_sav_modal_title_add' => 'Add a month',
|
||||||
|
'bud_sav_modal_title_edit' => 'Edit',
|
||||||
|
'bud_sav_month_concerned' => 'Target month',
|
||||||
|
'bud_sav_total_bank_eur' => 'Total in Bank (€)',
|
||||||
|
'bud_sav_ventilation' => 'Accounts breakdown',
|
||||||
|
'bud_sav_adj_help' => 'Enter adjustments (+/-) to auto-calculate.',
|
||||||
|
'bud_sav_add_line' => 'Add line',
|
||||||
|
'bud_category' => 'Category',
|
||||||
|
'bud_sav_current' => 'Current',
|
||||||
|
'bud_sav_adjust' => 'Adjustment',
|
||||||
|
'bud_sav_new' => 'New',
|
||||||
|
'bud_sav_sum_mode_title' => 'Toggle calculator',
|
||||||
|
'bud_sav_selection' => 'Selection:',
|
||||||
|
'bud_sav_ph_name' => 'Account name',
|
||||||
|
|
||||||
|
// JS & Errors
|
||||||
|
'bud_sav_confirm_delete_month' => 'Are you sure you want to delete all data of %m for %o?',
|
||||||
|
'bud_sav_prompt_duplicate' => "Do you want to duplicate data from %s to %t1?\n\nEnter the new BANK TOTAL (€) for %t2:",
|
||||||
|
'bud_err_tech' => 'A technical error occurred.',
|
||||||
|
'bud_err_server' => 'Server error: ',
|
||||||
|
'bud_err_network_dup' => 'Network error during duplication.',
|
||||||
|
'bud_sav_saving' => 'Saving...',
|
||||||
|
'bud_err_delete' => 'Error during deletion.',
|
||||||
|
|
||||||
// --- BUDGET: FORECAST ---
|
// --- BUDGET: FORECAST ---
|
||||||
'bud_prev_incomes' => 'Income',
|
'bud_prev_incomes' => 'Income',
|
||||||
|
|||||||
+109
-3
@@ -219,9 +219,9 @@ return [
|
|||||||
'fc_tab_foyer' => '🏡 Foyer',
|
'fc_tab_foyer' => '🏡 Foyer',
|
||||||
'fc_tab_members' => '👥 Membres & Droits',
|
'fc_tab_members' => '👥 Membres & Droits',
|
||||||
'fc_select_member' => 'Sélectionner un membre de la famille :',
|
'fc_select_member' => 'Sélectionner un membre de la famille :',
|
||||||
'fc_role_adult' => '👨👩👦 Adulte',
|
'fc_role_adult' => 'Adulte',
|
||||||
'fc_role_child' => '👶 Enfant',
|
'fc_role_child' => 'Enfant',
|
||||||
'fc_care_modes_title' => '👶 Modes de garde associés',
|
'fc_care_modes_title' => 'Modes de garde associés',
|
||||||
'fc_care_modes_desc' => 'Cochez les modes de garde qui concernent cet enfant.',
|
'fc_care_modes_desc' => 'Cochez les modes de garde qui concernent cet enfant.',
|
||||||
'fc_no_care_mode' => 'Aucun mode configuré.',
|
'fc_no_care_mode' => 'Aucun mode configuré.',
|
||||||
'fc_matrix_title' => '👨👩👦 Matrice des Droits aux Congés',
|
'fc_matrix_title' => '👨👩👦 Matrice des Droits aux Congés',
|
||||||
@@ -240,6 +240,49 @@ return [
|
|||||||
'fc_care_modes_desc' => 'Gérez la liste globale des modes de garde possibles pour la famille.',
|
'fc_care_modes_desc' => 'Gérez la liste globale des modes de garde possibles pour la famille.',
|
||||||
'fc_add_mode_placeholder' => 'Ex: Nounou, Casal, Grands-parents...',
|
'fc_add_mode_placeholder' => 'Ex: Nounou, Casal, Grands-parents...',
|
||||||
'fc_settings_updated' => 'Paramètres mis à jour avec succès !',
|
'fc_settings_updated' => 'Paramètres mis à jour avec succès !',
|
||||||
|
'fc_first_day' => 'Premier jour de la semaine',
|
||||||
|
// --- CALENDRIER : PARAMÈTRES (MEMBRES & AFFICHAGE) ---
|
||||||
|
'fc_tab_display' => '🖥️ Affichage',
|
||||||
|
'fc_display_title' => 'Paramètres d\'affichage',
|
||||||
|
'fc_display_desc' => 'Personnalisez l\'apparence et le comportement par défaut de votre calendrier.',
|
||||||
|
'fc_default_view' => 'Vue par défaut',
|
||||||
|
'fc_view_month' => '1 mois',
|
||||||
|
'fc_view_2months' => '2 mois',
|
||||||
|
'fc_view_3months' => '3 mois',
|
||||||
|
'fc_working_hours' => 'Horaires de garde (Défaut)',
|
||||||
|
'fc_working_hours_desc' => 'Exemple : 08:30-18:00',
|
||||||
|
|
||||||
|
'fc_role_unknown' => 'Inconnu',
|
||||||
|
'fc_role_adult' => 'Adulte',
|
||||||
|
'fc_role_child' => 'Enfant',
|
||||||
|
'fc_role_helper' => 'Intervenant',
|
||||||
|
'fc_error' => 'Erreur : ',
|
||||||
|
|
||||||
|
'fc_care_modes_title' => 'Modes de Garde Par Défaut',
|
||||||
|
'fc_care_modes_desc' => 'Quels modes de garde s\'appliquent à cet enfant ?',
|
||||||
|
'fc_no_care_mode' => 'Aucun mode de garde configuré dans le foyer.',
|
||||||
|
'fc_child_saved' => 'Sauvegardé !',
|
||||||
|
|
||||||
|
'fc_matrix_title' => 'Matrice des Congés',
|
||||||
|
'fc_matrix_desc' => 'Définissez les quotas et la date de renouvellement.',
|
||||||
|
'fc_matrix_empty' => 'Aucun compteur défini.',
|
||||||
|
'fc_matrix_saved' => 'Matrice enregistrée !',
|
||||||
|
|
||||||
|
'fc_leave_obsolete' => 'Obsolète',
|
||||||
|
'fc_leave_code_placeholder' => 'Code...',
|
||||||
|
'fc_leave_method_fixed' => 'Fixe',
|
||||||
|
'fc_leave_method_accumulated' => 'Cumul',
|
||||||
|
|
||||||
|
'fc_date_format_placeholder' => 'JJ/MM',
|
||||||
|
'fc_date_format_title' => 'Format JJ/MM',
|
||||||
|
|
||||||
|
'fc_table_code' => 'Code',
|
||||||
|
'fc_table_method' => 'Méthode',
|
||||||
|
'fc_table_quota' => 'Quota',
|
||||||
|
'fc_table_renewal' => 'Renouv.',
|
||||||
|
|
||||||
|
'btn_save_rights' => 'Enregistrer',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'leg_presence' => 'Présence Pep',
|
'leg_presence' => 'Présence Pep',
|
||||||
@@ -462,6 +505,36 @@ return [
|
|||||||
'bs_currency_label' => 'Devise du Foyer',
|
'bs_currency_label' => 'Devise du Foyer',
|
||||||
'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.',
|
'bs_currency_desc' => 'Devise principale utilisée pour l\'affichage de vos comptes et budgets.',
|
||||||
'bs_currency_updated' => 'Devise mise à jour avec succès !',
|
'bs_currency_updated' => 'Devise mise à jour avec succès !',
|
||||||
|
// ONGLET CSV MAPPING
|
||||||
|
'bs_csv_title' => 'Format de l\'import CSV',
|
||||||
|
'bs_csv_desc' => 'Configurez ici les colonnes correspondantes à l\'export de votre banque.',
|
||||||
|
'bs_csv_delimiter' => 'Séparateur',
|
||||||
|
'bs_csv_delim_semi' => 'Point-virgule (;)',
|
||||||
|
'bs_csv_delim_comma' => 'Virgule (,)',
|
||||||
|
'bs_csv_delim_tab' => 'Tabulation',
|
||||||
|
'bs_csv_date_format' => 'Format de la Date',
|
||||||
|
'bs_csv_col_index' => 'Index des colonnes',
|
||||||
|
'bs_csv_col_index_help' => '(Attention : la 1ère colonne est la Numéro 0)',
|
||||||
|
'bs_csv_col_date' => 'N° Col : Date',
|
||||||
|
'bs_csv_col_label' => 'N° Col : Libellé / Motif',
|
||||||
|
'bs_csv_amount_mgmt' => 'Gestion des montants',
|
||||||
|
'bs_csv_amount_single' => 'Une seule colonne (Positif = Revenu, Négatif = Dépense)',
|
||||||
|
'bs_csv_amount_split' => 'Deux colonnes séparées (Débit / Crédit)',
|
||||||
|
'bs_csv_col_amount' => 'N° Col : Montant',
|
||||||
|
'bs_csv_col_debit' => 'N° Col : Débit',
|
||||||
|
'bs_csv_col_credit' => 'N° Col : Crédit',
|
||||||
|
'bs_csv_col_ref' => 'N° Col : Référence unique (Optionnel)',
|
||||||
|
'btn_save_format' => 'Enregistrer le format',
|
||||||
|
'bs_csv_saved' => 'Format CSV enregistré',
|
||||||
|
'bs_tab_csv' => '📥 Import CSV',
|
||||||
|
'bs_csv_settings_title' => 'Configuration du format',
|
||||||
|
'bs_csv_drop_title' => 'Glissez votre fichier CSV ici',
|
||||||
|
'bs_csv_drop_desc' => 'ou cliquez pour parcourir vos fichiers',
|
||||||
|
'bs_csv_err_type' => 'Erreur : Seuls les fichiers .csv sont autorisés.',
|
||||||
|
'bs_csv_file_ok' => 'Fichier chargé',
|
||||||
|
'bs_csv_preview_title' => 'Aperçu de vos colonnes',
|
||||||
|
'bs_csv_empty_col' => 'Vide',
|
||||||
|
'bs_csv_no_data' => 'Aucune ligne de transaction trouvée dans ce fichier.',
|
||||||
|
|
||||||
// --- BUDGET : SUIVI ---
|
// --- BUDGET : SUIVI ---
|
||||||
'bud_rem_school' => 'École (Reste estimé)',
|
'bud_rem_school' => 'École (Reste estimé)',
|
||||||
@@ -602,6 +675,39 @@ return [
|
|||||||
'bud_sav_prompt_duplicate' => "Voulez-vous copier les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
|
'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_server' => 'Erreur serveur : ',
|
||||||
'bud_err_network_dup' => 'Erreur réseau lors de la duplication.',
|
'bud_err_network_dup' => 'Erreur réseau lors de la duplication.',
|
||||||
|
'budget_tab_kids' => 'Enfants 👶',
|
||||||
|
'bud_sav_add_one_month' => '+1 Mois',
|
||||||
|
'bud_sav_add_month' => 'Ajouter un mois',
|
||||||
|
'bud_sav_no_data' => 'Aucune donnée d\'épargne pour %s.',
|
||||||
|
'bud_sav_post_month' => 'Poste / Mois',
|
||||||
|
'bud_sav_from_date' => 'Dès le %s',
|
||||||
|
'bud_sav_edit_modal' => 'Éditer ce mois',
|
||||||
|
'bud_sav_delete_month' => 'Supprimer ce mois',
|
||||||
|
'bud_sav_total_bank' => 'TOTAL BANQUE',
|
||||||
|
'bud_sav_extra' => 'NON ALLOUÉ (EXTRAS)',
|
||||||
|
'bud_sav_modal_title_add' => 'Ajouter un mois',
|
||||||
|
'bud_sav_modal_title_edit' => 'Éditer',
|
||||||
|
'bud_sav_month_concerned' => 'Mois concerné',
|
||||||
|
'bud_sav_total_bank_eur' => 'Total en Banque (€)',
|
||||||
|
'bud_sav_ventilation' => 'Ventilation des comptes',
|
||||||
|
'bud_sav_adj_help' => 'Saisissez les ajustements (+/-) pour recalculer automatiquement.',
|
||||||
|
'bud_sav_add_line' => 'Ajouter une ligne',
|
||||||
|
'bud_category' => 'Catégorie',
|
||||||
|
'bud_sav_current' => 'Actuel',
|
||||||
|
'bud_sav_adjust' => 'Ajustement',
|
||||||
|
'bud_sav_new' => 'Nouveau',
|
||||||
|
'bud_sav_sum_mode_title' => 'Activer/Désactiver la calculatrice',
|
||||||
|
'bud_sav_selection' => 'Sélection :',
|
||||||
|
'bud_sav_ph_name' => 'Nom du compte',
|
||||||
|
|
||||||
|
// JS & Erreurs
|
||||||
|
'bud_sav_confirm_delete_month' => 'Voulez-vous vraiment supprimer toutes les données de %m pour %o ?',
|
||||||
|
'bud_sav_prompt_duplicate' => "Voulez-vous dupliquer les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
|
||||||
|
'bud_err_tech' => 'Une erreur technique est survenue.',
|
||||||
|
'bud_err_server' => 'Erreur serveur : ',
|
||||||
|
'bud_err_network_dup' => 'Erreur réseau lors de la duplication.',
|
||||||
|
'bud_sav_saving' => 'Sauvegarde en cours...',
|
||||||
|
'bud_err_delete' => 'Erreur lors de la suppression.',
|
||||||
|
|
||||||
// --- BUDGET : PREVISIONNEL ---
|
// --- BUDGET : PREVISIONNEL ---
|
||||||
'bud_prev_incomes' => 'Revenus',
|
'bud_prev_incomes' => 'Revenus',
|
||||||
|
|||||||
+33
-143
@@ -27,167 +27,57 @@ try {
|
|||||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// 1. UPDATE DE pf_people
|
||||||
// 1. UPDATE DE pf_people (Juste la date de naissance et l'état actif)
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
$colCheck = $pdo->prepare("SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'pf_people' AND COLUMN_NAME = 'birthdate'");
|
$colCheck = $pdo->prepare("SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = 'pf_people' AND COLUMN_NAME = 'birthdate'");
|
||||||
$colCheck->execute([$dbName]);
|
$colCheck->execute([$dbName]);
|
||||||
if ($colCheck->rowCount() === 0) {
|
if ($colCheck->rowCount() === 0) {
|
||||||
$pdo->exec("ALTER TABLE pf_people
|
$pdo->exec("ALTER TABLE pf_people ADD COLUMN birthdate DATE DEFAULT NULL, ADD COLUMN is_active TINYINT(1) DEFAULT 1");
|
||||||
ADD COLUMN birthdate DATE DEFAULT NULL,
|
echo "✅ pf_people mis à jour.<br>";
|
||||||
ADD COLUMN is_active TINYINT(1) DEFAULT 1
|
|
||||||
");
|
|
||||||
echo "✅ pf_people mis à jour (birthdate ajouté).<br>";
|
|
||||||
} else {
|
|
||||||
echo "➖ pf_people déjà à jour.<br>";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// 2. CRÉATION DES NOUVELLES TABLES
|
||||||
// 2. CRÉATION DES NOUVELLES TABLES (IF NOT EXISTS)
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_bank_accounts (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, owner_person_id INT DEFAULT NULL, account_type VARCHAR(50) DEFAULT 'savings', is_default TINYINT(1) DEFAULT 0, FOREIGN KEY (owner_person_id) REFERENCES pf_people(id) ON DELETE SET NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
// ---------------------------------------------------------
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_budget_categories (id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(50) NOT NULL, label VARCHAR(100) NOT NULL, type VARCHAR(50) NOT NULL, color VARCHAR(20) DEFAULT '#ccc', icon VARCHAR(20) DEFAULT '💰', UNIQUE KEY (code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_leave_types (id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) NOT NULL, label VARCHAR(100) NOT NULL, default_allowance DECIMAL(5,2) DEFAULT 0, reset_month INT DEFAULT 1, allow_carry_over TINYINT(1) DEFAULT 0, UNIQUE KEY (code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
// Comptes bancaires
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_occasions (id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) NOT NULL, name VARCHAR(100) NOT NULL, month_date VARCHAR(5) DEFAULT NULL, is_active TINYINT(1) DEFAULT 1, UNIQUE KEY (code)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_bank_accounts (
|
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_rules (id INT AUTO_INCREMENT PRIMARY KEY, adult_person_id INT NOT NULL, child_person_id INT NOT NULL, occasion_id INT NOT NULL, FOREIGN KEY (adult_person_id) REFERENCES pf_people(id) ON DELETE CASCADE, FOREIGN KEY (child_person_id) REFERENCES pf_people(id) ON DELETE CASCADE, FOREIGN KEY (occasion_id) REFERENCES pf_gift_occasions(id) ON DELETE CASCADE, UNIQUE KEY adult_child_occ (adult_person_id, child_person_id, occasion_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
owner_person_id INT DEFAULT NULL,
|
|
||||||
account_type VARCHAR(50) DEFAULT 'savings',
|
|
||||||
is_default TINYINT(1) DEFAULT 0,
|
|
||||||
FOREIGN KEY (owner_person_id) REFERENCES pf_people(id) ON DELETE SET NULL
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
|
||||||
|
|
||||||
// Catégories de budget
|
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_budget_categories (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
code VARCHAR(50) NOT NULL,
|
|
||||||
label VARCHAR(100) NOT NULL,
|
|
||||||
type VARCHAR(50) NOT NULL,
|
|
||||||
color VARCHAR(20) DEFAULT '#ccc',
|
|
||||||
icon VARCHAR(20) DEFAULT '💰',
|
|
||||||
UNIQUE KEY (code)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
|
||||||
|
|
||||||
// Types de congés
|
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_leave_types (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
code VARCHAR(20) NOT NULL,
|
|
||||||
label VARCHAR(100) NOT NULL,
|
|
||||||
default_allowance DECIMAL(5,2) DEFAULT 0,
|
|
||||||
reset_month INT DEFAULT 1,
|
|
||||||
allow_carry_over TINYINT(1) DEFAULT 0,
|
|
||||||
UNIQUE KEY (code)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
|
||||||
|
|
||||||
// Fêtes (Cadeaux)
|
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_occasions (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
code VARCHAR(20) NOT NULL,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
month_date VARCHAR(5) DEFAULT NULL,
|
|
||||||
is_active TINYINT(1) DEFAULT 1,
|
|
||||||
UNIQUE KEY (code)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
|
||||||
|
|
||||||
// Règles Cadeaux
|
|
||||||
$pdo->exec("CREATE TABLE IF NOT EXISTS pf_gift_rules (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
adult_person_id INT NOT NULL,
|
|
||||||
child_person_id INT NOT NULL,
|
|
||||||
occasion_id INT NOT NULL,
|
|
||||||
FOREIGN KEY (adult_person_id) REFERENCES pf_people(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (child_person_id) REFERENCES pf_people(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (occasion_id) REFERENCES pf_gift_occasions(id) ON DELETE CASCADE,
|
|
||||||
UNIQUE KEY adult_child_occ (adult_person_id, child_person_id, occasion_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
|
||||||
|
|
||||||
echo "✅ Nouvelles tables de configuration créées ou vérifiées.<br>";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
// 3. INJECTION DE DONNÉES PAR DÉFAUT (Pour ne rien casser)
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
|
|
||||||
|
// 3. INJECTION DONNÉES PAR DÉFAUT
|
||||||
if ($pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn() == 0) {
|
if ($pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn() == 0) {
|
||||||
$pdo->exec("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES
|
$pdo->exec("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES ('Compte Commun', 'checking', 1), ('Livret A Alex', 'savings', 0), ('Livret A Laia', 'savings', 0), ('Livret A Pol', 'savings', 0), ('Livret A Pep', 'savings', 0)");
|
||||||
('Compte Commun', 'checking', 1),
|
|
||||||
('Livret A Alex', 'savings', 0),
|
|
||||||
('Livret A Laia', 'savings', 0),
|
|
||||||
('Livret A Pol', 'savings', 0),
|
|
||||||
('Livret A Pep', 'savings', 0)
|
|
||||||
");
|
|
||||||
}
|
}
|
||||||
|
$pdo->exec("INSERT IGNORE INTO pf_budget_categories (code, label, type, color, icon) VALUES ('INCOME', 'Revenus', 'Income', '#22c55e', '💵'), ('FMCG', 'Alimentation', 'Expense', '#3b82f6', '🛒'), ('FUEL', 'Carburant', 'Expense', '#f59e0b', '⛽'), ('SCHOOL', 'École / Garde', 'Expense', '#a855f7', '🎒'), ('HEALTH', 'Santé', 'Expense', '#ef4444', '⚕️'), ('FIXED', 'Charges Fixes', 'Expense', '#ef4444', '🏢'), ('SAVINGS', 'Épargne & Projets', 'Expense', '#8b5cf6', '🐷'), ('AUTRES', 'Autres / Divers', 'Expense', '#64748b', '📁')");
|
||||||
|
|
||||||
if ($pdo->query("SELECT COUNT(*) FROM pf_budget_categories")->fetchColumn() == 0) {
|
// 4. MIGRATION MODULE CALENDRIER
|
||||||
$pdo->exec("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES
|
|
||||||
('INCOME', 'Revenus', 'Income', '#22c55e', '💵'),
|
|
||||||
('FMCG', 'Alimentation', 'Expense', '#3b82f6', '🛒'),
|
|
||||||
('FUEL', 'Carburant', 'Expense', '#f59e0b', '⛽'),
|
|
||||||
('SCHOOL', 'École / Garde', 'Expense', '#a855f7', '🎒'),
|
|
||||||
('HEALTH', 'Santé', 'Expense', '#ef4444', '⚕️')
|
|
||||||
");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pdo->query("SELECT COUNT(*) FROM pf_leave_types")->fetchColumn() == 0) {
|
|
||||||
$pdo->exec("INSERT INTO pf_leave_types (code, label, default_allowance, reset_month, allow_carry_over) VALUES
|
|
||||||
('CA', 'Congés Annuels', 25, 6, 1),
|
|
||||||
('JRA', 'Jours de Repos', 10, 1, 0),
|
|
||||||
('JA', 'Jour Anniversaire', 1, 1, 0)
|
|
||||||
");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pdo->query("SELECT COUNT(*) FROM pf_gift_occasions")->fetchColumn() == 0) {
|
|
||||||
$pdo->exec("INSERT INTO pf_gift_occasions (code, name, month_date) VALUES
|
|
||||||
('NOEL', 'Noël', '12-25'),
|
|
||||||
('ROIS', 'Les Rois', '01-06'),
|
|
||||||
('ANNIV', 'Anniversaire', NULL)
|
|
||||||
");
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "✅ Données de base injectées (si nécessaire).<br>";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
// 4. MIGRATION SPECIFIQUE : MODULE CALENDRIER (CONGÉS GRANULAIRES)
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
$tableExists = $pdo->query("SHOW TABLES LIKE 'pf_person_leave_meta'")->rowCount() > 0;
|
$tableExists = $pdo->query("SHOW TABLES LIKE 'pf_person_leave_meta'")->rowCount() > 0;
|
||||||
|
|
||||||
if (!$tableExists) {
|
if (!$tableExists) {
|
||||||
$pdo->exec("
|
$pdo->exec("CREATE TABLE pf_person_leave_meta (id INT AUTO_INCREMENT PRIMARY KEY, person_id INT NOT NULL, leave_type VARCHAR(50) NOT NULL, anniversary_date DATE NOT NULL, UNIQUE KEY uq_person_leave (person_id, leave_type), FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||||
CREATE TABLE pf_person_leave_meta (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
person_id INT NOT NULL,
|
|
||||||
leave_type VARCHAR(50) NOT NULL,
|
|
||||||
anniversary_date DATE NOT NULL,
|
|
||||||
UNIQUE KEY uq_person_leave (person_id, leave_type),
|
|
||||||
FOREIGN KEY (person_id) REFERENCES pf_people(id) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
");
|
|
||||||
echo "✅ Table pf_person_leave_meta créée (Nouvelle installation).<br>";
|
|
||||||
} else {
|
|
||||||
$checkColumn = $pdo->query("SHOW COLUMNS FROM pf_person_leave_meta LIKE 'leave_type'")->rowCount();
|
|
||||||
if ($checkColumn === 0) {
|
|
||||||
$pdo->exec("ALTER TABLE pf_person_leave_meta ADD COLUMN leave_type VARCHAR(50) NULL AFTER person_id");
|
|
||||||
$pdo->exec("UPDATE pf_person_leave_meta SET leave_type = 'CP' WHERE leave_type IS NULL");
|
|
||||||
$pdo->exec("ALTER TABLE pf_person_leave_meta MODIFY COLUMN leave_type VARCHAR(50) NOT NULL");
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo->exec("ALTER TABLE pf_person_leave_meta DROP INDEX person_id");
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// On ignore si l'index n'avait pas ce nom
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->exec("ALTER TABLE pf_person_leave_meta ADD UNIQUE KEY uq_person_leave (person_id, leave_type)");
|
|
||||||
echo "✅ Table pf_person_leave_meta mise à jour avec le type granulaire.<br>";
|
|
||||||
} else {
|
|
||||||
echo "➖ Table pf_person_leave_meta déjà à jour.<br>";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. MIGRATION PARAMÈTRES DYNAMIQUES
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS `pf_settings` (`setting_key` varchar(50) NOT NULL, `setting_value` text DEFAULT NULL, `module` varchar(50) NOT NULL, `updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`setting_key`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||||
|
$pdo->exec("INSERT IGNORE INTO `pf_settings` (`setting_key`, `setting_value`, `module`) VALUES ('calendar_default_view', 'month', 'calendar'), ('calendar_first_day', '1', 'calendar'), ('calendar_working_hours', '08:00-19:00', 'calendar'), ('budget_start_day', '1', 'budget'), ('budget_default_tab', 'dépenses', 'budget'), ('travel_default_transport', 'car', 'voyage'), ('travel_default_fuel_price', '1.85', 'voyage'), ('gifts_hide_purchased', '0', 'cadeaux'), ('gifts_default_sort', 'person', 'cadeaux'), ('gifts_budget_alert', '500', 'cadeaux')");
|
||||||
|
|
||||||
|
// 6. MIGRATION CSV MAPPING (La partie ajoutée)
|
||||||
|
$checkCol = $pdo->query("SHOW COLUMNS FROM pf_foyer_settings LIKE 'csv_mapping'")->fetch();
|
||||||
|
if (!$checkCol) {
|
||||||
|
$pdo->exec("ALTER TABLE pf_foyer_settings ADD COLUMN csv_mapping TEXT NULL");
|
||||||
|
echo "✅ Colonne 'csv_mapping' ajoutée.<br>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. MIGRATION STRUCTURE BUDGET & HISTORIQUE
|
||||||
|
$pdo->exec("ALTER TABLE pf_budget_items MODIFY category VARCHAR(100) DEFAULT NULL");
|
||||||
|
$pdo->exec("UPDATE pf_budget_items SET category = 'FIXED' WHERE category = 'expense' AND is_estimate = 0");
|
||||||
|
$pdo->exec("UPDATE pf_budget_items SET category = 'AUTRES' WHERE category = 'expense'");
|
||||||
|
$pdo->exec("UPDATE pf_budget_items SET category = 'INCOME' WHERE category = 'income'");
|
||||||
|
|
||||||
|
echo "✅ Migration terminée pour {$family['name']}.<br>";
|
||||||
|
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>";
|
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<br>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "<h1>🎉 Migration terminée avec succès !</h1>";
|
echo "<h1>🎉 Migration terminée avec succès !</h1>";
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
die("❌ Erreur fatale Meta DB : " . $e->getMessage());
|
die("❌ Erreur fatale Meta DB : " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ try {
|
|||||||
// 2. Catégories
|
// 2. Catégories
|
||||||
$categories = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type ASC, label ASC")->fetchAll(PDO::FETCH_ASSOC);
|
$categories = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type ASC, label ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// 3. Règles d'import (avec le nom de la catégorie correspondante)
|
// 3. Règles d'import
|
||||||
// Utilisation de COLLATE pour éviter l'erreur 1267 de mix de collations
|
|
||||||
$rules = $pdo->query("
|
$rules = $pdo->query("
|
||||||
SELECT r.*, c.label as cat_label
|
SELECT r.*, c.label as cat_label
|
||||||
FROM pf_import_rules r
|
FROM pf_import_rules r
|
||||||
@@ -32,73 +31,60 @@ try {
|
|||||||
$currentYear = (int)date('Y');
|
$currentYear = (int)date('Y');
|
||||||
$salaries = $pdo->query("SELECT * FROM pf_salary_config WHERE year = $currentYear ORDER BY person ASC")->fetchAll(PDO::FETCH_ASSOC);
|
$salaries = $pdo->query("SELECT * FROM pf_salary_config WHERE year = $currentYear ORDER BY person ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// Récupération de la devise du foyer (par défaut € si non définie)
|
// 5. Paramètres Foyer (Devise + Mapping CSV)
|
||||||
// Lecture directe de la colonne currency dans pf_foyer_settings
|
$foyerData = $pdo->query("SELECT currency, csv_mapping FROM pf_foyer_settings LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
||||||
$currencySetting = $pdo->query("SELECT currency FROM pf_foyer_settings LIMIT 1")->fetchColumn() ?: '€';
|
$currencySetting = $foyerData['currency'] ?? '€';
|
||||||
|
$csvMapping = !empty($foyerData['csv_mapping']) ? json_decode($foyerData['csv_mapping'], true) : null;
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
'accounts' => $accounts,
|
'accounts' => $accounts,
|
||||||
'categories' => $categories,
|
'categories' => $categories,
|
||||||
'rules' => $rules,
|
'rules' => $rules,
|
||||||
'salaries' => $salaries,
|
'salaries' => $salaries,
|
||||||
'year' => $currentYear,
|
'year' => $currentYear,
|
||||||
'currency' => $currencySetting
|
'currency' => $currencySetting,
|
||||||
|
'csv_mapping' => $csvMapping
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GESTION DES COMPTES BANCAIRES ---
|
// --- GESTION DES COMPTES BANCAIRES ---
|
||||||
|
|
||||||
if ($action === 'add_account') {
|
if ($action === 'add_account') {
|
||||||
$name = trim($_POST['name'] ?? '');
|
$name = trim($_POST['name'] ?? '');
|
||||||
$type = $_POST['type'] ?? 'checking';
|
$type = $_POST['type'] ?? 'checking';
|
||||||
// On met is_default à 0 par défaut pour les nouveaux comptes
|
|
||||||
|
|
||||||
if (empty($name)) throw new Exception("Le nom du compte est obligatoire.");
|
if (empty($name)) throw new Exception("Le nom du compte est obligatoire.");
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES (?, ?, 0)");
|
$stmt = $pdo->prepare("INSERT INTO pf_bank_accounts (name, account_type, is_default) VALUES (?, ?, 0)");
|
||||||
$stmt->execute([$name, $type]);
|
$stmt->execute([$name, $type]);
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($action === 'delete_account') {
|
if ($action === 'delete_account') {
|
||||||
$id = (int)($_POST['id'] ?? 0);
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
// Sécurité : on empêche de supprimer un compte s'il n'en reste qu'un seul
|
|
||||||
$count = $pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn();
|
$count = $pdo->query("SELECT COUNT(*) FROM pf_bank_accounts")->fetchColumn();
|
||||||
if ($count <= 1) throw new Exception("Impossible de supprimer le dernier compte.");
|
if ($count <= 1) throw new Exception("Impossible de supprimer le dernier compte.");
|
||||||
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_bank_accounts WHERE id = ?");
|
$stmt = $pdo->prepare("DELETE FROM pf_bank_accounts WHERE id = ?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GESTION DES CATÉGORIES ---
|
// --- GESTION DES CATÉGORIES ---
|
||||||
|
|
||||||
if ($action === 'add_category') {
|
if ($action === 'add_category') {
|
||||||
$code = strtoupper(trim($_POST['code'] ?? ''));
|
$code = strtoupper(trim($_POST['code'] ?? ''));
|
||||||
$label = trim($_POST['label'] ?? '');
|
$label = trim($_POST['label'] ?? '');
|
||||||
$type = $_POST['type'] ?? 'Expense';
|
$type = $_POST['type'] ?? 'Expense';
|
||||||
$color = $_POST['color'] ?? '#cccccc';
|
$color = $_POST['color'] ?? '#cccccc';
|
||||||
$icon = trim($_POST['icon'] ?? '📌');
|
$icon = trim($_POST['icon'] ?? '📌');
|
||||||
|
|
||||||
if (empty($code) || empty($label)) throw new Exception("Le code et le libellé sont obligatoires.");
|
if (empty($code) || empty($label)) throw new Exception("Le code et le libellé sont obligatoires.");
|
||||||
|
|
||||||
// Vérification anti-doublon sur le code
|
|
||||||
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_budget_categories WHERE code = ?");
|
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_budget_categories WHERE code = ?");
|
||||||
$check->execute([$code]);
|
$check->execute([$code]);
|
||||||
if ($check->fetchColumn() > 0) throw new Exception("Ce code de catégorie existe déjà.");
|
if ($check->fetchColumn() > 0) throw new Exception("Ce code de catégorie existe déjà.");
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES (?, ?, ?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO pf_budget_categories (code, label, type, color, icon) VALUES (?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([$code, $label, $type, $color, $icon]);
|
$stmt->execute([$code, $label, $type, $color, $icon]);
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -107,27 +93,20 @@ try {
|
|||||||
$id = (int)($_POST['id'] ?? 0);
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_budget_categories WHERE id = ?");
|
$stmt = $pdo->prepare("DELETE FROM pf_budget_categories WHERE id = ?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GESTION DES RÈGLES D'IMPORT ---
|
// --- GESTION DES RÈGLES D'IMPORT ---
|
||||||
|
|
||||||
if ($action === 'add_rule') {
|
if ($action === 'add_rule') {
|
||||||
$keyword = strtoupper(trim($_POST['keyword'] ?? ''));
|
$keyword = strtoupper(trim($_POST['keyword'] ?? ''));
|
||||||
$category = trim($_POST['category'] ?? ''); // Le code de la catégorie (ex: FMCG)
|
$category = trim($_POST['category'] ?? '');
|
||||||
|
|
||||||
if (empty($keyword) || empty($category)) throw new Exception("Le mot-clé et la catégorie sont obligatoires.");
|
if (empty($keyword) || empty($category)) throw new Exception("Le mot-clé et la catégorie sont obligatoires.");
|
||||||
|
|
||||||
// Vérification anti-doublon
|
|
||||||
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_import_rules WHERE keyword = ?");
|
$check = $pdo->prepare("SELECT COUNT(*) FROM pf_import_rules WHERE keyword = ?");
|
||||||
$check->execute([$keyword]);
|
$check->execute([$keyword]);
|
||||||
if ($check->fetchColumn() > 0) throw new Exception("Une règle pour ce mot-clé existe déjà.");
|
if ($check->fetchColumn() > 0) throw new Exception("Une règle pour ce mot-clé existe déjà.");
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category) VALUES (?, ?)");
|
||||||
$stmt->execute([$keyword, $category]);
|
$stmt->execute([$keyword, $category]);
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -136,36 +115,46 @@ try {
|
|||||||
$id = (int)($_POST['id'] ?? 0);
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_import_rules WHERE id = ?");
|
$stmt = $pdo->prepare("DELETE FROM pf_import_rules WHERE id = ?");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GESTION DES SALAIRES ---
|
// --- GESTION DES SALAIRES ---
|
||||||
|
|
||||||
if ($action === 'save_salary') {
|
if ($action === 'save_salary') {
|
||||||
$id = (int)($_POST['id'] ?? 0);
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
$salary = (float)($_POST['salary'] ?? 0);
|
$salary = (float)($_POST['salary'] ?? 0);
|
||||||
$mensualite = (float)($_POST['mensualite'] ?? 0);
|
$mensualite = (float)($_POST['mensualite'] ?? 0);
|
||||||
|
|
||||||
if ($id <= 0) throw new Exception("ID de configuration de salaire invalide.");
|
if ($id <= 0) throw new Exception("ID de configuration de salaire invalide.");
|
||||||
|
|
||||||
// Mise à jour de la ligne pour l'année en cours
|
|
||||||
$stmt = $pdo->prepare("UPDATE pf_salary_config SET salary = ?, mensualite = ? WHERE id = ?");
|
$stmt = $pdo->prepare("UPDATE pf_salary_config SET salary = ?, mensualite = ? WHERE id = ?");
|
||||||
$stmt->execute([$salary, $mensualite, $id]);
|
$stmt->execute([$salary, $mensualite, $id]);
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
// --- GESTION DE LA DEVISE GLOBALE ---
|
|
||||||
|
|
||||||
|
// --- GESTION DE LA DEVISE GLOBALE ---
|
||||||
if ($action === 'save_currency') {
|
if ($action === 'save_currency') {
|
||||||
$currency = trim($_POST['currency'] ?? '€');
|
$currency = trim($_POST['currency'] ?? '€');
|
||||||
|
|
||||||
// Mise à jour directe de la colonne currency pour le foyer
|
|
||||||
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?");
|
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?");
|
||||||
$stmt->execute([$currency]);
|
$stmt->execute([$currency]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTION DU FORMAT CSV ---
|
||||||
|
if ($action === 'save_csv_mapping') {
|
||||||
|
$mapping = [
|
||||||
|
'delimiter' => $_POST['csv_delimiter'] ?? ';',
|
||||||
|
'date_format' => $_POST['csv_date_format'] ?? 'd/m/Y',
|
||||||
|
'col_date' => (int)($_POST['csv_col_date'] ?? 0),
|
||||||
|
'col_label' => (int)($_POST['csv_col_label'] ?? 1),
|
||||||
|
'amount_type' => $_POST['csv_amount_type'] ?? 'single',
|
||||||
|
'col_debit' => (int)($_POST['csv_col_debit'] ?? 8),
|
||||||
|
'col_credit' => (int)($_POST['csv_col_credit'] ?? 9),
|
||||||
|
'col_ref' => (int)($_POST['csv_col_ref'] ?? 3)
|
||||||
|
];
|
||||||
|
$jsonContent = json_encode($mapping);
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_foyer_settings SET csv_mapping = ?");
|
||||||
|
$stmt->execute([$jsonContent]);
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
|
|
||||||
if ($role === 'parent') {
|
if ($role === 'parent') {
|
||||||
$familyParents[] = $row['name'];
|
$familyParents[] = $row['name'];
|
||||||
} elseif ($role === 'nounou') {
|
} elseif ($role === 'nounou' || $role === 'helper') {
|
||||||
// On ignore la nounou dans l'épargne
|
// On ignore la nounou dans l'épargne
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
// Fallback : Si c'est 'enfant', 'user', ou vide, ça va dans l'onglet Enfants
|
|
||||||
$familyKids[] = $row['name'];
|
$familyKids[] = $row['name'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,8 +22,8 @@ while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
// Sécurité anti-page blanche si la BDD est mal configurée
|
// Sécurité anti-page blanche si la BDD est mal configurée
|
||||||
if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2'];
|
if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2'];
|
||||||
|
|
||||||
$requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'Nens');
|
$requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'KIDS');
|
||||||
$ownersToDisplay = ($requestedOwner === 'Nens') ? $familyKids : [$requestedOwner];
|
$ownersToDisplay = ($requestedOwner === 'KIDS') ? $familyKids : [$requestedOwner];
|
||||||
|
|
||||||
// --- RÉCUPÉRATION CONFIGURATION DES MOIS ---
|
// --- RÉCUPÉRATION CONFIGURATION DES MOIS ---
|
||||||
$cycleConfigs = [];
|
$cycleConfigs = [];
|
||||||
@@ -47,23 +46,21 @@ function getMonthName($dateString) {
|
|||||||
<div class="budget-view">
|
<div class="budget-view">
|
||||||
<div class="view-header">
|
<div class="view-header">
|
||||||
<div class="owner-tabs">
|
<div class="owner-tabs">
|
||||||
<!-- Boucle dynamique sur les parents -->
|
|
||||||
<?php foreach ($familyParents as $p): ?>
|
<?php foreach ($familyParents as $p): ?>
|
||||||
<a href="?tab=epargne&owner=<?= urlencode($p) ?>" class="owner-tab <?= $requestedOwner === $p ? 'active' : '' ?>">
|
<a href="?tab=epargne&owner=<?= urlencode($p) ?>" class="owner-tab <?= $requestedOwner === $p ? 'active' : '' ?>">
|
||||||
<?= htmlspecialchars($p) ?>
|
<?= htmlspecialchars($p) ?>
|
||||||
</a>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<!-- Onglet Enfants -->
|
|
||||||
<?php if (!empty($familyKids)): ?>
|
<?php if (!empty($familyKids)): ?>
|
||||||
<a href="?tab=epargne&owner=Nens" class="owner-tab <?= $requestedOwner === 'Nens' ? 'active' : '' ?>">
|
<a href="?tab=epargne&owner=KIDS" class="owner-tab <?= $requestedOwner === 'KIDS' ? 'active' : '' ?>">
|
||||||
<?= tr('budget_tab_kids') ?? 'Nens 👶' ?>
|
<?= tr('budget_tab_kids') ?? 'Enfants 👶' ?>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php foreach ($ownersToDisplay as $currentOwner):
|
<?php foreach ($ownersToDisplay as $index => $currentOwner):
|
||||||
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
|
$stmt = $pdo->prepare("SELECT month_date, category, amount FROM pf_savings WHERE owner = ? ORDER BY month_date DESC");
|
||||||
$stmt->execute([$currentOwner]);
|
$stmt->execute([$currentOwner]);
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
@@ -83,28 +80,29 @@ function getMonthName($dateString) {
|
|||||||
$months = array_slice($months, 0, 7);
|
$months = array_slice($months, 0, 7);
|
||||||
sort($allCategories);
|
sort($allCategories);
|
||||||
|
|
||||||
// Définition de la classe couleur selon le propriétaire
|
|
||||||
$ownerTextClass = 'txt-global';
|
$ownerTextClass = 'txt-global';
|
||||||
|
// 🔄 CORRECTION ICI : Création d'un nom "safe" sans espace pour les classes CSS
|
||||||
|
$safeOwnerCls = htmlspecialchars(str_replace(' ', '_', $currentOwner), ENT_QUOTES);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'KIDS' && $index > 0) ? '40px' : '0' ?>;">
|
||||||
<div style="flex-grow: 1;">
|
<div style="flex-grow: 1;">
|
||||||
<?php if ($requestedOwner === 'Nens'):
|
<?php if ($requestedOwner === 'KIDS'):
|
||||||
$themeClass = 'theme-' . strtolower($currentOwner);
|
$themeClass = 'theme-' . strtolower($safeOwnerCls);
|
||||||
?>
|
?>
|
||||||
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
|
<h3 class="nens-title <?= $themeClass ?>" style="margin:0; font-size:1.2rem;">
|
||||||
<?= $currentOwner ?>
|
<?= htmlspecialchars($currentOwner) ?>
|
||||||
</h3>
|
</h3>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; align-items: center;">
|
<div style="display: flex; gap: 10px; align-items: center;">
|
||||||
<?php if (!empty($months)): ?>
|
<?php if (!empty($months)): ?>
|
||||||
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= $currentOwner ?>')" class="pf-btn btn-secondary">
|
<button onclick="duplicateLastMonth('<?= $months[0] ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn btn-secondary">
|
||||||
🔁 <?= tr('bud_sav_add_one_month') ?>
|
🔁 <?= tr('bud_sav_add_one_month') ?>
|
||||||
</button>
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button onclick="openCustomSavingsModal('<?= $currentOwner ?>')" class="pf-btn">
|
<button onclick="openCustomSavingsModal('<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')" class="pf-btn">
|
||||||
+ <?= tr('bud_sav_add_month') ?>
|
+ <?= tr('bud_sav_add_month') ?>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +114,7 @@ function getMonthName($dateString) {
|
|||||||
<p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p>
|
<p><?= sprintf(tr('bud_sav_no_data'), htmlspecialchars($currentOwner)) ?></p>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<table class="pf-table savings-table nens-table theme-<?= strtolower($currentOwner) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
|
<table class="pf-table savings-table nens-table theme-<?= strtolower($safeOwnerCls) ?>" style="margin-top:0; box-shadow:none; border-radius:16px;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></th>
|
<th class="sticky-col" style="background:#f8fafc;"><?= tr('bud_sav_post_month') ?></th>
|
||||||
@@ -135,11 +133,11 @@ function getMonthName($dateString) {
|
|||||||
<div class="month-actions" style="justify-content: center; width: 100%;">
|
<div class="month-actions" style="justify-content: center; width: 100%;">
|
||||||
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>"
|
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_edit_modal') ?>"
|
||||||
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
|
data-json="<?= htmlspecialchars(json_encode($data[$month] ?? []), ENT_QUOTES, 'UTF-8') ?>"
|
||||||
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= $currentOwner ?>", JSON.parse(this.getAttribute("data-json")))'>
|
onclick='editCustomSavingsMonth("<?= $month ?>", "<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>", JSON.parse(this.getAttribute("data-json")))'>
|
||||||
✏️
|
✏️
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_delete_month') ?>"
|
<button class="btn-icon-small btn-safe-click" title="<?= tr('bud_sav_delete_month') ?>"
|
||||||
onclick="deleteEntireMonth('<?= $month ?>', '<?= $currentOwner ?>')"
|
onclick="deleteEntireMonth('<?= $month ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>')"
|
||||||
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
|
style="color: #ef4444; border-color: #fca5a5; background: #fef2f2;">
|
||||||
🗑️
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
@@ -158,11 +156,11 @@ function getMonthName($dateString) {
|
|||||||
<td class="text-center" style="padding:4px;">
|
<td class="text-center" style="padding:4px;">
|
||||||
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
||||||
<input type="number" step="0.01"
|
<input type="number" step="0.01"
|
||||||
class="prev-input total-input-<?= $currentOwner ?>-<?= $month ?>"
|
class="prev-input total-input-<?= $safeOwnerCls ?>-<?= $month ?>"
|
||||||
style="width: 70px; font-weight:bold; color:#2563eb;"
|
style="width: 70px; font-weight:bold; color:#2563eb;"
|
||||||
value="<?= $val != 0 ? round($val) : '' ?>"
|
value="<?= $val != 0 ? round($val) : '' ?>"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= $currentOwner ?>', this)">
|
onchange="updateEpargneCell('<?= $month ?>', 'TOTAL_BANQUE', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
|
||||||
<span style="color:#2563eb; font-weight:bold; font-size:0.9rem;">€</span>
|
<span style="color:#2563eb; font-weight:bold; font-size:0.9rem;">€</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -178,11 +176,11 @@ function getMonthName($dateString) {
|
|||||||
<td class="text-center" style="padding:4px;">
|
<td class="text-center" style="padding:4px;">
|
||||||
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
<div style="display:flex; align-items:center; justify-content:center; gap:2px;">
|
||||||
<input type="number" step="0.01"
|
<input type="number" step="0.01"
|
||||||
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $currentOwner ?>-<?= $month ?>"
|
class="prev-input <?= $ownerTextClass ?> cat-input-<?= $safeOwnerCls ?>-<?= $month ?>"
|
||||||
style="width: 70px;"
|
style="width: 70px;"
|
||||||
value="<?= $amount != 0 ? round($amount) : '' ?>"
|
value="<?= $amount != 0 ? round($amount) : '' ?>"
|
||||||
placeholder="-"
|
placeholder="-"
|
||||||
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= $currentOwner ?>', this)">
|
onchange="updateEpargneCell('<?= $month ?>', '<?= htmlspecialchars($cat, ENT_QUOTES) ?>', '<?= htmlspecialchars($currentOwner, ENT_QUOTES) ?>', this)">
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -197,7 +195,7 @@ function getMonthName($dateString) {
|
|||||||
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
foreach ($allCategories as $cat) $sum += ($data[$month][$cat] ?? 0);
|
||||||
$extra = $total - $sum;
|
$extra = $total - $sum;
|
||||||
?>
|
?>
|
||||||
<td class="text-center font-bold sum-target" id="extra_<?= $currentOwner ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
|
<td class="text-center font-bold sum-target" id="extra_<?= $safeOwnerCls ?>_<?= $month ?>" style="color: <?= $extra >= 0 ? '#10b981' : '#ef4444' ?>; padding:12px;">
|
||||||
<?= number_format($extra, 0, ',', ' ') ?> €
|
<?= number_format($extra, 0, ',', ' ') ?> €
|
||||||
</td>
|
</td>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -289,6 +287,9 @@ window.I18N = {
|
|||||||
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>
|
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sécurisation de la devise pour la calculatrice
|
||||||
|
const systemCurrency = (typeof window.CONFIG !== 'undefined' && window.CONFIG.CURRENCY) ? window.CONFIG.CURRENCY : '€';
|
||||||
|
|
||||||
// --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT ---
|
// --- 2. GESTION DE L'ÉDITION INVISIBLE EN DIRECT ---
|
||||||
const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>;
|
const cycleConfigs = <?= json_encode($cycleConfigs ?? []) ?>;
|
||||||
|
|
||||||
@@ -306,16 +307,19 @@ function updateEpargneCell(month, category, owner, inputEl) {
|
|||||||
body: formData
|
body: formData
|
||||||
}).catch(err => alert(window.I18N['bud_err_tech'] || 'Erreur technique'));
|
}).catch(err => alert(window.I18N['bud_err_tech'] || 'Erreur technique'));
|
||||||
|
|
||||||
const totalInput = document.querySelector(`.total-input-${owner}-${month}`);
|
// Gère les IDs proprement
|
||||||
|
const safeOwnerClass = owner.replace(/\s+/g, '_');
|
||||||
|
|
||||||
|
const totalInput = document.querySelector(`.total-input-${CSS.escape(safeOwnerClass)}-${month}`);
|
||||||
const totalVal = parseFloat(totalInput ? totalInput.value : 0) || 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-${CSS.escape(safeOwnerClass)}-${month}`).forEach(inp => {
|
||||||
sumCats += parseFloat(inp.value) || 0;
|
sumCats += parseFloat(inp.value) || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const extra = totalVal - sumCats;
|
const extra = totalVal - sumCats;
|
||||||
const extraCell = document.getElementById(`extra_${owner}_${month}`);
|
const extraCell = document.getElementById(`extra_${safeOwnerClass}_${month}`);
|
||||||
|
|
||||||
if (extraCell) {
|
if (extraCell) {
|
||||||
extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €';
|
extraCell.innerText = Math.round(extra).toLocaleString(window.appLang) + ' €';
|
||||||
@@ -557,7 +561,7 @@ function updateSumResult() {
|
|||||||
total += val;
|
total += val;
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + systemCurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
|
|||||||
+146
-246
@@ -1,35 +1,32 @@
|
|||||||
<?php
|
<?php
|
||||||
// modules/budget/views/recap.php
|
// modules/budget/views/recap.php
|
||||||
|
|
||||||
// 1. Récupération des Items du Budget
|
// 1. Récupération des Catégories dynamiques
|
||||||
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY category DESC, sort_order ASC, name ASC");
|
$stmtCats = $pdo->query("SELECT code, label, icon, type FROM pf_budget_categories ORDER BY type DESC, label ASC");
|
||||||
$items = $stmt->fetchAll();
|
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// 2. Déterminer quel est le mois de gestion "ouvert" par défaut
|
// 2. Récupération des Salaires/Mensualités configurés (Revenus automatiques)
|
||||||
|
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y');
|
||||||
|
$stmtSalaries = $pdo->prepare("SELECT person, mensualite FROM pf_salary_config WHERE year = ?");
|
||||||
|
$stmtSalaries->execute([$currentYear]);
|
||||||
|
$salaries = $stmtSalaries->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 3. Récupération des Items du Budget (Charges Fixes et Estimations)
|
||||||
|
$stmt = $pdo->query("SELECT * FROM pf_budget_items ORDER BY is_estimate ASC, sort_order ASC, name ASC");
|
||||||
|
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 4. Gestion du mois actif
|
||||||
$stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1");
|
$stmtActive = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'active_gestion_month' LIMIT 1");
|
||||||
$defaultActiveMonth = $stmtActive->fetchColumn();
|
$defaultActiveMonth = $stmtActive->fetchColumn() ?: date('Y-m-01');
|
||||||
if (!$defaultActiveMonth) {
|
|
||||||
$defaultActiveMonth = date('Y-m-01');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Assigner le mois et l'année en fonction du mois ouvert
|
|
||||||
$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth));
|
$currentMonth = isset($_GET['m']) ? str_pad((int)$_GET['m'], 2, '0', STR_PAD_LEFT) : date('m', strtotime($defaultActiveMonth));
|
||||||
$currentYear = isset($_GET['y']) ? (int)$_GET['y'] : date('Y', strtotime($defaultActiveMonth));
|
|
||||||
$viewMonthDate = "$currentYear-$currentMonth-01";
|
$viewMonthDate = "$currentYear-$currentMonth-01";
|
||||||
|
|
||||||
$sqlReal = "SELECT budget_item_id, SUM(amount) as total_real
|
// 5. Récupération du Réel (Dépenses)
|
||||||
FROM pf_expenses
|
$stmtReal = $pdo->prepare("SELECT budget_item_id, SUM(amount) as total_real FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NOT NULL GROUP BY budget_item_id");
|
||||||
WHERE gestion_month = ? AND budget_item_id IS NOT NULL
|
|
||||||
GROUP BY budget_item_id";
|
|
||||||
$stmtReal = $pdo->prepare($sqlReal);
|
|
||||||
$stmtReal->execute([$viewMonthDate]);
|
$stmtReal->execute([$viewMonthDate]);
|
||||||
$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR); // Retourne un tableau [id => total]
|
$realTotals = $stmtReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
$sqlCatReal = "SELECT category, SUM(amount) as total_real
|
$stmtCatReal = $pdo->prepare("SELECT category, SUM(amount) as total_real FROM pf_expenses WHERE gestion_month = ? AND budget_item_id IS NULL GROUP BY category");
|
||||||
FROM pf_expenses
|
|
||||||
WHERE gestion_month = ? AND budget_item_id IS NULL
|
|
||||||
GROUP BY category";
|
|
||||||
$stmtCatReal = $pdo->prepare($sqlCatReal);
|
|
||||||
$stmtCatReal->execute([$viewMonthDate]);
|
$stmtCatReal->execute([$viewMonthDate]);
|
||||||
$catTotals = $stmtCatReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
$catTotals = $stmtCatReal->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
@@ -38,8 +35,7 @@ $stmtLabels->execute([$viewMonthDate]);
|
|||||||
$unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC);
|
$unlinkedExpenses = $stmtLabels->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
$moisFr = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
||||||
$monthTranslationKey = 'month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT);
|
$currentMonthName = tr('month_' . str_pad((int)$currentMonth, 2, '0', STR_PAD_LEFT)) . ' ' . $currentYear;
|
||||||
$currentMonthName = tr($monthTranslationKey) . ' ' . $currentYear;
|
|
||||||
|
|
||||||
$totalDepenses = 0;
|
$totalDepenses = 0;
|
||||||
$totalRevenus = 0;
|
$totalRevenus = 0;
|
||||||
@@ -64,46 +60,60 @@ $totalRevenus = 0;
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<?php foreach ($salaries as $salary):
|
||||||
|
$mensualite = (float)$salary['mensualite'];
|
||||||
|
$totalRevenus += $mensualite;
|
||||||
|
?>
|
||||||
|
<tr class="row-income" style="border-bottom:1px solid var(--border-light); background:#f0fdf4;">
|
||||||
|
<td style="padding:15px;">
|
||||||
|
<strong>Apport <?= htmlspecialchars($salary['person']) ?></strong>
|
||||||
|
<span title="Géré automatiquement depuis les paramètres des Salaires" style="font-size:0.8rem; cursor:help;">⚙️</span>
|
||||||
|
</td>
|
||||||
|
<td class="cell-amount" style="font-weight:600; padding:15px; color:#10b981;">
|
||||||
|
+ <?= number_format($mensualite, 2, ',', ' ') ?> €
|
||||||
|
</td>
|
||||||
|
<td style="padding:15px;">
|
||||||
|
<span class="badge-type" style="background:#dcfce7; color:#16a34a; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600;">Fixe (Auto)</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding:15px; color:#64748b; font-weight:bold;">-</td>
|
||||||
|
<td style="padding:15px;">
|
||||||
|
<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;">
|
||||||
|
Auto
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding:15px; text-align:right;">
|
||||||
|
<small style="color:#94a3b8;">Via Paramètres</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<?php foreach ($items as $item):
|
<?php foreach ($items as $item):
|
||||||
// --- 1. CALCUL DES TOTAUX PRÉVUS ---
|
|
||||||
$targetAbs = abs((float)$item['amount']);
|
$targetAbs = abs((float)$item['amount']);
|
||||||
$amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs;
|
$amountToAdd = ($item['type'] === 'Annuel') ? $targetAbs / 12 : $targetAbs;
|
||||||
|
$totalDepenses += $amountToAdd;
|
||||||
|
|
||||||
if ($item['category'] === 'income') $totalRevenus += $amountToAdd;
|
|
||||||
else $totalDepenses += $amountToAdd;
|
|
||||||
|
|
||||||
// --- 2. CALCUL DU RÉEL (Logique optimisée) ---
|
|
||||||
$realSum = 0;
|
$realSum = 0;
|
||||||
$hasMatchingExpense = false;
|
$hasMatchingExpense = false;
|
||||||
|
|
||||||
// A. Correspondance directe par ID
|
// A. Correspondance par ID direct
|
||||||
if (isset($realTotals[$item['id']])) {
|
if (isset($realTotals[$item['id']])) {
|
||||||
$realSum = $realTotals[$item['id']];
|
$realSum = $realTotals[$item['id']];
|
||||||
$hasMatchingExpense = true;
|
$hasMatchingExpense = true;
|
||||||
}
|
}
|
||||||
// B. Correspondance par catégorie système (Multi-tenant via Mots-clés)
|
// B. Correspondance par Catégorie système
|
||||||
else {
|
elseif (!empty($item['category']) && isset($catTotals[$item['category']])) {
|
||||||
$catKey = null;
|
$realSum = $catTotals[$item['category']];
|
||||||
if (!empty($item['mapping_keywords'])) {
|
$hasMatchingExpense = true;
|
||||||
if (stripos($item['mapping_keywords'], 'School') !== false) $catKey = 'School';
|
}
|
||||||
elseif (stripos($item['mapping_keywords'], 'Essence') !== false) $catKey = 'Essence';
|
// C. Correspondance par mots-clés bancaires
|
||||||
elseif (stripos($item['mapping_keywords'], 'FMCG') !== false) $catKey = 'FMCG';
|
elseif (!empty($item['mapping_keywords'])) {
|
||||||
}
|
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
||||||
|
foreach ($unlinkedExpenses as $uexp) {
|
||||||
if ($catKey && isset($catTotals[$catKey])) {
|
foreach ($keywords as $kw) {
|
||||||
$realSum = $catTotals[$catKey];
|
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
|
||||||
$hasMatchingExpense = true;
|
$realSum += (float)$uexp['amount'];
|
||||||
}
|
$hasMatchingExpense = true;
|
||||||
// C. Correspondance par mots-clés classiques (sur les dépenses non liées)
|
break;
|
||||||
elseif (!empty($item['mapping_keywords'])) {
|
|
||||||
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
|
||||||
foreach ($unlinkedExpenses as $uexp) {
|
|
||||||
foreach ($keywords as $kw) {
|
|
||||||
if (!empty($kw) && stripos($uexp['label'], $kw) !== false) {
|
|
||||||
$realSum += (float)$uexp['amount'];
|
|
||||||
$hasMatchingExpense = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,79 +121,55 @@ $totalRevenus = 0;
|
|||||||
|
|
||||||
$realAbs = abs($realSum);
|
$realAbs = abs($realSum);
|
||||||
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
|
$isAutoChecked = ($hasMatchingExpense && ($realAbs >= ($targetAbs - 0.10)));
|
||||||
|
$rowClass = 'row-expense' . ($item['is_estimate'] ? ' row-estimate' : '');
|
||||||
$rowClass = ($item['category'] === 'income') ? 'row-income' : 'row-expense';
|
|
||||||
if ($item['is_estimate']) $rowClass .= ' row-estimate';
|
|
||||||
?>
|
?>
|
||||||
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid var(--border-light);">
|
<tr class="<?= $rowClass ?>" style="border-bottom:1px solid var(--border-light);">
|
||||||
<td style="padding:15px;">
|
<td style="padding:15px;">
|
||||||
<strong><?= htmlspecialchars($item['name']) ?></strong>
|
<strong><?= htmlspecialchars($item['name']) ?></strong>
|
||||||
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">('.tr('bud_est_short').')</small>' : '' ?>
|
<?= $item['is_estimate'] ? ' <small style="color:#64748b;">(Variable)</small>' : '' ?>
|
||||||
|
|
||||||
<?php if(!empty($item['mapping_keywords'])): ?>
|
<?php if(!empty($item['mapping_keywords'])): ?>
|
||||||
<span title="<?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
|
<span title="Reconnaissance bancaire : <?= htmlspecialchars($item['mapping_keywords']) ?>" style="font-size:0.7rem; cursor:help;">🔗</span>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if(!empty($item['reg_month'])): ?>
|
|
||||||
<div style="font-size:0.75rem; color:#94a3b8; font-style:italic; margin-top:2px;">
|
|
||||||
📅 <?= sprintf(tr('bud_reg_planned_in'), tr('month_'.str_pad(array_search($item['reg_month'], $moisFr), 2, '0', STR_PAD_LEFT))) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="cell-amount" style="font-weight:600; padding:15px; color:<?= $item['category']==='income'?'#10b981':'#1e293b' ?>;">
|
<td class="cell-amount" style="font-weight:600; padding:15px; color:#1e293b;">
|
||||||
<?= number_format($targetAbs, 2, ',', ' ') ?> €
|
- <?= number_format($targetAbs, 2, ',', ' ') ?> €
|
||||||
|
|
||||||
<?php if ($hasMatchingExpense): ?>
|
<?php if ($hasMatchingExpense): ?>
|
||||||
<?php
|
<?php $gap = $realAbs - $targetAbs;
|
||||||
// Le "Gap" est la différence visuelle.
|
|
||||||
$gap = $realAbs - $targetAbs;
|
|
||||||
|
|
||||||
if ($gap > 0.05): ?>
|
if ($gap > 0.05): ?>
|
||||||
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">
|
<div style="font-size:0.75rem; color:#ef4444; font-weight:bold;">Dépassé : +<?= number_format($gap, 2, ',', ' ') ?> €</div>
|
||||||
<?= $item['category'] === 'income' ? tr('bud_bonus') : tr('bud_overrun') ?> : +<?= number_format($gap, 2, ',', ' ') ?> €
|
|
||||||
</div>
|
|
||||||
<?php elseif ($gap < -0.05): ?>
|
<?php elseif ($gap < -0.05): ?>
|
||||||
<div style="font-size:0.75rem; color:#f59e0b; font-weight:normal;">
|
<div style="font-size:0.75rem; color:#f59e0b;">Reste : <?= number_format(abs($gap), 2, ',', ' ') ?> €</div>
|
||||||
<?= tr('bud_remaining') ?> : <?= number_format(abs($gap), 2, ',', ' ') ?> €
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div style="font-size:0.75rem; color:#10b981; font-weight:normal;">
|
<div style="font-size:0.75rem; color:#10b981;">Atteint ✓</div>
|
||||||
<?= tr('bud_exact_amount') ?> ✓
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php elseif ($item['type'] === 'Annuel'): ?>
|
<?php elseif ($item['type'] === 'Annuel'): ?>
|
||||||
<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>
|
<div style="font-size:0.75rem; color:#94a3b8;"><?= number_format($amountToAdd, 2, ',', ' ') ?>/mois</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style="padding:15px;">
|
<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;">
|
<span class="badge-type" style="background:#e2e8f0; padding:4px 8px; border-radius:12px; font-size:0.8rem; font-weight:600; color:#475569;">
|
||||||
<?= tr('bud_freq_'.strtolower($item['type'])) ?>
|
<?= htmlspecialchars($item['type']) ?>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ? $item['payment_day'] : '-' ?></td>
|
<td style="padding:15px; color:#64748b; font-weight:bold;"><?= $item['payment_day'] ?: '-' ?></td>
|
||||||
|
|
||||||
<td style="padding:15px;">
|
<td style="padding:15px;">
|
||||||
<?php if ($isAutoChecked): ?>
|
<?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;">
|
<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é</div>
|
||||||
<span>✓</span> <?= tr('bud_state_validated') ?>
|
|
||||||
</div>
|
|
||||||
<?php elseif($hasMatchingExpense): ?>
|
<?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;">
|
<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</div>
|
||||||
<span>⏳</span> <?= tr('bud_state_partial') ?>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
<?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;">
|
<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</div>
|
||||||
<span>○</span> <?= tr('bud_state_waiting') ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td style="padding:15px; text-align:right;">
|
<td style="padding:15px; text-align:right;">
|
||||||
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
|
<div class="action-buttons" style="display:flex; gap:5px; justify-content:flex-end;">
|
||||||
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="<?= tr('edit') ?>">✏️</button>
|
<button class="btn-icon-action edit" onclick='editRecapItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>)' title="Modifier">✏️</button>
|
||||||
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="<?= tr('delete') ?>">🗑️</button>
|
<button class="btn-icon-action delete" onclick="deleteRecapItem(<?= $item['id'] ?>)" title="Supprimer">🗑️</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -191,34 +177,30 @@ $totalRevenus = 0;
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tfoot style="background:#f8fafc;">
|
<tfoot style="background:#f8fafc;">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_income_smoothed') ?></strong></td>
|
<td colspan="1" style="padding:15px;"><strong>Total Revenus Lissés</strong></td>
|
||||||
<td colspan="5" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenus, 2, ',', ' ') ?> €</strong></td>
|
<td colspan="5" style="padding:15px; color:#10b981;"><strong>+ <?= number_format($totalRevenus, 2, ',', ' ') ?> €</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="1" style="padding:15px;"><strong><?= tr('bud_total_expenses_smoothed') ?></strong></td>
|
<td colspan="1" style="padding:15px;"><strong>Total Dépenses & Estimations</strong></td>
|
||||||
<td colspan="5" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepenses, 2, ',', ' ') ?> €</strong></td>
|
<td colspan="5" style="padding:15px; color:#ef4444;"><strong>- <?= number_format($totalDepenses, 2, ',', ' ') ?> €</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr style="border-top: 2px solid #e2e8f0; background: white;">
|
<tr style="border-top: 2px solid #e2e8f0; background: white;">
|
||||||
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong><?= tr('bud_theoretical_balance_recap') ?></strong></td>
|
<td colspan="1" style="padding:15px; font-size:1.1rem;"><strong>Reste à Vivre (Équilibre)</strong></td>
|
||||||
<?php $balance = $totalRevenus - $totalDepenses; ?>
|
<?php $balance = $totalRevenus - $totalDepenses; ?>
|
||||||
<td colspan="5" style="padding:15px; font-size: 1.3em;" class="<?= $balance >= 0 ? 'text-success' : 'text-danger' ?>">
|
<td colspan="5" style="padding:15px; font-size: 1.3em;">
|
||||||
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / <?= tr('bud_month_short') ?></strong>
|
<strong style="color:<?= $balance >= 0 ? '#10b981' : '#ef4444' ?>;"><?= number_format($balance, 2, ',', ' ') ?> € / mois</strong>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="budget-note" style="margin-top:15px; font-size:0.85rem; color:#64748b;">
|
|
||||||
<p>* <?= tr('bud_recap_footer_note') ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="budgetRecapModal" 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 id="budgetRecapModal" 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:500px; border-radius:20px; box-shadow:0 20px 25px -5px rgba(0,0,0,0.1); padding:30px; position:relative;">
|
<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;">
|
<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;"><?= tr('bud_recap_modal_add') ?></h3>
|
<h3 id="recapModalTitle" style="margin:0; font-size:1.2rem; color:#1e293b;">Ajouter une ligne</h3>
|
||||||
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">×</button>
|
<button type="button" onclick="closeRecapModal()" style="border:none; background:none; font-size:1.8rem; cursor:pointer; color:#64748b; line-height:1;">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -227,90 +209,65 @@ $totalRevenus = 0;
|
|||||||
<input type="hidden" name="id" id="item_id">
|
<input type="hidden" name="id" id="item_id">
|
||||||
|
|
||||||
<div class="form-group" style="margin-bottom:15px;">
|
<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;"><?= tr('bud_label_name') ?></label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Nom de la ligne</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;">
|
<input type="text" name="name" id="item_name" required class="pf-input" placeholder="ex: Assurance Auto, Estimation Courses..." 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;"><?= 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>
|
||||||
|
|
||||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_amount_eur') ?></label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Montant Prévu (€)</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;">
|
<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>
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_day') ?></label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Type de Dépense</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;">
|
<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, Prélèvement)</option>
|
||||||
|
<option value="1">Variable (Estimation, Enveloppe)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
<div style="display:flex; gap:15px; margin-bottom:15px;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_category') ?></label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Catégorie Cible (La jauge)</label>
|
||||||
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
<select name="category" id="item_category" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;" required>
|
||||||
<option value="expense"><?= tr('bud_cat_expense') ?></option>
|
<option value="" disabled selected>-- Choisir --</option>
|
||||||
<option value="income"><?= tr('bud_cat_income') ?></option>
|
<?php foreach ($dbCategories as $c): ?>
|
||||||
|
<?php if(strtolower($c['type']) === 'expense'): // On n'affiche que les catégories de dépenses ?>
|
||||||
|
<option value="<?= htmlspecialchars($c['code']) ?>">
|
||||||
|
<?= htmlspecialchars($c['icon'] . ' ' . $c['label']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;"><?= tr('bud_label_frequency') ?></label>
|
<label class="pf-label" style="display:block; margin-bottom:5px; font-weight:600; color:#475569; font-size:0.9rem;">Fréquence</label>
|
||||||
<select name="type" id="item_type" class="pf-input" style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:white;">
|
<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"><?= tr('bud_freq_mensuel') ?></option>
|
<option value="Mensuel">Mensuel</option>
|
||||||
<option value="Annuel"><?= tr('bud_freq_annuel') ?></option>
|
<option value="Annuel">Annuel</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:15px; margin-bottom:25px;">
|
<div class="form-group" style="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;">Mots-clés (Reconnaissance Bancaire)</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>
|
<input type="text" name="mapping_keywords" id="item_keywords" class="pf-input" placeholder="ex: NETFLIX, DOMOFINANCE..." style="width:100%; padding:10px; border:1px solid #cbd5e1; border-radius:8px; background:#f0f9ff; border-color:#bae6fd;">
|
||||||
<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;">
|
<small style="color:#64748b; font-size:0.75rem;">Optionnel. Permet au système de lier automatiquement un import CSV à cette ligne.</small>
|
||||||
<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;"><?= 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=""><?= tr('bud_reg_none') ?></option>
|
|
||||||
<?php
|
|
||||||
foreach($moisFr as $index => $m) {
|
|
||||||
if($index == 0) continue;
|
|
||||||
echo "<option value='$m'>" . tr('month_'.str_pad($index, 2, '0', STR_PAD_LEFT)) . "</option>";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top: 25px;">
|
||||||
<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="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;"><?= tr('btn_save') ?></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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
|
||||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
|
||||||
window.I18N = {
|
|
||||||
...(window.I18N || {}),
|
|
||||||
'bud_recap_modal_add': <?= json_encode(tr('bud_recap_modal_add')) ?>,
|
|
||||||
'bud_recap_modal_edit': <?= json_encode(tr('bud_recap_modal_edit')) ?>,
|
|
||||||
'bud_recap_confirm_delete': <?= json_encode(tr('bud_recap_confirm_delete')) ?>,
|
|
||||||
'bud_err_delete': <?= json_encode(tr('bud_err_delete')) ?>,
|
|
||||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
|
||||||
'bud_saving': <?= json_encode(tr('bud_sav_saving') ?? 'Sauvegarde...') ?>
|
|
||||||
};
|
|
||||||
|
|
||||||
function openRecapModal(mode) {
|
function openRecapModal(mode) {
|
||||||
if (mode === "add") {
|
if (mode === "add") {
|
||||||
document.getElementById("recapModalTitle").innerText = window.I18N['bud_recap_modal_add'] || 'Ajouter';
|
document.getElementById("recapModalTitle").innerText = 'Ajouter une ligne';
|
||||||
document.getElementById("item_id").value = "";
|
document.getElementById("item_id").value = "";
|
||||||
document.getElementById("recapForm").reset();
|
document.getElementById("recapForm").reset();
|
||||||
}
|
}
|
||||||
@@ -324,113 +281,56 @@ function closeRecapModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
const modal = document.getElementById('budgetRecapModal');
|
if (event.target == document.getElementById('budgetRecapModal')) closeRecapModal();
|
||||||
if (event.target == modal) {
|
|
||||||
closeRecapModal();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function editRecapItem(item) {
|
function editRecapItem(item) {
|
||||||
const data = typeof item === "string" ? JSON.parse(item) : item;
|
const data = typeof item === "string" ? JSON.parse(item) : item;
|
||||||
|
document.getElementById("recapModalTitle").innerText = "Editer : " + data.name;
|
||||||
document.getElementById("recapModalTitle").innerText = (window.I18N['bud_recap_modal_edit'] || 'Editer') + " : " + data.name;
|
|
||||||
document.getElementById("item_id").value = data.id;
|
document.getElementById("item_id").value = data.id;
|
||||||
document.getElementById("item_name").value = data.name;
|
document.getElementById("item_name").value = data.name;
|
||||||
document.getElementById("item_keywords").value = data.mapping_keywords || '';
|
document.getElementById("item_keywords").value = data.mapping_keywords || '';
|
||||||
document.getElementById("item_amount").value = Math.abs(data.amount);
|
document.getElementById("item_amount").value = Math.abs(data.amount);
|
||||||
document.getElementById("item_category").value = data.category;
|
document.getElementById("item_category").value = data.category;
|
||||||
document.getElementById("item_type").value = data.type;
|
document.getElementById("item_type").value = data.type;
|
||||||
document.getElementById("item_day").value = data.payment_day;
|
|
||||||
document.getElementById("item_reg_month").value = data.reg_month || "";
|
|
||||||
document.getElementById("item_is_estimate").value = data.is_estimate;
|
document.getElementById("item_is_estimate").value = data.is_estimate;
|
||||||
|
|
||||||
document.getElementById("budgetRecapModal").style.display = "flex";
|
document.getElementById("budgetRecapModal").style.display = "flex";
|
||||||
document.body.classList.add('no-scroll');
|
document.body.classList.add('no-scroll');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. INTERCEPTION ASYNCHRONE DU FORMULAIRE ---
|
document.getElementById('recapForm').addEventListener('submit', async function(e) {
|
||||||
const recapForm = document.getElementById('recapForm');
|
e.preventDefault();
|
||||||
if (recapForm) {
|
const btn = this.querySelector('button[type="submit"]');
|
||||||
recapForm.addEventListener('submit', async function(e) {
|
btn.disabled = true; btn.innerText = '⏳ ...';
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const submitBtn = this.querySelector('button[type="submit"]');
|
const formData = new FormData(this);
|
||||||
const originalText = submitBtn.innerText;
|
formData.append('ajax', '1');
|
||||||
submitBtn.innerText = window.I18N['bud_saving'] || '⏳ ...';
|
const url = this.getAttribute('action');
|
||||||
submitBtn.disabled = true;
|
|
||||||
|
|
||||||
const formData = new FormData(this);
|
|
||||||
formData.append('ajax', '1');
|
|
||||||
|
|
||||||
// 💡 Utilisation sécurisée de getAttribute
|
|
||||||
const actionUrl = this.getAttribute('action');
|
|
||||||
const finalUrl = actionUrl.startsWith('/') ? actionUrl.substring(1) : actionUrl;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(finalUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
||||||
|
|
||||||
// 💡 Lecture robuste (anti-Warnings PHP)
|
|
||||||
const textResult = await response.text();
|
|
||||||
try {
|
|
||||||
const result = JSON.parse(textResult);
|
|
||||||
if (result.success) {
|
|
||||||
closeRecapModal();
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
|
||||||
}
|
|
||||||
} catch (jsonError) {
|
|
||||||
console.error("Réponse non-JSON :", textResult);
|
|
||||||
alert("Le serveur a renvoyé une erreur PHP. Regarde la console (F12).");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur Fetch:", error);
|
|
||||||
alert(window.I18N['bud_err_tech'] || 'Erreur technique');
|
|
||||||
} finally {
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.innerText = originalText;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 3. SUPPRESSION ASYNCHRONE ---
|
|
||||||
async function deleteRecapItem(id) {
|
|
||||||
if (!confirm(window.I18N['bud_recap_confirm_delete'] || "Confirmer la suppression ?")) return;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("action", "delete");
|
|
||||||
formData.append("id", id);
|
|
||||||
formData.append("ajax", "1"); // Signale à l'API qu'on attend du JSON
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("modules/budget/includes/api/manage-item.php", {
|
const res = await fetch(url.startsWith('/') ? url.substring(1) : url, { method: 'POST', body: formData });
|
||||||
method: "POST",
|
const data = await res.json();
|
||||||
body: formData,
|
if (data.success) window.location.reload();
|
||||||
});
|
else alert("Erreur : " + data.error);
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
||||||
|
|
||||||
const textResult = await response.text();
|
|
||||||
try {
|
|
||||||
const result = JSON.parse(textResult);
|
|
||||||
// On s'assure que si l'API ne renvoie pas success, on le signale
|
|
||||||
if (result.success !== false) {
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
alert((window.I18N['bud_err_delete'] || 'Erreur de suppression') + " : " + (result.error || ""));
|
|
||||||
}
|
|
||||||
} catch (jsonErr) {
|
|
||||||
console.error("Réponse non-JSON lors de la suppression :", textResult);
|
|
||||||
window.location.reload(); // Fallback si l'API redirige au lieu de répondre en JSON
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erreur réseau Suppression:", err);
|
alert("Erreur technique lors de la sauvegarde.");
|
||||||
alert(window.I18N['bud_err_delete'] || 'Erreur réseau');
|
} finally {
|
||||||
|
btn.disabled = false; btn.innerText = 'Enregistrer';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function deleteRecapItem(id) {
|
||||||
|
if (!confirm("Confirmer la suppression ?")) return;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("action", "delete"); formData.append("id", id); formData.append("ajax", "1");
|
||||||
|
try {
|
||||||
|
const res = await fetch("modules/budget/includes/api/manage-item.php", { method: "POST", body: formData });
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success !== false) window.location.reload();
|
||||||
|
else alert("Erreur : " + data.error);
|
||||||
|
} catch (err) {
|
||||||
|
window.location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
+144
-106
@@ -60,7 +60,6 @@ if (isset($_POST['action']) && $_POST['action'] === 'reopen_month') {
|
|||||||
if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
if (isset($_POST['action']) && $_POST['action'] === 'save_import') {
|
||||||
$count = 0;
|
$count = 0;
|
||||||
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
$stmtExp = $pdo->prepare("INSERT INTO pf_expenses (date_exp, gestion_month, category, label, amount, import_ref, budget_item_id, holiday_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
// Mémorisation du mapping étendu (incluant budget_item_id)
|
|
||||||
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category, budget_item_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category), budget_item_id = VALUES(budget_item_id)");
|
$stmtRule = $pdo->prepare("INSERT INTO pf_import_rules (keyword, category, budget_item_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE category = VALUES(category), budget_item_id = VALUES(budget_item_id)");
|
||||||
|
|
||||||
if (isset($_POST['lines']) && is_array($_POST['lines'])) {
|
if (isset($_POST['lines']) && is_array($_POST['lines'])) {
|
||||||
@@ -152,9 +151,43 @@ if (isset($_FILES['csv_file']) && $_FILES['csv_file']['error'] == 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 3. RECUPERATION DES DONNEES ET CALCULS
|
// 3. RECUPERATION DES DONNEES ET CALCULS (100% DYNAMIQUE)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// --- LECTURE DYNAMIQUE DES CATEGORIES ---
|
||||||
|
$stmtCats = $pdo->query("SELECT * FROM pf_budget_categories ORDER BY type DESC, label ASC");
|
||||||
|
$dbCategories = $stmtCats->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$categoriesConfig = [];
|
||||||
|
foreach ($dbCategories as $c) {
|
||||||
|
$catType = ($c['type'] === 'Income') ? 'credit' : 'debit';
|
||||||
|
$categoriesConfig[$c['code']] = [
|
||||||
|
'type' => $catType,
|
||||||
|
'db_type' => $c['type'],
|
||||||
|
'label' => ($c['icon'] ? $c['icon'] . ' ' : '') . $c['label'],
|
||||||
|
'budget' => 0, // Sera rempli par les règles
|
||||||
|
'color' => $c['color'] ?: '#64748b',
|
||||||
|
'suggestions' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback "Autres" au cas où la BDD serait vide ou pour les dépenses non classées
|
||||||
|
if (!isset($categoriesConfig['AUTRES'])) {
|
||||||
|
$categoriesConfig['AUTRES'] = [
|
||||||
|
'type'=>'debit', 'db_type'=>'Expense', 'label'=>'📁 Autres / Divers',
|
||||||
|
'budget'=>0, 'color'=>'#94a3b8', 'suggestions'=>[]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peuplement dynamique des suggestions via les règles existantes
|
||||||
|
$stmtRules = $pdo->query("SELECT keyword, category FROM pf_import_rules");
|
||||||
|
while ($rule = $stmtRules->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
if (isset($categoriesConfig[$rule['category']])) {
|
||||||
|
$categoriesConfig[$rule['category']]['suggestions'][] = $rule['keyword'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
|
$stmtCheckClose = $pdo->prepare("SELECT content FROM pf_notes WHERE note_type = 'month_closure' AND reference_id = ?");
|
||||||
$stmtCheckClose->execute([$viewMonthDate]);
|
$stmtCheckClose->execute([$viewMonthDate]);
|
||||||
$closureJson = $stmtCheckClose->fetchColumn();
|
$closureJson = $stmtCheckClose->fetchColumn();
|
||||||
@@ -184,27 +217,42 @@ $allExpenses = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
$paidItemIds = array_column(array_filter($allExpenses, fn($e) => !empty($e['budget_item_id'])), 'budget_item_id');
|
$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');
|
$realExpensesLabels = array_column(array_filter($allExpenses, fn($e) => $e['amount'] < 0), 'label');
|
||||||
|
|
||||||
$budget_fmcg = 0; $budget_school = 0; $budget_essence = 0; $budget_frais = 0; $budget_income_prevu = 0;
|
$budget_income_prevu = 0;
|
||||||
$total_income = 0; $total_expenses_prevues = 0;
|
$total_income = 0; $total_expenses_prevues = 0;
|
||||||
$reste_a_venir_calc = 0;
|
$reste_a_venir_calc = 0;
|
||||||
$fixedChargesList = []; $incomeList = []; $pending_charges = [];
|
$fixedChargesList = []; $incomeList = []; $pending_charges = [];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// MAPPING DYNAMIQUE DES BUDGETS PRÉVISIONNELS (NOUVELLE LOGIQUE)
|
||||||
|
// ============================================================================
|
||||||
$stmt = $pdo->query("SELECT id, name, amount, type, category, is_estimate, payment_day, mapping_keywords FROM pf_budget_items ORDER BY name ASC");
|
$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)) {
|
while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
$absAmount = abs((float)$item['amount']);
|
$absAmount = abs((float)$item['amount']);
|
||||||
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
$amt = ($item['type'] === 'Annuel') ? $absAmount / 12 : $absAmount;
|
||||||
$name = trim($item['name']);
|
$name = trim($item['name']);
|
||||||
|
$catCode = $item['category']; // Ex: FIXED, FMCG, INCOME...
|
||||||
|
$isIncome = ((float)$item['amount'] > 0); // La norme est désormais définie par le signe du montant
|
||||||
|
|
||||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
if ($isIncome) {
|
||||||
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
|
||||||
}
|
|
||||||
if ($item['category'] === 'income') {
|
|
||||||
$incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
$incomeList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||||
$total_income += $amt;
|
$total_income += $amt;
|
||||||
$budget_income_prevu += $amt;
|
$budget_income_prevu += $amt;
|
||||||
|
|
||||||
|
// Attribution au compte de revenu défini, sinon au premier disponible
|
||||||
|
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
|
||||||
|
$categoriesConfig[$catCode]['budget'] += $amt;
|
||||||
|
} else {
|
||||||
|
$incomeCatKey = array_key_first(array_filter($categoriesConfig, fn($c) => $c['db_type'] === 'Income'));
|
||||||
|
if ($incomeCatKey) $categoriesConfig[$incomeCatKey]['budget'] += $amt;
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$total_expenses_prevues += $amt;
|
$total_expenses_prevues += $amt;
|
||||||
if ($item['category'] === 'expense' && $item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
|
||||||
|
// C'est une charge fixe (Mensuel + Is_Estimate = 0)
|
||||||
|
if ($item['type'] === 'Mensuel' && (int)$item['is_estimate'] === 0) {
|
||||||
|
$fixedChargesList[] = ['id' => $item['id'], 'name' => $name, 'amount' => $absAmount];
|
||||||
|
|
||||||
$isPaid = false;
|
$isPaid = false;
|
||||||
if (in_array($item['id'], $paidItemIds)) $isPaid = true;
|
if (in_array($item['id'], $paidItemIds)) $isPaid = true;
|
||||||
elseif (!empty($item['mapping_keywords'])) {
|
elseif (!empty($item['mapping_keywords'])) {
|
||||||
@@ -221,90 +269,68 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount];
|
$pending_charges[] = ['name' => $name, 'amount' => $absAmount];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!empty($item['mapping_keywords'])) {
|
|
||||||
if (stripos($item['mapping_keywords'], 'FMCG') !== false) $budget_fmcg += $amt;
|
|
||||||
if (stripos($item['mapping_keywords'], 'School') !== false) $budget_school += $amt;
|
|
||||||
if (stripos($item['mapping_keywords'], 'Essence') !== false) $budget_essence += $amt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') {
|
// NOUVEAU : Attribution du budget prévisionnel (le Plafond) via la catégorie dynamique
|
||||||
$budget_frais += $absAmount;
|
if (!empty($catCode) && isset($categoriesConfig[$catCode])) {
|
||||||
|
$categoriesConfig[$catCode]['budget'] += $amt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$budget_autres = max(0, $total_income - $total_expenses_prevues);
|
// L'enveloppe "Autres" prend tout le reste du budget non alloué
|
||||||
|
$categoriesConfig['AUTRES']['budget'] = max(0, $total_income - $total_expenses_prevues);
|
||||||
// TRADUCTION DES CATÉGORIES
|
|
||||||
$categoriesConfig = [
|
|
||||||
'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']],
|
|
||||||
'Apports' => ['type'=>'debit', 'label'=>tr('cat_contributions') ?? 'Apports & Projets', 'budget'=>0, 'color'=>'#0ea5e9', 'suggestions'=>['Alex', 'Laia', 'Remboursement']],
|
|
||||||
'LivretA' => ['type'=>'debit', 'label'=>tr('cat_savings'),'budget'=>0, 'color'=>'#8b5cf6', 'suggestions'=>['Virement']]
|
|
||||||
];
|
|
||||||
|
|
||||||
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
|
$totals = array_fill_keys(array_keys($categoriesConfig), 0);
|
||||||
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
|
$expensesByCategory = array_fill_keys(array_keys($categoriesConfig), []);
|
||||||
$total_rentrees = 0;
|
$total_rentrees = 0;
|
||||||
$depenses_reelles = 0;
|
$depenses_reelles = 0;
|
||||||
|
|
||||||
|
// VENTILATION DYNAMIQUE DES DÉPENSES
|
||||||
foreach ($allExpenses as $exp) {
|
foreach ($allExpenses as $exp) {
|
||||||
$cat = $exp['category'];
|
$cat = $exp['category'];
|
||||||
if (!isset($totals[$cat])) $cat = 'Autres';
|
if (!isset($categoriesConfig[$cat])) $cat = 'AUTRES';
|
||||||
|
|
||||||
$val = (float)$exp['amount'];
|
$val = (float)$exp['amount'];
|
||||||
|
|
||||||
if ($cat === 'Income') { $totals[$cat] += $val; }
|
if ($categoriesConfig[$cat]['db_type'] === 'Income') {
|
||||||
else {
|
$totals[$cat] += $val;
|
||||||
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val;
|
} else {
|
||||||
|
if ($val > 0) $categoriesConfig[$cat]['budget'] += $val; // Remboursement
|
||||||
else $totals[$cat] += abs($val);
|
else $totals[$cat] += abs($val);
|
||||||
}
|
}
|
||||||
|
|
||||||
$expensesByCategory[$cat][] = $exp;
|
$expensesByCategory[$cat][] = $exp;
|
||||||
|
|
||||||
if ($val > 0) {
|
if ($val > 0) {
|
||||||
if ($cat === 'Income' || $cat !== 'Frais') $total_rentrees += $val;
|
if ($categoriesConfig[$cat]['db_type'] === 'Income') { $total_rentrees += $val; }
|
||||||
else $depenses_reelles -= $val;
|
else { $depenses_reelles -= $val; }
|
||||||
} else {
|
} else {
|
||||||
$depenses_reelles += abs($val);
|
$depenses_reelles += abs($val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reste à venir dynamiques
|
// RESTE A VENIR PAR CATEGORIE (Remplace les variables codées en dur)
|
||||||
$ecole_depense = isset($totals['School']) ? $totals['School'] : 0;
|
foreach ($categoriesConfig as $code => $conf) {
|
||||||
$reste_ecole = max(0, $budget_school - $ecole_depense);
|
if ($conf['db_type'] === 'Expense' && $conf['budget'] > 0) {
|
||||||
if ($reste_ecole > 0) {
|
$spent = $totals[$code] ?? 0;
|
||||||
$reste_a_venir_calc += $reste_ecole;
|
$rem = max(0, $conf['budget'] - $spent);
|
||||||
$pending_charges[] = ['name' => tr('bud_rem_school'), 'amount' => $reste_ecole];
|
if ($rem > 0) {
|
||||||
}
|
$reste_a_venir_calc += $rem;
|
||||||
|
$pending_charges[] = ['name' => 'Reste ' . strip_tags($conf['label']), 'amount' => $rem];
|
||||||
$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' => tr('bud_rem_fmcg'), 'amount' => $reste_fmcg];
|
|
||||||
}
|
|
||||||
|
|
||||||
$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' => tr('bud_rem_fuel'), 'amount' => $reste_essence];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// F. Calculs des KPIs finaux
|
// F. Calculs des KPIs finaux
|
||||||
$rentrees_salaires_reels = $totals['Income'] ?? 0;
|
$rentrees_salaires_reels = 0;
|
||||||
|
foreach($categoriesConfig as $code => $conf) { if($conf['db_type'] === 'Income') $rentrees_salaires_reels += ($totals[$code] ?? 0); }
|
||||||
|
|
||||||
$rentrees_autres = $total_rentrees - $rentrees_salaires_reels;
|
$rentrees_autres = $total_rentrees - $rentrees_salaires_reels;
|
||||||
$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu);
|
$salaires_retenus = max($rentrees_salaires_reels, $budget_income_prevu);
|
||||||
|
|
||||||
$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres;
|
$capacite_max_calc = $solde_initial + $salaires_retenus + $rentrees_autres;
|
||||||
|
|
||||||
// 1. On calcule s'il reste des salaires/revenus prévus à encaisser
|
|
||||||
$revenus_a_venir = max(0, $budget_income_prevu - $rentrees_salaires_reels);
|
$revenus_a_venir = max(0, $budget_income_prevu - $rentrees_salaires_reels);
|
||||||
|
|
||||||
// 2. Le solde théorique part de ta vraie saisie bancaire actuelle
|
|
||||||
$solde_theorique_calc = $snapshot['amount'] + $revenus_a_venir - $reste_a_venir_calc;
|
$solde_theorique_calc = $snapshot['amount'] + $revenus_a_venir - $reste_a_venir_calc;
|
||||||
|
|
||||||
if ($isClosed) {
|
if ($isClosed) {
|
||||||
@@ -335,7 +361,6 @@ function getDisplayLogic($spent, $bg, $type) {
|
|||||||
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
|
return ['pct' => $pct, 'isOver' => $isOver, 'text' => $text];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Noms des mois traduits
|
|
||||||
$monthNames = [
|
$monthNames = [
|
||||||
1 => tr('month_01'), 2 => tr('month_02'), 3 => tr('month_03'), 4 => tr('month_04'),
|
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'),
|
5 => tr('month_05'), 6 => tr('month_06'), 7 => tr('month_07'), 8 => tr('month_08'),
|
||||||
@@ -505,11 +530,11 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php if (!$isClosed): ?>
|
<?php if (!$isClosed): ?>
|
||||||
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes($conf['label']) ?>')">+</button>
|
<button class="btn-add-item" style="color:<?= $conf['color'] ?>;" onclick="openAddModal('<?= $key ?>', '<?= addslashes(strip_tags($conf['label'])) ?>')">+</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php $barCol = ($key === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>
|
<?php $barCol = ($conf['db_type'] === 'Income') ? '#10b981' : ($logic['isOver'] ? '#ef4444' : $conf['color']); ?>
|
||||||
<div style="background:#f1f5f9; height:4px; width:100%;">
|
<div style="background:#f1f5f9; height:4px; width:100%;">
|
||||||
<div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
|
<div style="width:<?= $logic['pct'] ?>%; background:<?= $barCol ?>; height:100%;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -568,7 +593,7 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
<label class="pf-label"><?= tr('bud_category') ?></label>
|
<label class="pf-label"><?= tr('bud_category') ?></label>
|
||||||
<select name="category" id="modalCatSelect" class="pf-input" onchange="handleModalCatChange(this)">
|
<select name="category" id="modalCatSelect" class="pf-input" onchange="handleModalCatChange(this)">
|
||||||
<?php foreach($categoriesConfig as $key => $conf): ?>
|
<?php foreach($categoriesConfig as $key => $conf): ?>
|
||||||
<option value="<?= $key ?>"><?= $conf['label'] ?></option>
|
<option value="<?= $key ?>"><?= strip_tags($conf['label']) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -587,23 +612,10 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
<datalist id="modalSuggestions"></datalist>
|
<datalist id="modalSuggestions"></datalist>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="blockInputSelect" style="margin-bottom:15px; display:none;">
|
|
||||||
<label class="pf-label"><?= tr('bud_beneficiary') ?></label>
|
|
||||||
<select name="label_select" id="schoolSelect" class="pf-input">
|
|
||||||
<?php
|
|
||||||
$stmtSchoolLabel = $pdo->query("SELECT name FROM pf_people WHERE role IN ('enfant', 'nounou') OR role IS NULL ORDER BY id ASC");
|
|
||||||
while($k = $stmtSchoolLabel->fetch(PDO::FETCH_ASSOC)) {
|
|
||||||
$pName = htmlspecialchars($k['name']);
|
|
||||||
echo "<option value='{$pName}'>{$pName}</option>";
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;">
|
<div class="form-group" id="blockInputFrais" style="margin-bottom:15px; display:none;">
|
||||||
<label class="pf-label"><?= tr('bud_fixed_charge') ?></label>
|
<label class="pf-label">Lier à une Charge Fixe (Optionnel)</label>
|
||||||
<select name="budget_item_id" id="fraisSelect" class="pf-input" disabled>
|
<select name="budget_item_id" id="fraisSelect" class="pf-input" disabled>
|
||||||
<option value=""><?= tr('bud_select_beneficiary') ?></option>
|
<option value="">-- Aucune --</option>
|
||||||
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
|
<?php foreach ($fixedChargesList as $fc): ?><option value="<?= $fc['id'] ?>"><?= htmlspecialchars($fc['name']) ?></option><?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -720,11 +732,11 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="padding:4px; font-size:0.85rem; flex:1;">
|
<select name="lines[<?= $idx ?>][cat]" class="pf-input line-select" onchange="handleLineCatChange(this)" <?= $dis ?> style="padding:4px; font-size:0.85rem; flex:1;">
|
||||||
<option value="">-- <?= $isCrd ? tr('bud_ignore') : tr('bud_to_define') ?> --</option>
|
<option value="">-- <?= $isCrd ? tr('bud_ignore') : tr('bud_to_define') ?> --</option>
|
||||||
<?php foreach ($categoriesConfig as $k => $c): ?>
|
<?php foreach ($categoriesConfig as $k => $c): ?>
|
||||||
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= $c['label'] ?></option>
|
<option value="<?= $k ?>" <?= ($row['cat']===$k)?'selected':'' ?>><?= strip_tags($c['label']) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
|
<select name="lines[<?= $idx ?>][budget_item_id]" class="pf-input select-frais" onchange="checkValidation()" style="display:none; padding:4px; font-size:0.85rem; flex:1;" disabled>
|
||||||
<option value="">-- <?= tr('bud_is_charge') ?> --</option>
|
<option value="">-- Lier à une Charge Fixe --</option>
|
||||||
<?php foreach ($fixedChargesList as $fc): ?>
|
<?php foreach ($fixedChargesList as $fc): ?>
|
||||||
<option value="<?= $fc['id'] ?>" <?= ($row['suggested_item_id'] == $fc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($fc['name']) ?></option>
|
<option value="<?= $fc['id'] ?>" <?= ($row['suggested_item_id'] == $fc['id']) ? 'selected' : '' ?>><?= htmlspecialchars($fc['name']) ?></option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -757,7 +769,6 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
document.body.classList.add('no-scroll');
|
document.body.classList.add('no-scroll');
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
// --- 1. SÉCURISATION TRADUCTIONS ET LANGUE ---
|
|
||||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||||
window.I18N = {
|
window.I18N = {
|
||||||
...(window.I18N || {}),
|
...(window.I18N || {}),
|
||||||
@@ -769,34 +780,42 @@ window.I18N = {
|
|||||||
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- DICTIONNAIRE JS DYNAMIQUE ---
|
||||||
|
const catConfigs = <?= json_encode($categoriesConfig) ?>;
|
||||||
|
|
||||||
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
|
const activeViewMonth = '<?= substr($viewMonthDate, 0, 7) ?>';
|
||||||
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
|
function toggleDiv(id) { const el = document.getElementById(id); el.style.display = (el.style.display === 'none') ? 'block' : 'none'; }
|
||||||
function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('no-scroll'); }
|
function openSuiviModal(id) { document.getElementById(id).classList.add('open'); document.body.classList.add('no-scroll'); }
|
||||||
function closeSuiviModal(id) { document.getElementById(id).classList.remove('open'); document.body.classList.remove('no-scroll');}
|
function closeSuiviModal(id) { document.getElementById(id).classList.remove('open'); document.body.classList.remove('no-scroll');}
|
||||||
|
|
||||||
const suggestions = <?= json_encode(array_map(fn($c) => $c['suggestions'], $categoriesConfig)) ?>;
|
// Gestion dynamique du formulaire modal
|
||||||
|
|
||||||
function handleModalCatChange(select) {
|
function handleModalCatChange(select) {
|
||||||
const catKey = select.value;
|
const catKey = select.value;
|
||||||
|
const conf = catConfigs[catKey] || {db_type: 'Expense', suggestions: []};
|
||||||
|
|
||||||
document.getElementById('blockInputText').style.display = 'none';
|
document.getElementById('blockInputText').style.display = 'block';
|
||||||
document.getElementById('blockInputSelect').style.display = 'none';
|
|
||||||
document.getElementById('blockInputFrais').style.display = 'none';
|
document.getElementById('blockInputFrais').style.display = 'none';
|
||||||
document.getElementById('blockInputIncome').style.display = 'none';
|
document.getElementById('blockInputIncome').style.display = 'none';
|
||||||
|
|
||||||
document.getElementById('modalLabelInput').required = false;
|
document.getElementById('modalLabelInput').required = true;
|
||||||
document.getElementById('fraisSelect').required = false;
|
document.getElementById('fraisSelect').required = false;
|
||||||
document.getElementById('incomeSelect').required = false;
|
document.getElementById('incomeSelect').required = false;
|
||||||
document.getElementById('fraisSelect').disabled = true;
|
document.getElementById('fraisSelect').disabled = true;
|
||||||
document.getElementById('incomeSelect').disabled = true;
|
document.getElementById('incomeSelect').disabled = true;
|
||||||
|
|
||||||
if (catKey === 'School') { document.getElementById('blockInputSelect').style.display = 'block'; }
|
if (conf.db_type === 'Income') {
|
||||||
else if (catKey === 'Frais') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputFrais').style.display = 'block'; document.getElementById('fraisSelect').required = true; document.getElementById('fraisSelect').disabled = false; }
|
document.getElementById('blockInputIncome').style.display = 'block';
|
||||||
else if (catKey === 'Income') { document.getElementById('blockInputText').style.display = 'block'; document.getElementById('blockInputIncome').style.display = 'block'; document.getElementById('incomeSelect').required = true; document.getElementById('incomeSelect').disabled = false; }
|
document.getElementById('incomeSelect').disabled = false;
|
||||||
else {
|
document.getElementById('incomeSelect').required = true;
|
||||||
document.getElementById('blockInputText').style.display = 'block'; document.getElementById('modalLabelInput').required = true;
|
} else {
|
||||||
const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
|
// Optionnel : lier l'Expense à une charge fixe
|
||||||
if (suggestions[catKey]) { suggestions[catKey].forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); }); }
|
document.getElementById('blockInputFrais').style.display = 'block';
|
||||||
|
document.getElementById('fraisSelect').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = document.getElementById('modalSuggestions'); list.innerHTML = '';
|
||||||
|
if (conf.suggestions) {
|
||||||
|
conf.suggestions.forEach(i => { const op = document.createElement('option'); op.value = i; list.appendChild(op); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,8 +827,10 @@ function openAddModal(catKey, catLabel) {
|
|||||||
document.getElementById('modalGestionMonth').value = activeViewMonth;
|
document.getElementById('modalGestionMonth').value = activeViewMonth;
|
||||||
document.getElementById('modalLabelInput').value = "";
|
document.getElementById('modalLabelInput').value = "";
|
||||||
document.getElementById('modalAmount').value = "";
|
document.getElementById('modalAmount').value = "";
|
||||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey; handleModalCatChange(catSelect);
|
|
||||||
document.getElementById('modalIsCredit').value = (catKey === 'Income') ? "1" : "0";
|
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = catKey;
|
||||||
|
handleModalCatChange(catSelect);
|
||||||
|
document.getElementById('modalIsCredit').value = (catConfigs[catKey]?.db_type === 'Income') ? "1" : "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditModal(e) {
|
function openEditModal(e) {
|
||||||
@@ -821,21 +842,30 @@ function openEditModal(e) {
|
|||||||
document.getElementById('modalLabelInput').value = e.label;
|
document.getElementById('modalLabelInput').value = e.label;
|
||||||
document.getElementById('modalIsCredit').value = parseFloat(e.amount) > 0 ? "1" : "0";
|
document.getElementById('modalIsCredit').value = parseFloat(e.amount) > 0 ? "1" : "0";
|
||||||
document.getElementById('modalAmount').value = Math.abs(parseFloat(e.amount));
|
document.getElementById('modalAmount').value = Math.abs(parseFloat(e.amount));
|
||||||
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category; handleModalCatChange(catSelect);
|
|
||||||
if (e.category === 'Frais') document.getElementById('fraisSelect').value = e.budget_item_id;
|
const catSelect = document.getElementById('modalCatSelect'); catSelect.value = e.category;
|
||||||
else if (e.category === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
|
handleModalCatChange(catSelect);
|
||||||
|
|
||||||
|
if (catConfigs[e.category]?.db_type === 'Income') document.getElementById('incomeSelect').value = e.budget_item_id;
|
||||||
|
else document.getElementById('fraisSelect').value = e.budget_item_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gestion dynamique du CSV Import
|
||||||
function handleLineCatChange(select, isInit = false) {
|
function handleLineCatChange(select, isInit = false) {
|
||||||
const row = select.closest('tr');
|
const row = select.closest('tr');
|
||||||
const fSel = row.querySelector('.select-frais'); const iSel = row.querySelector('.select-income');
|
const fSel = row.querySelector('.select-frais');
|
||||||
|
const iSel = row.querySelector('.select-income');
|
||||||
|
const conf = catConfigs[select.value] || null;
|
||||||
|
|
||||||
fSel.style.display = 'none'; iSel.style.display = 'none';
|
fSel.style.display = 'none'; iSel.style.display = 'none';
|
||||||
if (!isInit) { fSel.value = ''; iSel.value = ''; }
|
if (!isInit) { fSel.value = ''; iSel.value = ''; }
|
||||||
fSel.disabled = true; iSel.disabled = true;
|
fSel.disabled = true; iSel.disabled = true;
|
||||||
|
|
||||||
if (select.value === 'Frais') { fSel.style.display = 'block'; fSel.disabled = false; }
|
if (conf && conf.db_type === 'Income') {
|
||||||
else if (select.value === 'Income') { iSel.style.display = 'block'; iSel.disabled = false; }
|
iSel.style.display = 'block'; iSel.disabled = false;
|
||||||
|
} else if (conf) {
|
||||||
|
fSel.style.display = 'block'; fSel.disabled = false;
|
||||||
|
}
|
||||||
checkValidation();
|
checkValidation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,16 +874,25 @@ function toggleAll(src) { document.querySelectorAll('.line-checkbox:not([disable
|
|||||||
function checkValidation() {
|
function checkValidation() {
|
||||||
let miss = 0;
|
let miss = 0;
|
||||||
document.querySelectorAll('.line-checkbox:checked').forEach(cb => {
|
document.querySelectorAll('.line-checkbox:checked').forEach(cb => {
|
||||||
const row = cb.closest('tr'); const isCrd = row.querySelector('.is-credit-flag').value === '1'; const cat = row.querySelector('.line-select').value;
|
const row = cb.closest('tr');
|
||||||
let v = true; if (cat==="") { if(!isCrd) v = false; } else if (cat==='Frais' && row.querySelector('.select-frais').value==="") v=false; else if (cat==='Income' && row.querySelector('.select-income').value==="") v=false;
|
const isCrd = row.querySelector('.is-credit-flag').value === '1';
|
||||||
|
const cat = row.querySelector('.line-select').value;
|
||||||
|
const conf = catConfigs[cat] || null;
|
||||||
|
|
||||||
|
let v = true;
|
||||||
|
if (cat === "") {
|
||||||
|
if (!isCrd) v = false;
|
||||||
|
} else if (conf && conf.db_type === 'Income' && row.querySelector('.select-income').value === "") {
|
||||||
|
v = false;
|
||||||
|
}
|
||||||
if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = '';
|
if (!v) { miss++; row.style.background = '#fff1f2'; } else row.style.background = '';
|
||||||
});
|
});
|
||||||
const btn = document.getElementById('btnImport'); const msg = document.getElementById('missingCount');
|
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 + ' ' + (window.I18N['bud_to_define_js'] || ''); }
|
if(miss > 0) { btn.disabled = true; btn.style.opacity = 0.5; msg.style.display = 'inline'; msg.innerText = miss + ' ' + (window.I18N['bud_to_define_js'] || ''); }
|
||||||
else { btn.disabled = false; btn.style.opacity=1; msg.style.display='none'; }
|
else { btn.disabled = false; btn.style.opacity = 1; msg.style.display = 'none'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
if(document.getElementById('formMapping')) {
|
if (document.getElementById('formMapping')) {
|
||||||
document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s, true));
|
document.querySelectorAll('.line-select').forEach(s => handleLineCatChange(s, true));
|
||||||
checkValidation();
|
checkValidation();
|
||||||
}
|
}
|
||||||
@@ -897,7 +936,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
submitBtn.innerText = '⏳ ...';
|
submitBtn.innerText = '⏳ ...';
|
||||||
|
|
||||||
const formData = new FormData(formExpense);
|
const formData = new FormData(formExpense);
|
||||||
// Force l'action ici pour être sûr
|
|
||||||
formData.set('action', 'save_expense_manual');
|
formData.set('action', 'save_expense_manual');
|
||||||
|
|
||||||
const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php';
|
const actionUrl = formExpense.getAttribute('action') || 'modules/budget/includes/api/manage-item.php';
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* En-tête & Boutons */
|
/* =========================================================
|
||||||
|
EN-TÊTE & BOUTONS
|
||||||
|
========================================================= */
|
||||||
.fc-header-row {
|
.fc-header-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -62,7 +64,9 @@
|
|||||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Calendrier Mensuel */
|
/* =========================================================
|
||||||
|
CALENDRIER MENSUEL (Vue Grille Mois)
|
||||||
|
========================================================= */
|
||||||
.fc-month-calendar-wrapper {
|
.fc-month-calendar-wrapper {
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid var(--pf-border);
|
border: 1px solid var(--pf-border);
|
||||||
@@ -114,6 +118,7 @@
|
|||||||
|
|
||||||
.fc-view-controls {
|
.fc-view-controls {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
background: #f1f5f9 !important;
|
background: #f1f5f9 !important;
|
||||||
padding: 4px !important;
|
padding: 4px !important;
|
||||||
@@ -128,7 +133,7 @@
|
|||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--pf-text-muted);
|
color: #64748b;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: 0.2s;
|
transition: 0.2s;
|
||||||
}
|
}
|
||||||
@@ -206,7 +211,9 @@
|
|||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Décompte Congés (Pills) */
|
/* =========================================================
|
||||||
|
DÉCOMPTE CONGÉS MENSUEL (PILLS)
|
||||||
|
========================================================= */
|
||||||
.fc-month-balances {
|
.fc-month-balances {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -226,12 +233,6 @@
|
|||||||
border: 1px solid var(--pf-border);
|
border: 1px solid var(--pf-border);
|
||||||
box-shadow: var(--pf-shadow-sm);
|
box-shadow: var(--pf-shadow-sm);
|
||||||
}
|
}
|
||||||
.fc-minimal-balance-card strong.alex {
|
|
||||||
color: var(--text-alex);
|
|
||||||
}
|
|
||||||
.fc-minimal-balance-card strong.laia {
|
|
||||||
color: var(--text-laia);
|
|
||||||
}
|
|
||||||
.fc-minimal-chips {
|
.fc-minimal-chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -249,17 +250,17 @@
|
|||||||
}
|
}
|
||||||
.fc-min-chip .type {
|
.fc-min-chip .type {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--pf-text-muted);
|
color: #64748b;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
.fc-min-chip .val {
|
.fc-min-chip .val {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: var(--pf-text-main);
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
.fc-used-badge {
|
.fc-used-badge {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
background: #fee2e2;
|
background: #fee2e2;
|
||||||
color: var(--pf-danger);
|
color: #ef4444;
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
@@ -272,7 +273,7 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--pf-danger);
|
color: #ef4444;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@keyframes pulseBurn {
|
@keyframes pulseBurn {
|
||||||
@@ -290,7 +291,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Couleurs des jours */
|
/* =========================================================
|
||||||
|
COULEURS DES JOURS (GRILLES)
|
||||||
|
========================================================= */
|
||||||
.fc-day--school-holiday {
|
.fc-day--school-holiday {
|
||||||
background: var(--c-school-holiday) !important;
|
background: var(--c-school-holiday) !important;
|
||||||
}
|
}
|
||||||
@@ -304,35 +307,13 @@
|
|||||||
.fc-day--extra-off-carole {
|
.fc-day--extra-off-carole {
|
||||||
background: var(--c-extra-off) !important;
|
background: var(--c-extra-off) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-day--selected {
|
.fc-day--selected {
|
||||||
background: var(--c-selected) !important;
|
background: var(--c-selected) !important;
|
||||||
outline: 2px solid var(--pf-primary);
|
outline: 2px solid var(--pf-primary);
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
.fc-day--centre::after {
|
|
||||||
content: "🏫";
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.fc-day--avis::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
background: url("/modules/family-calendar/assets/img/avis.svg") no-repeat
|
|
||||||
center;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
.fc-pep-sick-emoji {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 2px;
|
|
||||||
right: 2px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.fc-day--off-carole.fc-day--school-holiday,
|
.fc-day--off-carole.fc-day--school-holiday,
|
||||||
.fc-day--extra-off-carole.fc-day--school-holiday,
|
.fc-day--extra-off-carole.fc-day--school-holiday,
|
||||||
.fc-day--centre.fc-day--school-holiday,
|
.fc-day--centre.fc-day--school-holiday,
|
||||||
@@ -340,7 +321,26 @@
|
|||||||
box-shadow: inset 0 -8px 0 0 var(--c-school-holiday) !important;
|
box-shadow: inset 0 -8px 0 0 var(--c-school-holiday) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Planning Hebo (Desktop) */
|
.fc-day--today {
|
||||||
|
background: #eff6ff !important;
|
||||||
|
box-shadow: inset 0 0 0 1px #bfdbfe !important;
|
||||||
|
}
|
||||||
|
.fc-day--today .fc-day-number {
|
||||||
|
display: inline;
|
||||||
|
background: none;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--pf-primary);
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
PLANNING GLOBAL (Matrice Annuelle)
|
||||||
|
========================================================= */
|
||||||
.fc-week-header {
|
.fc-week-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -379,22 +379,17 @@
|
|||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* HARMONISATION DES BORDURES ET HEADERS STICKY */
|
||||||
HARMONISATION DES BORDURES ET HEADERS STICKY (1px strict)
|
|
||||||
========================================================= */
|
|
||||||
#planningTable thead tr {
|
#planningTable thead tr {
|
||||||
height: 30px; /* On fixe la hauteur pour un calcul parfait */
|
height: 30px;
|
||||||
}
|
}
|
||||||
#planningTable thead th {
|
#planningTable thead th {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
background: #f8fafc !important;
|
background: #f8fafc !important;
|
||||||
border: none !important; /* On supprime les bordures natives */
|
border: none !important;
|
||||||
|
|
||||||
/* On utilise UNIQUEMENT des ombres internes pour simuler 1px de bordure sans doublement */
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 -1px 0 var(--pf-border),
|
inset 0 -1px 0 var(--pf-border),
|
||||||
inset -1px 0 0 var(--pf-border) !important;
|
inset -1px 0 0 var(--pf-border) !important;
|
||||||
|
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
@@ -403,14 +398,14 @@
|
|||||||
}
|
}
|
||||||
#planningTable thead tr:nth-child(1) th {
|
#planningTable thead tr:nth-child(1) th {
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 22; /* Au premier plan */
|
z-index: 22;
|
||||||
}
|
}
|
||||||
#planningTable thead tr:nth-child(2) th {
|
#planningTable thead tr:nth-child(2) th {
|
||||||
top: 30px; /* Exactement la hauteur de la ligne 1 */
|
top: 30px;
|
||||||
z-index: 21;
|
z-index: 21;
|
||||||
}
|
}
|
||||||
#planningTable thead tr:nth-child(3) th {
|
#planningTable thead tr:nth-child(3) th {
|
||||||
top: 60px; /* Ligne 1 + Ligne 2 */
|
top: 60px;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,10 +420,9 @@
|
|||||||
color: var(--text-laia) !important;
|
color: var(--text-laia) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cellules standards du corps */
|
|
||||||
#planningTable tbody td {
|
#planningTable tbody td {
|
||||||
position: relative;
|
position: relative;
|
||||||
border: none !important; /* Reset de sécurité */
|
border: none !important;
|
||||||
border-right: 1px solid var(--pf-border) !important;
|
border-right: 1px solid var(--pf-border) !important;
|
||||||
border-bottom: 1px solid var(--pf-border) !important;
|
border-bottom: 1px solid var(--pf-border) !important;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@@ -467,13 +461,10 @@ td.col-laia-sub {
|
|||||||
writing-mode: vertical-rl;
|
writing-mode: vertical-rl;
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
color: var(--pf-text-muted);
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* GESTION DES COLONNES STICKY (Mois & Semaine) */
|
||||||
GESTION DES COLONNES STICKY (Mois & Semaine)
|
|
||||||
========================================================= */
|
|
||||||
|
|
||||||
#planningTable tbody td.col-sticky-mois {
|
#planningTable tbody td.col-sticky-mois {
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
@@ -481,7 +472,6 @@ td.col-laia-sub {
|
|||||||
background: white !important;
|
background: white !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#planningTable tbody td.col-sticky-mois .fc-sticky-mois-label {
|
#planningTable tbody td.col-sticky-mois .fc-sticky-mois-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
@@ -491,42 +481,31 @@ td.col-laia-sub {
|
|||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. Sticky Semaine (Body) */
|
|
||||||
#planningTable tbody td.col-sticky-sem {
|
#planningTable tbody td.col-sticky-sem {
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
left: 54px !important;
|
left: 54px !important;
|
||||||
z-index: 14 !important;
|
z-index: 14 !important;
|
||||||
background: white !important;
|
background: white !important;
|
||||||
/* Ombre portée pour délimiter la zone sticky du reste du tableau */
|
|
||||||
box-shadow: 4px 0 6px -2px rgba(0, 0, 0, 0.1);
|
box-shadow: 4px 0 6px -2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3. Sticky Mois (Header) */
|
|
||||||
#planningTable thead tr th.col-sticky-mois {
|
#planningTable thead tr th.col-sticky-mois {
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
top: 0 !important;
|
top: 0 !important;
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
z-index: 30 !important;
|
z-index: 30 !important;
|
||||||
background: var(--pf-bg-lighter) !important;
|
background: var(--pf-bg-lighter) !important;
|
||||||
/* Les bordures sont gérées par le box-shadow général du thead th */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 4. Sticky Semaine (Header) */
|
|
||||||
#planningTable thead tr th.col-sticky-sem {
|
#planningTable thead tr th.col-sticky-sem {
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
top: 0 !important;
|
top: 0 !important;
|
||||||
left: 54px !important;
|
left: 54px !important;
|
||||||
z-index: 29 !important;
|
z-index: 29 !important;
|
||||||
background: var(--pf-bg-lighter) !important;
|
background: var(--pf-bg-lighter) !important;
|
||||||
/* Ombre interne (bordures) + Ombre portée (effet sticky) */
|
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 -1px 0 var(--pf-border),
|
inset 0 -1px 0 var(--pf-border),
|
||||||
inset -1px 0 0 var(--pf-border),
|
inset -1px 0 0 var(--pf-border),
|
||||||
4px 0 6px -2px rgba(0, 0, 0, 0.1) !important;
|
4px 0 6px -2px rgba(0, 0, 0, 0.1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rétablissement horizontal pour les headers mois et semaine */
|
|
||||||
#planningTable thead th.col-month,
|
#planningTable thead th.col-month,
|
||||||
#planningTable tbody td.col-sticky-sem {
|
#planningTable tbody td.col-sticky-sem {
|
||||||
writing-mode: horizontal-tb;
|
writing-mode: horizontal-tb;
|
||||||
@@ -535,15 +514,15 @@ td.col-laia-sub {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Panneaux bas & Légendes */
|
/* =========================================================
|
||||||
|
PANNEAUX BAS, LÉGENDES & RÉSUMÉS
|
||||||
|
========================================================= */
|
||||||
.fc-bottom-grid {
|
.fc-bottom-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1fr;
|
grid-template-columns: 2fr 1fr;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nouvelles bordures style "Budget" pour les conteneurs */
|
|
||||||
.pf-card {
|
.pf-card {
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid var(--pf-border);
|
border: 1px solid var(--pf-border);
|
||||||
@@ -560,6 +539,7 @@ td.col-laia-sub {
|
|||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Légendes statiques */
|
||||||
.pf-legend-grid {
|
.pf-legend-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -607,53 +587,62 @@ td.col-laia-sub {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-summary-header {
|
/* Résumé Mensuel Injecté */
|
||||||
|
.fc-month-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: 8px;
|
||||||
margin-bottom: 16px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 1px solid var(--pf-border);
|
|
||||||
}
|
}
|
||||||
.fc-summ-select {
|
.fc-month-summary-inline {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 5px;
|
||||||
|
background: var(--pf-bg-lighter, #f8fafc);
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
border: 1px solid var(--pf-border);
|
border: 1px solid var(--pf-border);
|
||||||
border-radius: 6px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
height: auto;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
}
|
||||||
.fc-summary-item {
|
.fc-summ-pill {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
padding: 12px 4px;
|
align-items: center;
|
||||||
border-bottom: 1px solid var(--pf-border); /* Ligne de séparation visible */
|
font-size: 0.75rem;
|
||||||
}
|
|
||||||
.fc-summary-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.fc-summary-label {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-weight: 600;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.fc-summary-value {
|
.fc-summ-pill strong {
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.pf-presence-pill strong {
|
||||||
|
color: var(--pf-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Menu Contextuel Tactile */
|
[data-theme="dark"] .fc-month-summary-inline {
|
||||||
|
background: #1c2128;
|
||||||
|
border-color: #30363d;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .fc-summ-pill strong {
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
MENU CONTEXTUEL (COMPACT & PROPRE)
|
||||||
|
========================================================= */
|
||||||
|
#selectionMenu,
|
||||||
|
#fc-month-selectionMenu,
|
||||||
.fc-selection-menu {
|
.fc-selection-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 9000;
|
z-index: 9000;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
border: 1px solid rgba(226, 232, 240, 0.8);
|
||||||
box-shadow: var(--pf-shadow-lg);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||||
border-radius: 16px;
|
border-radius: 12px;
|
||||||
padding: 10px 12px;
|
padding: 8px 12px;
|
||||||
min-width: 220px;
|
min-width: 210px;
|
||||||
display: none;
|
display: none;
|
||||||
animation: menuPopIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: menuPopIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
transform-origin: top center;
|
transform-origin: top center;
|
||||||
@@ -668,88 +657,70 @@ td.col-laia-sub {
|
|||||||
transform: scale(1) translateY(0);
|
transform: scale(1) translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-menu-section {
|
.fc-menu-section {
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
border-bottom: 1px dashed #cbd5e1;
|
border-bottom: 1px dashed #cbd5e1;
|
||||||
}
|
}
|
||||||
.fc-menu-section:last-child {
|
.fc-menu-section:last-child {
|
||||||
border: none;
|
border-bottom: none;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
.fc-menu-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
.fc-menu-section strong {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--pf-text-muted);
|
|
||||||
}
|
|
||||||
.fc-menu-grid {
|
.fc-menu-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.fc-menu-btn {
|
.fc-menu-btn {
|
||||||
background: white;
|
background: transparent;
|
||||||
border: 1px solid var(--pf-border);
|
border: 1px solid transparent;
|
||||||
padding: 6px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: 0.2s;
|
text-align: left;
|
||||||
box-shadow: var(--pf-shadow-sm);
|
transition: all 0.2s ease;
|
||||||
|
color: #0f172a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.fc-menu-btn:hover {
|
.fc-menu-btn:hover {
|
||||||
background: var(--pf-bg-page);
|
background: #f1f5f9;
|
||||||
border-color: var(--pf-primary);
|
|
||||||
color: var(--pf-primary);
|
|
||||||
}
|
}
|
||||||
.fc-menu-clear-icon {
|
|
||||||
background: transparent;
|
/* --- LE BOUTON ACTIF (SANS COCHE !) --- */
|
||||||
border: none;
|
.fc-menu-btn--active {
|
||||||
color: #94a3b8;
|
border: 1px solid var(--pf-primary) !important;
|
||||||
cursor: pointer;
|
background: #eff6ff !important;
|
||||||
padding: 4px;
|
color: var(--pf-primary) !important;
|
||||||
display: flex;
|
font-weight: 700 !important;
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
.fc-menu-clear-icon svg {
|
/* Sécurité absolue pour tuer les coches fantômes */
|
||||||
|
.fc-menu-btn--active::before,
|
||||||
|
.fc-menu-btn--active::after {
|
||||||
|
display: none !important;
|
||||||
|
content: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icônes intégrées dans le JS */
|
||||||
|
.fc-icon-centre {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.fc-icon-avis {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
object-fit: contain;
|
||||||
.fc-menu-clear-icon:hover {
|
margin-right: 4px;
|
||||||
background: #fef2f2;
|
vertical-align: middle;
|
||||||
color: var(--pf-danger);
|
|
||||||
}
|
|
||||||
.fc-menu-leaves-table table {
|
|
||||||
width: 100%;
|
|
||||||
border-spacing: 2px;
|
|
||||||
}
|
|
||||||
.fc-menu-leaves-table th {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--pf-text-muted);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.fc-th-inline {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- BOUTON DE MENU ACTIF (Élément déjà enregistré) --- */
|
/* =========================================================
|
||||||
.fc-menu-btn--active {
|
MODALES (Paramètres & Vacances)
|
||||||
background: #eff6ff;
|
========================================================= */
|
||||||
border-color: var(--pf-primary);
|
|
||||||
color: var(--pf-primary);
|
|
||||||
/* Un léger inset shadow pour rendre la bordure un peu plus épaisse sans casser la taille */
|
|
||||||
box-shadow: inset 0 0 0 1px var(--pf-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table specifique modale vacances */
|
|
||||||
.fc-holidays-table {
|
.fc-holidays-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -772,7 +743,6 @@ td.col-laia-sub {
|
|||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === MODALES : Alignement du Header et de la Croix === */
|
|
||||||
.pf-modal-header {
|
.pf-modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -781,14 +751,12 @@ td.col-laia-sub {
|
|||||||
border-bottom: 1px solid var(--pf-border);
|
border-bottom: 1px solid var(--pf-border);
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-modal-header .pf-modal-title {
|
.pf-modal-header .pf-modal-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-bottom: none; /* On annule la bordure par défaut du global.css pour la déléguer au header */
|
border-bottom: none;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-modal-close {
|
.pf-modal-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -799,9 +767,37 @@ td.col-laia-sub {
|
|||||||
padding: 0 0 0 15px;
|
padding: 0 0 0 15px;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pf-modal-close:hover {
|
.pf-modal-close:hover {
|
||||||
color: #ef4444; /* Devient rouge au survol */
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-sidebar-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 1.2rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #0f172a;
|
||||||
|
transition: all 0.2s;
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
margin: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.fc-sidebar-tab.active {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.fc-sidebar-tab:hover:not(.active) {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .fc-sidebar-tab.active {
|
||||||
|
background: var(--pf-primary);
|
||||||
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
@@ -811,36 +807,25 @@ td.col-laia-sub {
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #cbd5e1;
|
border: 1px solid #cbd5e1;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-main);
|
color: #0f172a;
|
||||||
|
|
||||||
/* Padding standard */
|
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
box-shadow: var(--pf-shadow-sm);
|
box-shadow: var(--pf-shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-smart-select:hover {
|
.fc-smart-select:hover {
|
||||||
border-color: #94a3b8;
|
border-color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-smart-select:focus {
|
.fc-smart-select:focus {
|
||||||
background: #ffffff;
|
border-color: var(--pf-primary);
|
||||||
border-color: var(--primary);
|
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- BOUTON AUJOURD'HUI & CONTROLES --- */
|
|
||||||
.fc-view-controls {
|
|
||||||
align-items: center; /* Pour bien centrer le nouveau bouton */
|
|
||||||
}
|
|
||||||
|
|
||||||
.fc-today-button {
|
.fc-today-button {
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid var(--pf-border);
|
border: 1px solid var(--pf-border);
|
||||||
@@ -853,12 +838,10 @@ td.col-laia-sub {
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
box-shadow: var(--pf-shadow-sm);
|
box-shadow: var(--pf-shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-today-button:hover {
|
.fc-today-button:hover {
|
||||||
background: #eff6ff;
|
background: #eff6ff;
|
||||||
border-color: var(--pf-primary);
|
border-color: var(--pf-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-view-divider {
|
.fc-view-divider {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@@ -866,34 +849,14 @@ td.col-laia-sub {
|
|||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- MISE EN ÉVIDENCE DU JOUR EN COURS --- */
|
/* =========================================================
|
||||||
.fc-day--today {
|
MOBILE RESPONSIVE
|
||||||
background: #eff6ff !important;
|
========================================================= */
|
||||||
box-shadow: inset 0 0 0 1px #bfdbfe !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fc-day--today .fc-day-number {
|
|
||||||
display: inline;
|
|
||||||
background: none;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
box-shadow: none;
|
|
||||||
border-radius: 0;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--pf-primary);
|
|
||||||
font-weight: 900;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.fc-smart-select {
|
.fc-smart-select {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.pf-family-calendar .pf-container {
|
.pf-family-calendar .pf-container {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -927,6 +890,9 @@ td.col-laia-sub {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#selectionMenu,
|
||||||
|
#fc-month-selectionMenu,
|
||||||
.fc-selection-menu {
|
.fc-selection-menu {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
top: auto !important;
|
top: auto !important;
|
||||||
@@ -939,7 +905,8 @@ td.col-laia-sub {
|
|||||||
animation: slideUpSheet 0.3s forwards;
|
animation: slideUpSheet 0.3s forwards;
|
||||||
z-index: 10000 !important;
|
z-index: 10000 !important;
|
||||||
}
|
}
|
||||||
.fc-selection-menu::before {
|
#selectionMenu::before,
|
||||||
|
#fc-month-selectionMenu::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@@ -955,39 +922,8 @@ td.col-laia-sub {
|
|||||||
gap: 20px !important;
|
gap: 20px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@keyframes slideUpSheet {
|
||||||
/* ==========================================================================
|
to {
|
||||||
MODALE PARAMÈTRES (SIDEBAR STYLE)
|
transform: translateY(0);
|
||||||
========================================================================== */
|
}
|
||||||
.fc-sidebar-tab {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 1.2rem;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--text-main);
|
|
||||||
transition: all 0.2s;
|
|
||||||
width: calc(100% - 16px);
|
|
||||||
margin: 2px 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fc-sidebar-tab.active {
|
|
||||||
background: #111827; /* Gris très foncé typique des sidebars */
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fc-sidebar-tab:hover:not(.active) {
|
|
||||||
background: var(--bg-soft, #f1f5f9);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Variante Dark Mode native pour l'onglet actif */
|
|
||||||
[data-theme="dark"] .fc-sidebar-tab.active {
|
|
||||||
background: var(--primary);
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -52,3 +52,10 @@
|
|||||||
[2026-05-06T20:43:43+02:00] RAW INPUT: [{"date":"2026-05-06","type":"AVIS","duration":1,"person":"Carole"}]
|
[2026-05-06T20:43:43+02:00] RAW INPUT: [{"date":"2026-05-06","type":"AVIS","duration":1,"person":"Carole"}]
|
||||||
[2026-05-06T20:44:36+02:00] RAW INPUT: [{"date":"2026-05-06","type":"PEP_SICK","duration":1,"person":"Carole"}]
|
[2026-05-06T20:44:36+02:00] RAW INPUT: [{"date":"2026-05-06","type":"PEP_SICK","duration":1,"person":"Carole"}]
|
||||||
[2026-05-20T16:40:17+02:00] RAW INPUT: [{"date":"2026-05-19","type":"OFF_CAROLE","duration":1,"person":"Carole"}]
|
[2026-05-20T16:40:17+02:00] RAW INPUT: [{"date":"2026-05-19","type":"OFF_CAROLE","duration":1,"person":"Carole"}]
|
||||||
|
[2026-06-16T14:20:35+02:00] RAW INPUT: [{"date":"2026-07-29","type":"CARE_MODE","duration":1,"person":"Centre"}]
|
||||||
|
[2026-06-16T14:20:43+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CARE_MODE","duration":1,"person":"Centre"}]
|
||||||
|
[2026-06-16T14:22:51+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CHILD_SICK","duration":1,"person":"5"}]
|
||||||
|
[2026-06-16T14:23:10+02:00] RAW INPUT: [{"date":"2026-06-16","type":"CHILD_SICK","duration":1,"person":"5"}]
|
||||||
|
[2026-06-16T14:23:33+02:00] RAW INPUT: [{"date":"2026-06-18","type":"CARE_MODE","duration":1,"person":"Nounou"}]
|
||||||
|
[2026-06-16T14:23:54+02:00] RAW INPUT: [{"date":"2026-06-11","type":"HELPER_OFF","duration":1,"person":"1"}]
|
||||||
|
[2026-06-16T14:23:57+02:00] RAW INPUT: [{"date":"2026-06-25","type":"CHILD_SICK","duration":1,"person":"4"}]
|
||||||
|
|||||||
@@ -8,77 +8,98 @@ $action = $input['action'] ?? '';
|
|||||||
|
|
||||||
if (!$action) {
|
if (!$action) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Action manquante.']);
|
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action manquante.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- SUPPRESSION UNITAIRE ---
|
|
||||||
if ($action === 'delete') {
|
if ($action === 'delete') {
|
||||||
$eventId = (int)($input['event_id'] ?? 0);
|
$eventId = (int)($input['event_id'] ?? 0);
|
||||||
if ($eventId <= 0) {
|
if ($eventId <= 0) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'ID manquant.']);
|
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'ID manquant.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
|
$stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?");
|
||||||
$stmt->execute([$eventId]);
|
$stmt->execute([$eventId]);
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MISE À JOUR UNITAIRE ---
|
|
||||||
if ($action === 'update') {
|
if ($action === 'update') {
|
||||||
$eventId = (int)($input['event_id'] ?? 0);
|
$eventId = (int)($input['event_id'] ?? 0);
|
||||||
$newType = $input['new_type'] ?? '';
|
$newType = $input['new_type'] ?? '';
|
||||||
if ($eventId <= 0 || !$newType) {
|
if ($eventId <= 0 || !$newType) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Données manquantes.']);
|
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Données manquantes.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
|
$stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?");
|
||||||
$stmt->execute([$newType, $eventId]);
|
$stmt->execute([$newType, $eventId]);
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SUPPRESSION DE MASSE (Par date et type) ---
|
if ($action === 'bulk_delete_day_types_person') {
|
||||||
// Utilisé quand on ajoute un événement pour nettoyer les doublons potentiels (ex: Off vs Extra)
|
|
||||||
if ($action === 'bulk_delete_day_types') {
|
|
||||||
$dates = $input['dates'] ?? [];
|
$dates = $input['dates'] ?? [];
|
||||||
$types = $input['types'] ?? [];
|
$types = $input['types'] ?? [];
|
||||||
$dates = array_filter($dates, function($d) {
|
$person_id = $input['person_id'] ?? null;
|
||||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
|
|
||||||
});
|
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||||
|
|
||||||
if (empty($dates) || empty($types)) {
|
if (empty($dates) || empty($types)) {
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
|
||||||
|
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
|
||||||
|
|
||||||
|
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
|
||||||
|
$params = array_merge($dates, $types);
|
||||||
|
|
||||||
|
// 🔥 LE CORRECTIF : On cherche le 0 en base de données
|
||||||
|
if ($person_id === null || $person_id === '' || (int)$person_id === 0) {
|
||||||
|
$sql .= " AND person_id = 0";
|
||||||
|
} else {
|
||||||
|
$sql .= " AND person_id = ?";
|
||||||
|
$params[] = (int)$person_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'bulk_delete_day_types') {
|
||||||
|
$dates = $input['dates'] ?? [];
|
||||||
|
$types = $input['types'] ?? [];
|
||||||
|
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||||
|
|
||||||
|
if (empty($dates) || empty($types)) {
|
||||||
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Création des placeholders IN (?,?,?)
|
|
||||||
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
|
$datePlaceholders = implode(',', array_fill(0, count($dates), '?'));
|
||||||
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
|
$typePlaceholders = implode(',', array_fill(0, count($types), '?'));
|
||||||
|
|
||||||
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
|
$sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)";
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
// Fusion des tableaux pour l'exécution
|
|
||||||
$stmt->execute(array_merge($dates, $types));
|
$stmt->execute(array_merge($dates, $types));
|
||||||
|
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SUPPRESSION TOTALE SUR DES DATES ---
|
|
||||||
if ($action === 'bulk_delete_all') {
|
if ($action === 'bulk_delete_all') {
|
||||||
$dates = $input['dates'] ?? [];
|
$dates = $input['dates'] ?? [];
|
||||||
$dates = array_filter($dates, function($d) {
|
$dates = array_filter($dates, function($d) { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); });
|
||||||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (empty($dates) || empty($types)) {
|
if (empty($dates)) {
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,15 +108,15 @@ try {
|
|||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute($dates);
|
$stmt->execute($dates);
|
||||||
|
|
||||||
echo json_encode(['status' => 'success']);
|
echo json_encode(['success' => true, 'status' => 'success']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Action inconnue
|
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Action non reconnue : ' . $action]);
|
echo json_encode(['success' => false, 'status' => 'error', 'message' => 'Action non reconnue : ' . $action]);
|
||||||
|
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
|
echo json_encode(['success' => false, 'status' => 'error', 'message' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
@@ -1,33 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
// includes/api/save-events.php
|
// modules/family-calendar/includes/api/save-events.php
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
require __DIR__ . '/../../../../includes/db.php';
|
require_once __DIR__ . '/../../../../includes/db.php';
|
||||||
|
|
||||||
$rawInput = file_get_contents('php://input');
|
|
||||||
$eventsToSave = json_decode($rawInput, true);
|
|
||||||
|
|
||||||
// Optionnel : tu peux commenter/supprimer ces logs en production pour économiser du disque
|
|
||||||
file_put_contents(
|
|
||||||
__DIR__ . '/events-debug.log',
|
|
||||||
"[" . date('c') . "] RAW INPUT: " . $rawInput . PHP_EOL,
|
|
||||||
FILE_APPEND
|
|
||||||
);
|
|
||||||
|
|
||||||
if (empty($eventsToSave) || !is_array($eventsToSave)) {
|
|
||||||
http_response_code(400);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Aucune donnée d\'événement reçue.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$inserted = [];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. OPTIMISATION : On récupère toutes les personnes d'un coup (Mapping)
|
$rawInput = file_get_contents('php://input');
|
||||||
$stmtPeople = $pdo->query("SELECT id, name FROM pf_people");
|
$eventsToSave = json_decode($rawInput, true);
|
||||||
$peopleMap = [];
|
|
||||||
while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
if (empty($eventsToSave) || !is_array($eventsToSave)) {
|
||||||
// On crée un tableau associatif : ['Carole' => 1, 'Alex' => 2, etc.]
|
throw new Exception("Aucune donnée d'événement reçue.");
|
||||||
$peopleMap[$row['name']] = $row['id'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
@@ -36,26 +17,32 @@ try {
|
|||||||
VALUES (:event_date, :event_type, :person_id, :duration)";
|
VALUES (:event_date, :event_type, :person_id, :duration)";
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
foreach ($eventsToSave as $event) {
|
$inserted = [];
|
||||||
$person_id = null;
|
|
||||||
|
|
||||||
// 2. On vérifie simplement dans notre tableau (plus de requête SQL ici !)
|
foreach ($eventsToSave as $event) {
|
||||||
if (!empty($event['person']) && isset($peopleMap[$event['person']])) {
|
// 🔥 LE CORRECTIF : On initialise à 0 (car NOT NULL en base)
|
||||||
$person_id = $peopleMap[$event['person']];
|
$person_id = 0;
|
||||||
|
|
||||||
|
if (!empty($event['person_id']) && is_numeric($event['person_id'])) {
|
||||||
|
$person_id = (int)$event['person_id'];
|
||||||
|
} elseif (!empty($event['person']) && is_numeric($event['person'])) {
|
||||||
|
$person_id = (int)$event['person'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$duration = isset($event['duration']) ? (float)$event['duration'] : 1.0;
|
||||||
|
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
':event_date' => $event['date'],
|
':event_date' => $event['date'],
|
||||||
':event_type' => $event['type'],
|
':event_type' => $event['type'],
|
||||||
':person_id' => $person_id,
|
':person_id' => $person_id,
|
||||||
':duration' => $event['duration'] ?? 1.0,
|
':duration' => $duration,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$inserted[] = [
|
$inserted[] = [
|
||||||
'id' => $pdo->lastInsertId(),
|
'id' => $pdo->lastInsertId(),
|
||||||
'date' => $event['date'],
|
'date' => $event['date'],
|
||||||
'type' => $event['type'],
|
'type' => $event['type'],
|
||||||
'duration' => $event['duration'] ?? 1.0,
|
'duration' => $duration,
|
||||||
'person_id' => $person_id,
|
'person_id' => $person_id,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -63,12 +50,20 @@ try {
|
|||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'inserted' => $inserted,
|
'inserted' => $inserted,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$pdo->rollBack();
|
if (isset($pdo) && $pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode(['status' => 'error', 'message' => 'Erreur lors de la sauvegarde : ' . $e->getMessage()]);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Erreur SQL : ' . $e->getMessage()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
@@ -11,17 +11,18 @@ try {
|
|||||||
if ($action === 'get_all') {
|
if ($action === 'get_all') {
|
||||||
// A. Options globales du foyer
|
// A. Options globales du foyer
|
||||||
$stmtFoyer = $pdo->query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1");
|
$stmtFoyer = $pdo->query("SELECT zone_scolaire, care_modes FROM pf_foyer_settings LIMIT 1");
|
||||||
$foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '["Nounou","Centre"]'];
|
// On retire le fallback "Nounou" codé en dur, on part sur vide si rien n'est configuré
|
||||||
|
$foyer = $stmtFoyer->fetch(PDO::FETCH_ASSOC) ?: ['zone_scolaire' => 'C', 'care_modes' => '[]'];
|
||||||
|
|
||||||
// B. Liste des membres de la famille
|
// B. Liste des membres de la famille (Triés par rôle pour grouper Parents, Enfants, Helpers)
|
||||||
$stmtPeople = $pdo->query("SELECT id, name, role FROM pf_people WHERE is_active = 1 ORDER BY role DESC, name ASC");
|
// 🟢 CORRECTION : Ajout de la colonne `care_modes` et `color` pour le Javascript !
|
||||||
|
$stmtPeople = $pdo->query("SELECT id, name, role, care_modes, color FROM pf_people WHERE is_active = 1 ORDER BY role ASC, name ASC");
|
||||||
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
$people = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// C. Matrice des congés par personne
|
// C. Matrice des congés par personne
|
||||||
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date FROM pf_person_leave_meta");
|
$stmtLeaves = $pdo->query("SELECT person_id, leave_type, anniversary_date, method, allowance FROM pf_person_leave_meta");
|
||||||
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
$leavesRaw = $stmtLeaves->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// On organise les congés par ID de personne pour faciliter le traitement côté JS
|
|
||||||
$leavesMap = [];
|
$leavesMap = [];
|
||||||
foreach ($leavesRaw as $leave) {
|
foreach ($leavesRaw as $leave) {
|
||||||
$leavesMap[$leave['person_id']][] = [
|
$leavesMap[$leave['person_id']][] = [
|
||||||
@@ -32,6 +33,21 @@ try {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// D. Paramètres dynamiques du calendrier (pf_settings)
|
||||||
|
$stmtSettings = $pdo->query("SELECT setting_key, setting_value FROM pf_settings WHERE module = 'calendar'");
|
||||||
|
$calendarSettingsRaw = $stmtSettings->fetchAll(PDO::FETCH_KEY_PAIR); // Crée un tableau [key => value]
|
||||||
|
|
||||||
|
// Paramètres par défaut si la table est vide
|
||||||
|
$calendarSettings = [
|
||||||
|
'calendar_default_view' => $calendarSettingsRaw['calendar_default_view'] ?? 'month',
|
||||||
|
'calendar_first_day' => $calendarSettingsRaw['calendar_first_day'] ?? '1',
|
||||||
|
'calendar_working_hours' => $calendarSettingsRaw['calendar_working_hours'] ?? '08:00-19:00'
|
||||||
|
];
|
||||||
|
|
||||||
|
// E. NOUVEAU : Récupération du catalogue des types de congés de la famille
|
||||||
|
$stmtLeaveTypes = $pdo->query("SELECT code, label, default_allowance, reset_month, allow_carry_over FROM pf_leave_types ORDER BY label ASC");
|
||||||
|
$leaveTypes = $stmtLeaveTypes->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'data' => [
|
'data' => [
|
||||||
@@ -40,7 +56,9 @@ try {
|
|||||||
'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true)
|
'care_modes' => json_decode($foyer['care_modes'] ?? '[]', true)
|
||||||
],
|
],
|
||||||
'people' => $people,
|
'people' => $people,
|
||||||
'leaves' => $leavesMap
|
'leaves' => $leavesMap,
|
||||||
|
'calendar_settings' => $calendarSettings,
|
||||||
|
'leave_types' => $leaveTypes // On envoie le catalogue au JS !
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
exit;
|
exit;
|
||||||
@@ -48,7 +66,7 @@ try {
|
|||||||
|
|
||||||
// ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ───
|
// ─── 2. SAUVEGARDE DU FOYER (ONGLET 1) ───
|
||||||
if ($action === 'save_foyer') {
|
if ($action === 'save_foyer') {
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("Méthode non autorisée");
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||||
|
|
||||||
$zone = trim($_POST['zone_scolaire'] ?? 'C');
|
$zone = trim($_POST['zone_scolaire'] ?? 'C');
|
||||||
$modesRaw = $_POST['care_modes'] ?? '[]';
|
$modesRaw = $_POST['care_modes'] ?? '[]';
|
||||||
@@ -64,7 +82,7 @@ try {
|
|||||||
|
|
||||||
// ─── 3. SAUVEGARDE DES CONGÉS D'UN MEMBRE (ONGLET 2) ───
|
// ─── 3. SAUVEGARDE DES CONGÉS D'UN MEMBRE (ONGLET 2) ───
|
||||||
if ($action === 'save_member_leaves') {
|
if ($action === 'save_member_leaves') {
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Error("Méthode non autorisée");
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||||
|
|
||||||
$personId = (int)($_POST['person_id'] ?? 0);
|
$personId = (int)($_POST['person_id'] ?? 0);
|
||||||
$leavesData = json_decode($_POST['leaves'] ?? '[]', true);
|
$leavesData = json_decode($_POST['leaves'] ?? '[]', true);
|
||||||
@@ -73,19 +91,22 @@ try {
|
|||||||
|
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
// On nettoie les anciennes configurations de congés de cette personne
|
|
||||||
$stmtDelete = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE person_id = ?");
|
$stmtDelete = $pdo->prepare("DELETE FROM pf_person_leave_meta WHERE person_id = ?");
|
||||||
$stmtDelete->execute([$personId]);
|
$stmtDelete->execute([$personId]);
|
||||||
|
|
||||||
// On réinsère la nouvelle matrice propre
|
|
||||||
if (!empty($leavesData)) {
|
if (!empty($leavesData)) {
|
||||||
$stmtInsert = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, method, allowance, anniversary_date) VALUES (?, ?, ?, ?, ?)");
|
$stmtInsert = $pdo->prepare("INSERT INTO pf_person_leave_meta (person_id, leave_type, method, allowance, anniversary_date) VALUES (?, ?, ?, ?, ?)");
|
||||||
foreach ($leavesData as $leave) {
|
foreach ($leavesData as $leave) {
|
||||||
$type = strtoupper(trim($leave['type']));
|
$type = strtoupper(trim($leave['type']));
|
||||||
$method = in_array($leave['method'], ['FIXED', 'ACCUMULATED']) ? $leave['method'] : 'FIXED';
|
$method = in_array($leave['method'] ?? '', ['FIXED', 'ACCUMULATED']) ? $leave['method'] : 'FIXED';
|
||||||
$allowance = (float)($leave['allowance'] ?? 0);
|
$allowance = (float)($leave['allowance'] ?? 0);
|
||||||
$date = trim($leave['date']);
|
$date = trim($leave['date']);
|
||||||
|
|
||||||
|
// Si le JS n'envoie que "MM-DD" (Renouvellement perpétuel), on ajoute l'année bissextile 2000 pour satisfaire le format DATE de MySQL
|
||||||
|
if (preg_match('/^\d{2}-\d{2}$/', $date)) {
|
||||||
|
$date = "2000-" . $date;
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($type) && !empty($date)) {
|
if (!empty($type) && !empty($date)) {
|
||||||
$stmtInsert->execute([$personId, $type, $method, $allowance, $date]);
|
$stmtInsert->execute([$personId, $type, $method, $allowance, $date]);
|
||||||
}
|
}
|
||||||
@@ -97,6 +118,52 @@ try {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 4. SAUVEGARDE DES PARAMÈTRES D'AFFICHAGE DU CALENDRIER (ONGLET 3) ───
|
||||||
|
if ($action === 'save_calendar_settings') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||||
|
|
||||||
|
$allowed_keys = ['calendar_default_view', 'calendar_first_day', 'calendar_working_hours'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO pf_settings (setting_key, setting_value, module)
|
||||||
|
VALUES (:key, :val, 'calendar')
|
||||||
|
ON DUPLICATE KEY UPDATE setting_value = :val2
|
||||||
|
");
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
foreach ($allowed_keys as $key) {
|
||||||
|
if (isset($_POST[$key])) {
|
||||||
|
$value = trim($_POST[$key]);
|
||||||
|
$stmt->execute([
|
||||||
|
'key' => $key,
|
||||||
|
'val' => $value,
|
||||||
|
'val2' => $value
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 5. SAUVEGARDE DES MODES DE GARDE D'UN ENFANT ───
|
||||||
|
if ($action === 'save_child_care_modes') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Méthode non autorisée");
|
||||||
|
|
||||||
|
$person_id = (int)($_POST['person_id'] ?? 0);
|
||||||
|
$care_modes = $_POST['care_modes'] ?? '[]';
|
||||||
|
|
||||||
|
if ($person_id > 0) {
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_people SET care_modes = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$care_modes, $person_id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'ID de l\'enfant manquant.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
throw new Exception("Action inconnue");
|
throw new Exception("Action inconnue");
|
||||||
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
|
|||||||
+112
-307
@@ -40,57 +40,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
} else {
|
} else {
|
||||||
$action = $_POST['action'] ?? '';
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
// 🟢 SAUVEGARDE GLOBALE : CONFIGURATION DU FOYER (Unifiée)
|
|
||||||
if ($action === 'update_family_info') {
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
try {
|
|
||||||
require_once __DIR__ . '/includes/db.php';
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
// 1. Sauvegarde des paramètres globaux
|
|
||||||
$currency = trim($_POST['currency'] ?? '€');
|
|
||||||
$zone = trim($_POST['zone_scolaire'] ?? 'C');
|
|
||||||
$careModesJson = $_POST['custom_care_modes'] ?? '[]';
|
|
||||||
|
|
||||||
$stmtSave = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?, zone_scolaire = ?, care_modes = ? WHERE id = 1");
|
|
||||||
$stmtSave->execute([$currency, $zone, $careModesJson]);
|
|
||||||
|
|
||||||
// 2. Gestion des enfants
|
|
||||||
$kids = $_POST['kids'] ?? [];
|
|
||||||
$deleted = json_decode($_POST['deleted_kids'] ?? '[]', true);
|
|
||||||
|
|
||||||
if (!empty($deleted)) {
|
|
||||||
$in = str_repeat('?,', count($deleted) - 1) . '?';
|
|
||||||
$stmtDel = $pdo->prepare("DELETE FROM pf_people WHERE id IN ($in) AND role = 'enfant'");
|
|
||||||
$stmtDel->execute($deleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmtInsert = $pdo->prepare("INSERT INTO pf_people (name, role, care_modes) VALUES (?, 'enfant', ?)");
|
|
||||||
$stmtUpdate = $pdo->prepare("UPDATE pf_people SET name = ?, care_modes = ? WHERE id = ? AND role = 'enfant'");
|
|
||||||
|
|
||||||
foreach ($kids as $kidId => $kidData) {
|
|
||||||
$name = trim($kidData['name'] ?? '');
|
|
||||||
if (empty($name)) continue;
|
|
||||||
|
|
||||||
$modes = $kidData['modes'] ?? [];
|
|
||||||
$modesJson = json_encode(array_values($modes));
|
|
||||||
|
|
||||||
if (strpos($kidId, 'new_') === 0) {
|
|
||||||
$stmtInsert->execute([$name, $modesJson]);
|
|
||||||
} else {
|
|
||||||
$stmtUpdate->execute([$name, $modesJson, (int)$kidId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'set_modules' && $family_id) {
|
if ($action === 'set_modules' && $family_id) {
|
||||||
$all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka'];
|
$all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka'];
|
||||||
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
|
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
|
||||||
@@ -100,7 +49,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?")
|
$meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?")
|
||||||
->execute([json_encode($enabled), $family_id]);
|
->execute([json_encode($enabled), $family_id]);
|
||||||
$_SESSION['enabled_modules'] = $enabled;
|
$_SESSION['enabled_modules'] = $enabled;
|
||||||
$success = "Modules mis à jour.";
|
$success = "Modules mis à jour avec succès.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +59,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$_SESSION['app_lang'] = $lang;
|
$_SESSION['app_lang'] = $lang;
|
||||||
$meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?")
|
$meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?")
|
||||||
->execute([$lang, $user_id]);
|
->execute([$lang, $user_id]);
|
||||||
$success = "Language updated.";
|
$success = "Langue mise à jour.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +80,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$_SESSION['user']['display_name'] = $display_name;
|
$_SESSION['user']['display_name'] = $display_name;
|
||||||
$success = "Profil mis à jour.";
|
$success = "Profil mis à jour avec succès.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,9 +158,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$pdo->prepare(
|
$pdo->prepare(
|
||||||
"INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('grocery_settings','history_max',?) ON DUPLICATE KEY UPDATE content=VALUES(content)"
|
"INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('grocery_settings','history_max',?) ON DUPLICATE KEY UPDATE content=VALUES(content)"
|
||||||
)->execute([(string) $n]);
|
)->execute([(string) $n]);
|
||||||
$success = tr('groceries_settings_saved');
|
$success = tr('groceries_settings_saved') ?: 'Paramètres des courses enregistrés.';
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$error = tr('groceries_settings_error') . ' ' . $e->getMessage();
|
$error = (tr('groceries_settings_error') ?: 'Erreur') . ' : ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,6 +223,36 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]);
|
$meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]);
|
||||||
$success = "Connexion iOS supprimée.";
|
$success = "Connexion iOS supprimée.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- GESTION DES PROFILS RATTACHÉS (Enfants, Nounous, etc.) ---
|
||||||
|
if ($action === 'add_attached_profile') {
|
||||||
|
$name = trim($_POST['profile_name'] ?? '');
|
||||||
|
$role = trim($_POST['profile_role'] ?? 'child');
|
||||||
|
|
||||||
|
if ($name) {
|
||||||
|
require_once __DIR__ . '/includes/db.php';
|
||||||
|
if (isset($pdo)) {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_people (name, role) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$name, $role]);
|
||||||
|
$success = "Profil de {$name} ajouté avec succès.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$error = "Le prénom est obligatoire.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete_attached_profile') {
|
||||||
|
$profile_id = (int)($_POST['profile_id'] ?? 0);
|
||||||
|
if ($profile_id > 0) {
|
||||||
|
require_once __DIR__ . '/includes/db.php';
|
||||||
|
if (isset($pdo)) {
|
||||||
|
// On protège les vrais utilisateurs en s'assurant que user_id est NULL
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_people WHERE id = ? AND user_id IS NULL");
|
||||||
|
$stmt->execute([$profile_id]);
|
||||||
|
$success = "Profil supprimé.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,29 +276,10 @@ if ($family_id && isset($pdo)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🟢 Récupération Unifiée : Foyer, Modes de Garde, Enfants
|
|
||||||
$kidsList = [];
|
|
||||||
$familyCareModes = ['Nounou', 'Centre', 'Avis'];
|
|
||||||
$currentCurrency = defined('CURRENCY') ? CURRENCY : '€';
|
|
||||||
$currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C';
|
|
||||||
|
|
||||||
if ($family_id && isset($pdo)) {
|
|
||||||
$foyerSettings = $pdo->query("SELECT currency, zone_scolaire, care_modes FROM pf_foyer_settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if ($foyerSettings) {
|
|
||||||
if (!empty($foyerSettings['currency'])) $currentCurrency = $foyerSettings['currency'];
|
|
||||||
if (!empty($foyerSettings['zone_scolaire'])) $currentZone = $foyerSettings['zone_scolaire'];
|
|
||||||
if (!empty($foyerSettings['care_modes'])) {
|
|
||||||
$parsed = json_decode($foyerSettings['care_modes'], true);
|
|
||||||
if (is_array($parsed)) $familyCareModes = $parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmtKids = $pdo->query("SELECT id, name, care_modes FROM pf_people WHERE role = 'enfant' ORDER BY id ASC");
|
|
||||||
$kidsList = $stmtKids->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
$family = null;
|
$family = null;
|
||||||
$members = [];
|
$members = [];
|
||||||
|
$attached_profiles = [];
|
||||||
|
|
||||||
if ($family_id) {
|
if ($family_id) {
|
||||||
$fam = $meta_pdo->prepare("SELECT * FROM families WHERE id = ?");
|
$fam = $meta_pdo->prepare("SELECT * FROM families WHERE id = ?");
|
||||||
$fam->execute([$family_id]);
|
$fam->execute([$family_id]);
|
||||||
@@ -328,6 +288,12 @@ if ($family_id) {
|
|||||||
$mem = $meta_pdo->prepare("SELECT display_name, username, created_at, is_admin FROM users WHERE family_id = ? ORDER BY id");
|
$mem = $meta_pdo->prepare("SELECT display_name, username, created_at, is_admin FROM users WHERE family_id = ? ORDER BY id");
|
||||||
$mem->execute([$family_id]);
|
$mem->execute([$family_id]);
|
||||||
$members = $mem->fetchAll();
|
$members = $mem->fetchAll();
|
||||||
|
|
||||||
|
// Récupération des profils rattachés (ceux qui n'ont pas de compte de connexion, donc user_id IS NULL)
|
||||||
|
if (isset($pdo)) {
|
||||||
|
$stmtAttached = $pdo->query("SELECT id, name, role FROM pf_people WHERE user_id IS NULL ORDER BY role ASC, name ASC");
|
||||||
|
$attached_profiles = $stmtAttached->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$calendarIntegration = $meta_pdo->prepare("SELECT username, calendar_url, status, last_sync_at FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'");
|
$calendarIntegration = $meta_pdo->prepare("SELECT username, calendar_url, status, last_sync_at FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'");
|
||||||
@@ -353,13 +319,6 @@ require __DIR__ . '/header.php';
|
|||||||
|
|
||||||
<h1 class="pf-settings-title">⚙️ Paramètres</h1>
|
<h1 class="pf-settings-title">⚙️ Paramètres</h1>
|
||||||
|
|
||||||
<?php if ($success): ?>
|
|
||||||
<div class="pf-alert pf-alert--success">✓ <?= htmlspecialchars($success) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($error): ?>
|
|
||||||
<div class="pf-alert pf-alert--error">✗ <?= htmlspecialchars($error) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<section class="pf-panel-card">
|
<section class="pf-panel-card">
|
||||||
<h2 class="pf-card-h2">🌐 Langue / Language</h2>
|
<h2 class="pf-card-h2">🌐 Langue / Language</h2>
|
||||||
<form method="post" class="pf-lang-form">
|
<form method="post" class="pf-lang-form">
|
||||||
@@ -517,96 +476,6 @@ require __DIR__ . '/header.php';
|
|||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$showCurrency = count(array_intersect(['budget', 'holidays', 'gifts', 'garage'], $_SESSION['enabled_modules'] ?? [])) > 0;
|
|
||||||
$showZone = in_array('calendar', $_SESSION['enabled_modules'] ?? []);
|
|
||||||
|
|
||||||
if ($family_id):
|
|
||||||
$currentCurrency = defined('CURRENCY') ? CURRENCY : '€';
|
|
||||||
$currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C';
|
|
||||||
?>
|
|
||||||
<section class="pf-panel-card">
|
|
||||||
<h2 class="pf-card-h2" style="font-size: 1.25rem;">👪 <?= tr('set_family_config') ?></h2>
|
|
||||||
<p class="pf-muted-note">Configurez les indicateurs et les membres partagés par votre foyer.</p>
|
|
||||||
|
|
||||||
<form id="unifiedFamilyForm" style="margin-top: 20px; display: flex; flex-direction: column;">
|
|
||||||
<input type="hidden" name="action" value="update_family_info">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
|
||||||
<input type="hidden" name="custom_care_modes" id="customCareModes" value="<?= htmlspecialchars(json_encode($familyCareModes)) ?>">
|
|
||||||
<input type="hidden" name="deleted_kids" id="deletedKids" value="[]">
|
|
||||||
|
|
||||||
<!-- ACCORDÉON 1 : PARAMÈTRES GLOBAUX -->
|
|
||||||
<?php if ($showCurrency || $showZone): ?>
|
|
||||||
<details class="pf-accordion">
|
|
||||||
<summary class="pf-accordion-summary">🌍 <?= tr('set_global_params') ?></summary>
|
|
||||||
<div class="pf-accordion-content">
|
|
||||||
<div class="form-row" style="margin-bottom: 0;">
|
|
||||||
<?php if ($showCurrency): ?>
|
|
||||||
<div class="pf-form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="pf-label">Devise monétaire</label>
|
|
||||||
<input type="text" name="currency" class="pf-input" value="<?= htmlspecialchars($currentCurrency) ?>" placeholder="ex: €, $, CHF" required maxlength="10">
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($showZone): ?>
|
|
||||||
<div class="pf-form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="pf-label">Zone de vacances scolaires (France)</label>
|
|
||||||
<select name="zone_scolaire" class="pf-input" style="background:var(--bg-panel);">
|
|
||||||
<option value="A" <?= $currentZone === 'A' ? 'selected' : '' ?>>Zone A</option>
|
|
||||||
<option value="B" <?= $currentZone === 'B' ? 'selected' : '' ?>>Zone B</option>
|
|
||||||
<option value="C" <?= $currentZone === 'C' ? 'selected' : '' ?>>Zone C</option>
|
|
||||||
<option value="Autre" <?= $currentZone === 'Autre' ? 'selected' : '' ?>>Hors France / Autre</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- ACCORDÉON 2 : MODES DE GARDE -->
|
|
||||||
<details class="pf-accordion">
|
|
||||||
<summary class="pf-accordion-summary">🏷️ <?= tr('set_care_modes') ?></summary>
|
|
||||||
<div class="pf-accordion-content">
|
|
||||||
<div id="careModesContainer" style="display: flex; gap: 8px; flex-wrap: wrap;"></div>
|
|
||||||
<!-- Largeur contrainte avec max-content -->
|
|
||||||
<button type="button" class="pf-btn btn-secondary pf-btn-sm-text" onclick="promptNewCareMode()" style="padding: 4px 10px; font-size: 0.8rem; width: max-content;">+ <?= tr('set_add_care_mode') ?></button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<!-- ACCORDÉON 3 : ENFANTS (Ouvert par défaut) -->
|
|
||||||
<details class="pf-accordion">
|
|
||||||
<summary class="pf-accordion-summary">👶 <?= tr('set_kids_title') ?></summary>
|
|
||||||
<div class="pf-accordion-content" style="gap: 10px;">
|
|
||||||
<ul id="kidsListContainer" style="list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px;">
|
|
||||||
<?php foreach ($kidsList as $kid):
|
|
||||||
$kidModes = json_decode($kid['care_modes'] ?? '[]', true);
|
|
||||||
if (!is_array($kidModes)) $kidModes = [];
|
|
||||||
?>
|
|
||||||
<!-- Version compactée -->
|
|
||||||
<li class="kid-row" data-id="<?= $kid['id'] ?>" style="padding: 10px 12px; background: var(--bg-page); border: 1px solid var(--border-light); border-radius: 8px; display: flex; flex-direction: column; gap: 8px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; gap: 10px;">
|
|
||||||
<input type="text" name="kids[<?= $kid['id'] ?>][name]" class="pf-input" value="<?= htmlspecialchars($kid['name']) ?>" style="flex: 1; padding: 8px; font-weight: bold;" required>
|
|
||||||
<button type="button" class="btn-icon-action delete" onclick="removeKidRow(this, <?= $kid['id'] ?>)" title="<?= tr('delete') ?>" style="padding: 4px;">🗑️</button>
|
|
||||||
</div>
|
|
||||||
<div class="kid-modes-container" style="font-size: 0.85rem; color: var(--text-muted); display: flex; gap: 12px; flex-wrap: wrap; align-items: center;">
|
|
||||||
<span class="stored-modes" style="display:none;"><?= htmlspecialchars(json_encode($kidModes)) ?></span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<!-- Largeur contrainte avec max-content -->
|
|
||||||
<button type="button" class="pf-btn btn-secondary pf-btn-sm-text" onclick="addKidRow()" style="padding: 4px 10px; font-size: 0.8rem; width: max-content;">+ <?= tr('set_btn_add_kid') ?></button>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
|
|
||||||
<section class="pf-panel-card">
|
<section class="pf-panel-card">
|
||||||
<h2 class="pf-card-h2">👤 Mon profil</h2>
|
<h2 class="pf-card-h2">👤 Mon profil</h2>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@@ -707,6 +576,57 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3 class="pf-members-heading" style="margin-top: 2rem;">🧸 Profils rattachés</h3>
|
||||||
|
<p class="pf-muted-note pf-muted-note--tight" style="margin-bottom: 1rem;">Membres gérés par le foyer (Enfants, Nounous, Proches) n'ayant pas d'accès direct.</p>
|
||||||
|
|
||||||
|
<div class="pf-stack-sm" style="margin-bottom: 1.5rem;">
|
||||||
|
<?php foreach ($attached_profiles as $p):
|
||||||
|
$roleBadge = 'Inconnu';
|
||||||
|
if (in_array($p['role'], ['child', 'enfant'])) $roleBadge = '👶 Enfant';
|
||||||
|
elseif ($p['role'] === 'helper') $roleBadge = '💼 Intervenant';
|
||||||
|
elseif ($p['role'] === 'relative') $roleBadge = '👵 Proche';
|
||||||
|
?>
|
||||||
|
<div class="pf-member-card" style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<strong><?= htmlspecialchars($p['name']) ?></strong>
|
||||||
|
<span class="pf-muted-inline"><?= $roleBadge ?></span>
|
||||||
|
</div>
|
||||||
|
<form method="post" style="margin:0;">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||||
|
<input type="hidden" name="action" value="delete_attached_profile">
|
||||||
|
<input type="hidden" name="profile_id" value="<?= $p['id'] ?>">
|
||||||
|
<button type="submit" class="btn-icon-action delete" title="Supprimer" style="background: none; border: none; cursor: pointer; font-size: 1.2rem; padding: 4px;" onclick="return confirm('Supprimer définitivement ce profil ?')">🗑️</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php if (empty($attached_profiles)): ?>
|
||||||
|
<div class="pf-muted-note" style="padding: 10px; text-align: center; border: 1px dashed var(--border-light); border-radius: 8px;">Aucun profil rattaché.</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" style="display: flex; gap: 8px; background: var(--bg-soft); padding: 12px; border-radius: 8px; border: 1px dashed var(--border-light); align-items: flex-end;">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||||
|
<input type="hidden" name="action" value="add_attached_profile">
|
||||||
|
|
||||||
|
<div class="pf-form-group" style="margin-bottom: 0; flex: 2;">
|
||||||
|
<label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;">Prénom</label>
|
||||||
|
<input type="text" name="profile_name" class="pf-input" placeholder="Ex: Pol, Carole..." required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pf-form-group" style="margin-bottom: 0; flex: 1.5;">
|
||||||
|
<label class="pf-label" style="font-size: 0.8rem; margin-bottom: 4px;">Rôle</label>
|
||||||
|
<select name="profile_role" class="pf-input" required>
|
||||||
|
<option value="child">👶 Enfant</option>
|
||||||
|
<option value="helper">💼 Intervenant</option>
|
||||||
|
<option value="relative">👵 Proche</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="pf-btn pf-shrink-0" style="padding: 8px 16px;">Ajouter</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
@@ -719,6 +639,7 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// --- INJECTION CSRF GLOBALE ---
|
||||||
document.querySelectorAll('form[method="post"]').forEach((form) => {
|
document.querySelectorAll('form[method="post"]').forEach((form) => {
|
||||||
if (!form.querySelector('input[name="csrf_token"]')) {
|
if (!form.querySelector('input[name="csrf_token"]')) {
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
@@ -729,6 +650,7 @@ document.querySelectorAll('form[method="post"]').forEach((form) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- COPIE DU CODE D'INVITATION ---
|
||||||
function copyCode() {
|
function copyCode() {
|
||||||
const code = document.getElementById('invite-code').textContent.trim();
|
const code = document.getElementById('invite-code').textContent.trim();
|
||||||
navigator.clipboard.writeText(code).then(() => {
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
@@ -738,140 +660,23 @@ function copyCode() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🟢 LOGIQUE FRONT-END UNIFIÉE (Vanilla JS) : Configuration Foyer
|
// --- GESTION DES NOTIFICATIONS TOAST (Remplaçant l'ancien bandeau vert) ---
|
||||||
let globalCareModes = <?= json_encode($familyCareModes) ?>;
|
|
||||||
let deletedKidsIds = [];
|
|
||||||
let newKidCounter = 0;
|
|
||||||
|
|
||||||
function renderCareModes() {
|
|
||||||
const container = document.getElementById('careModesContainer');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
document.getElementById('customCareModes').value = JSON.stringify(globalCareModes);
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
globalCareModes.forEach((mode, index) => {
|
|
||||||
const badge = document.createElement('div');
|
|
||||||
badge.style.cssText = "background: var(--bg-soft); color: var(--primary); padding: 4px 10px; border-radius: 12px; font-size: 0.85rem; font-weight: 600; display: flex; align-items: center; gap: 6px; border: 1px solid rgba(59, 130, 246, 0.2);";
|
|
||||||
badge.innerHTML = `
|
|
||||||
${mode}
|
|
||||||
<span style="cursor: pointer; opacity: 0.6; padding: 2px;" onclick="removeCareMode(${index})">×</span>
|
|
||||||
`;
|
|
||||||
container.appendChild(badge);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.kid-row').forEach(row => renderKidCheckboxes(row));
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptNewCareMode() {
|
|
||||||
const newMode = prompt("Entrez le nom du nouveau mode de garde (ex: Papi/Mamie, Centre aéré...)");
|
|
||||||
if (newMode && newMode.trim() !== '') {
|
|
||||||
if (!globalCareModes.includes(newMode.trim())) {
|
|
||||||
globalCareModes.push(newMode.trim());
|
|
||||||
renderCareModes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeCareMode(index) {
|
|
||||||
if (confirm("Supprimer ce mode de garde pour tous les enfants ?")) {
|
|
||||||
globalCareModes.splice(index, 1);
|
|
||||||
renderCareModes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderKidCheckboxes(row) {
|
|
||||||
const container = row.querySelector('.kid-modes-container');
|
|
||||||
const storedSpan = row.querySelector('.stored-modes');
|
|
||||||
let activeModes = [];
|
|
||||||
|
|
||||||
if (storedSpan) {
|
|
||||||
try { activeModes = JSON.parse(storedSpan.innerText); } catch(e){}
|
|
||||||
storedSpan.remove();
|
|
||||||
} else {
|
|
||||||
const checked = Array.from(container.querySelectorAll('input:checked')).map(i => i.value);
|
|
||||||
if (checked.length > 0) activeModes = checked;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kidId = row.getAttribute('data-id');
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
if (globalCareModes.length === 0) {
|
|
||||||
container.innerHTML = '<span style="font-style: italic;">Aucun mode de garde configuré.</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
globalCareModes.forEach(mode => {
|
|
||||||
const isChecked = activeModes.includes(mode) ? 'checked' : '';
|
|
||||||
const label = document.createElement('label');
|
|
||||||
label.style.cssText = "cursor: pointer; display: flex; align-items: center; gap: 5px;";
|
|
||||||
label.innerHTML = `<input type="checkbox" name="kids[${kidId}][modes][]" value="${mode}" class="pf-checkbox-lg" style="transform: scale(0.8);" ${isChecked}> ${mode}`;
|
|
||||||
container.appendChild(label);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addKidRow() {
|
|
||||||
newKidCounter++;
|
|
||||||
const kidId = 'new_' + newKidCounter;
|
|
||||||
const ul = document.getElementById('kidsListContainer');
|
|
||||||
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.className = 'kid-row';
|
|
||||||
li.setAttribute('data-id', kidId);
|
|
||||||
li.style.cssText = "padding: 10px 12px; background: var(--bg-page); border: 1px solid var(--border-light); border-radius: 8px; display: flex; flex-direction: column; gap: 8px;";
|
|
||||||
|
|
||||||
li.innerHTML = `
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; gap: 10px;">
|
|
||||||
<input type="text" name="kids[${kidId}][name]" class="pf-input" placeholder="Prénom" style="flex: 1; padding: 8px; font-weight: bold;" required>
|
|
||||||
<button type="button" class="btn-icon-action delete" onclick="removeKidRow(this)" title="Retirer" style="padding: 4px;">🗑️</button>
|
|
||||||
</div>
|
|
||||||
<div class="kid-modes-container" style="font-size: 0.85rem; color: var(--text-muted); display: flex; gap: 12px; flex-wrap: wrap; align-items: center;"></div>
|
|
||||||
`;
|
|
||||||
ul.appendChild(li);
|
|
||||||
renderKidCheckboxes(li);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeKidRow(btn, dbId = null) {
|
|
||||||
const row = btn.closest('.kid-row');
|
|
||||||
if (dbId) {
|
|
||||||
deletedKidsIds.push(dbId);
|
|
||||||
document.getElementById('deletedKids').value = JSON.stringify(deletedKidsIds);
|
|
||||||
}
|
|
||||||
row.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
const unifiedFamilyForm = document.getElementById('unifiedFamilyForm');
|
|
||||||
if (unifiedFamilyForm) {
|
|
||||||
unifiedFamilyForm.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = e.target;
|
|
||||||
const btn = form.querySelector('button[type="submit"]');
|
|
||||||
const oldHtml = btn.innerHTML;
|
|
||||||
|
|
||||||
btn.innerHTML = '⏳ Enregistrement...';
|
|
||||||
btn.disabled = true;
|
|
||||||
|
|
||||||
const fd = new FormData(form);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await pachaFetch('settings.php', { method: 'POST', body: fd });
|
|
||||||
if (res.success) {
|
|
||||||
showToast("Configuration du foyer enregistrée avec succès.", "success");
|
|
||||||
setTimeout(() => window.location.reload(), 1000);
|
|
||||||
} else {
|
|
||||||
showToast(res.error || "Erreur de sauvegarde.", "error");
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
showToast("Erreur technique de sauvegarde.", "error");
|
|
||||||
} finally {
|
|
||||||
btn.innerHTML = oldHtml;
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
if (document.getElementById('careModesContainer')) renderCareModes();
|
<?php if ($success): ?>
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(<?= json_encode($success) ?>, 'success');
|
||||||
|
} else {
|
||||||
|
alert(<?= json_encode($success) ?>);
|
||||||
|
}
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(<?= json_encode($error) ?>, 'error');
|
||||||
|
} else {
|
||||||
|
alert(<?= json_encode($error) ?>);
|
||||||
|
}
|
||||||
|
<?php endif; ?>
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user