This commit is contained in:
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
househub_db_data
|
househub_db_data
|
||||||
.env
|
.env
|
||||||
data/.hh_app_secret
|
data/.hh_app_secret
|
||||||
extract_source.php
|
extract_source.php
|
||||||
|
.htaccess
|
||||||
+12
-10
@@ -165,16 +165,18 @@ CREATE TABLE IF NOT EXISTS pf_import_rules (
|
|||||||
budget_item_id INT(11) NULL DEFAULT NULL
|
budget_item_id INT(11) NULL DEFAULT NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_advances (
|
|
||||||
id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
CREATE TABLE IF NOT EXISTS `pf_advances` (
|
||||||
advance_date date NOT NULL,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
payer varchar(50) NOT NULL,
|
`advance_date` date NOT NULL,
|
||||||
description varchar(255) NOT NULL,
|
`payer` varchar(50) NOT NULL,
|
||||||
amount decimal(10,2) DEFAULT 0.00,
|
`description` varchar(255) NOT NULL,
|
||||||
from_savings tinyint(1) DEFAULT 0,
|
`amount` decimal(10,2) DEFAULT 0.00,
|
||||||
is_resolved tinyint(1) DEFAULT 0,
|
`from_savings` tinyint(1) DEFAULT 0,
|
||||||
created_at datetime DEFAULT current_timestamp()
|
`is_resolved` tinyint(1) DEFAULT 0,
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
`created_at` datetime DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_notes (
|
CREATE TABLE IF NOT EXISTS pf_notes (
|
||||||
|
|||||||
+69
-52
@@ -8,11 +8,33 @@ require_login();
|
|||||||
require __DIR__ . '/includes/db.php';
|
require __DIR__ . '/includes/db.php';
|
||||||
require_once __DIR__ . '/includes/i18n.php';
|
require_once __DIR__ . '/includes/i18n.php';
|
||||||
|
|
||||||
$stmtPeople = $pdo->query("SELECT id, name, user_id, role FROM pf_people ORDER BY id ASC");
|
// 1. Récupération de toutes les personnes avec leurs modes de garde
|
||||||
|
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, care_modes FROM pf_people ORDER BY id ASC");
|
||||||
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
$familyPeople = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$parents = array_values(array_filter($familyPeople, function($p) {
|
|
||||||
return strtolower($p['role'] ?? '') === 'parent';
|
$parents = [];
|
||||||
}));
|
$kids = [];
|
||||||
|
|
||||||
|
// Liste de tous les modes de garde actifs dans le foyer
|
||||||
|
$activeCareModes = [];
|
||||||
|
|
||||||
|
foreach ($familyPeople as $p) {
|
||||||
|
$role = strtolower($p['role'] ?? '');
|
||||||
|
if ($role === 'parent') {
|
||||||
|
$parents[] = $p;
|
||||||
|
} elseif ($role === 'enfant') {
|
||||||
|
// Décodage sécurisé des modes de garde
|
||||||
|
$modes = json_decode($p['care_modes'] ?? '[]', true);
|
||||||
|
if (!is_array($modes)) $modes = [];
|
||||||
|
$p['modes'] = $modes;
|
||||||
|
$kids[] = $p;
|
||||||
|
|
||||||
|
foreach ($modes as $m) {
|
||||||
|
$activeCareModes[$m] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$activeCareModes = array_keys($activeCareModes); // On récupère uniquement les noms uniques
|
||||||
|
|
||||||
$pageTitle = tr('fc_page_title');
|
$pageTitle = tr('fc_page_title');
|
||||||
$activePage = "family-calendar";
|
$activePage = "family-calendar";
|
||||||
@@ -114,22 +136,17 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="pf-section">
|
<section class="pf-section">
|
||||||
<div class="fc-month-calendar-wrapper">
|
<div class="fc-month-calendar-wrapper">
|
||||||
|
|
||||||
<div class="fc-month-header">
|
<div class="fc-month-header">
|
||||||
|
|
||||||
<div class="fc-month-nav-row">
|
<div class="fc-month-nav-row">
|
||||||
<button id="fc-prev-month" class="fc-nav-button">‹</button>
|
<button id="fc-prev-month" class="fc-nav-button">‹</button>
|
||||||
|
|
||||||
<div id="fc-smart-date-selector" style="display:flex; align-items:center; justify-content:center; flex-grow:1; gap:6px;">
|
<div id="fc-smart-date-selector" style="display:flex; align-items:center; justify-content:center; flex-grow:1; gap:6px;">
|
||||||
<select id="fc-select-month" class="fc-smart-select"></select>
|
<select id="fc-select-month" class="fc-smart-select"></select>
|
||||||
<select id="fc-select-year" class="fc-smart-select"></select>
|
<select id="fc-select-year" class="fc-smart-select"></select>
|
||||||
<span id="fc-multi-month-suffix" style="display:none; font-size:1.3rem; font-weight:800; color:#0f172a; margin-left:4px;"></span>
|
<span id="fc-multi-month-suffix" style="display:none; font-size:1.3rem; font-weight:800; color:#0f172a; margin-left:4px;"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="fc-next-month" class="fc-nav-button">›</button>
|
<button id="fc-next-month" class="fc-nav-button">›</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -139,7 +156,6 @@ require __DIR__ . '/header.php';
|
|||||||
<button class="fc-view-button" data-view="2months"><?= tr('fc_view_2m') ?></button>
|
<button class="fc-view-button" data-view="2months"><?= tr('fc_view_2m') ?></button>
|
||||||
<button class="fc-view-button" data-view="3months"><?= tr('fc_view_3m') ?></button>
|
<button class="fc-view-button" data-view="3months"><?= tr('fc_view_3m') ?></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fc-calendar-container">
|
<div class="fc-calendar-container">
|
||||||
@@ -148,7 +164,6 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="fc-month-balances" class="fc-month-balances"></div>
|
<div id="fc-month-balances" class="fc-month-balances"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -173,18 +188,29 @@ require __DIR__ . '/header.php';
|
|||||||
<th rowspan="3" class="col-day"><?= tr('day_wed') ?></th>
|
<th rowspan="3" class="col-day"><?= tr('day_wed') ?></th>
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_thu') ?></th>
|
<th rowspan="3" class="col-day"><?= tr('day_thu') ?></th>
|
||||||
<th rowspan="3" class="col-day"><?= tr('day_fri') ?></th>
|
<th rowspan="3" class="col-day"><?= tr('day_fri') ?></th>
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_off_carole') ?></span></th>
|
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_extra_off') ?></span></th>
|
<?php foreach ($activeCareModes as $mode): ?>
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_centre') ?></span></th>
|
<th rowspan="3" class="col-total rotated-text"><span><?= htmlspecialchars($mode) ?></span></th>
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_avis') ?></span></th>
|
<?php endforeach; ?>
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_pep_sick') ?></span></th>
|
|
||||||
<th rowspan="3" class="col-total rotated-text"><span><?= tr('leg_presence') ?></span></th>
|
<?php foreach ($kids as $kid): ?>
|
||||||
<th colspan="6" class="col-alex header-group"><?= htmlspecialchars(strtoupper($parents[0]['name'] ?? 'PARENT 1')) ?></th>
|
<th rowspan="3" class="col-total rotated-text"><span>Maladie <?= htmlspecialchars($kid['name']) ?></span></th>
|
||||||
<th colspan="6" class="col-laia header-group"><?= htmlspecialchars(strtoupper($parents[1]['name'] ?? 'PARENT 2')) ?></th>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php foreach ($parents as $index => $parent):
|
||||||
|
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
||||||
|
?>
|
||||||
|
<th colspan="6" class="<?= $parentClass ?> header-group"><?= htmlspecialchars(strtoupper($parent['name'])) ?></th>
|
||||||
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" class="col-alex-sub">CP</th><th colspan="2" class="col-alex-sub">JRA</th><th colspan="2" class="col-alex-sub">JA</th>
|
<?php foreach ($parents as $index => $parent):
|
||||||
<th colspan="2" class="col-laia-sub">CP</th><th colspan="2" class="col-laia-sub">JRA</th><th colspan="2" class="col-laia-sub">JA</th>
|
$parentClass = ($index % 2 === 0) ? 'col-alex' : 'col-laia';
|
||||||
|
?>
|
||||||
|
<th colspan="2" class="<?= $parentClass ?>-sub">CP</th>
|
||||||
|
<th colspan="2" class="<?= $parentClass ?>-sub">JRA</th>
|
||||||
|
<th colspan="2" class="<?= $parentClass ?>-sub">JA</th>
|
||||||
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<?php foreach ($parents as $index => $parent):
|
<?php foreach ($parents as $index => $parent):
|
||||||
@@ -230,11 +256,23 @@ require __DIR__ . '/header.php';
|
|||||||
<div class="pf-legend-grid">
|
<div class="pf-legend-grid">
|
||||||
<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>
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-off-carole"></div><span><?= tr('leg_off_carole') ?></span></div>
|
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-extra-off-carole"></div><span><?= tr('leg_extra_off') ?></span></div>
|
<?php foreach ($activeCareModes as $index => $mode):
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-centre"></div><span><?= tr('leg_centre') ?></span></div>
|
// Génération d'une couleur pseudo-aléatoire mais fixe par mode
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-avis"></div><span><?= tr('leg_avis') ?></span></div>
|
$hue = ($index * 137) % 360;
|
||||||
<div class="pf-legend-item"><div class="pf-legend-color fc-legend-pep-sick"></div><span><?= tr('leg_pep_sick') ?></span></div>
|
?>
|
||||||
|
<div class="pf-legend-item">
|
||||||
|
<div class="pf-legend-color" style="background: hsl(<?= $hue ?>, 70%, 50%);"></div>
|
||||||
|
<span><?= htmlspecialchars($mode) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php foreach ($kids as $kid): ?>
|
||||||
|
<div class="pf-legend-item">
|
||||||
|
<div class="pf-legend-color" style="background: var(--pf-danger, #ef4444); opacity: 0.8;"></div>
|
||||||
|
<span>Maladie <?= htmlspecialchars($kid['name']) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,32 +281,11 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.I18N = {
|
// 🟢 OBJET DE CONFIGURATION GLOBAL POUR LE JAVASCRIPT
|
||||||
...(window.I18N || {}),
|
window.FAMILY_CONFIG = {
|
||||||
'fc_menu_carole': "<?= tr('fc_menu_carole') ?>",
|
parents: <?= json_encode($parents) ?>,
|
||||||
'btn_off': "<?= tr('btn_off') ?>",
|
kids: <?= json_encode($kids) ?>,
|
||||||
'btn_extra': "<?= tr('btn_extra') ?>",
|
activeCareModes: <?= json_encode($activeCareModes) ?>
|
||||||
'leg_centre': "<?= tr('leg_centre') ?>",
|
|
||||||
'leg_avis': "<?= tr('leg_avis') ?>",
|
|
||||||
'leg_pep_sick': "<?= tr('leg_pep_sick') ?>",
|
|
||||||
'leg_off_carole': "<?= tr('leg_off_carole') ?>",
|
|
||||||
'leg_extra_off': "<?= tr('leg_extra_off') ?>",
|
|
||||||
'leg_presence': "<?= tr('leg_presence') ?>",
|
|
||||||
'fc_menu_kids_leaves': "<?= tr('fc_menu_kids_leaves') ?>",
|
|
||||||
'fc_clear': "<?= tr('fc_clear') ?>",
|
|
||||||
'fc_unit_days': "<?= tr('fc_unit_days') ?>",
|
|
||||||
'vac_toussaint': "<?= tr('vac_toussaint') ?>",
|
|
||||||
'vac_noel': "<?= tr('vac_noel') ?>",
|
|
||||||
'vac_hiver': "<?= tr('vac_hiver') ?>",
|
|
||||||
'vac_printemps': "<?= tr('vac_printemps') ?>",
|
|
||||||
'vac_ascension': "<?= tr('vac_ascension') ?>",
|
|
||||||
'vac_ete': "<?= tr('vac_ete') ?>",
|
|
||||||
'leg_school_holidays': "<?= tr('leg_school_holidays') ?>",
|
|
||||||
'fc_school_year': "<?= tr('fc_school_year') ?>",
|
|
||||||
'fc_modal_holidays_title': "<?= tr('fc_modal_holidays_title') ?>",
|
|
||||||
'fc_alert_burn_days': "<?= tr('fc_alert_burn_days') ?>",
|
|
||||||
'fc_alert_burn_jra': "<?= tr('fc_alert_burn_jra') ?>",
|
|
||||||
'ANNIV': "<?= tr('ANNIV') ?>"
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="/modules/family-calendar/family-calendar.js"></script>
|
<script src="/modules/family-calendar/family-calendar.js"></script>
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* HouseHub - CSS Auditor 🦙
|
|
||||||
* Génère un rapport complet de tous les styles (fichiers, balises, inline)
|
|
||||||
*/
|
|
||||||
|
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
|
||||||
|
|
||||||
$root = __DIR__;
|
|
||||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root));
|
|
||||||
$report = "=== 🦙 RAPPORT CSS COMPLET HOUSEHUB ===\n";
|
|
||||||
$report .= "Généré le : " . date('Y-m-d H:i:s') . "\n\n";
|
|
||||||
|
|
||||||
foreach ($iterator as $file) {
|
|
||||||
if ($file->isDir()) continue;
|
|
||||||
|
|
||||||
$filePath = $file->getPathname();
|
|
||||||
$relativePath = str_replace($root, '', $filePath);
|
|
||||||
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
|
|
||||||
|
|
||||||
// 1. EXTRACTION DES FICHIERS .css
|
|
||||||
if ($ext === 'css') {
|
|
||||||
$report .= "/* 📄 FICHIER SOURCE : $relativePath */\n";
|
|
||||||
$report .= file_get_contents($filePath) . "\n";
|
|
||||||
$report .= str_repeat("-", 40) . "\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. EXTRACTION DES BALISES <style> ET ATTRIBUTS style="" DANS LES .php
|
|
||||||
if ($ext === 'php') {
|
|
||||||
$content = file_get_contents($filePath);
|
|
||||||
$foundInFile = false;
|
|
||||||
|
|
||||||
// Extraction des blocs <style>
|
|
||||||
if (preg_match_all('/<style[^>]*>(.*?)<\/style>/is', $content, $matches)) {
|
|
||||||
if (!$foundInFile) { $report .= "/* 📄 FICHIER SOURCE : $relativePath */\n"; $foundInFile = true; }
|
|
||||||
foreach ($matches[1] as $idx => $styleBlock) {
|
|
||||||
$report .= "/* Bloc <style> #$idx */\n";
|
|
||||||
$report .= trim($styleBlock) . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des styles inline style="..."
|
|
||||||
// On capture le tag pour donner du contexte au style inline
|
|
||||||
if (preg_match_all('/<([a-z0-9]+)[^>]*?\sstyle=["\']([^"]*?)["\'][^>]*>/i', $content, $matches)) {
|
|
||||||
if (!$foundInFile) { $report .= "/* 📄 FICHIER SOURCE : $relativePath */\n"; $foundInFile = true; }
|
|
||||||
foreach ($matches[0] as $idx => $fullTag) {
|
|
||||||
// On nettoie un peu pour ne garder que l'essentiel du tag et son style
|
|
||||||
$report .= "/* Style Inline #$idx */\n";
|
|
||||||
$report .= trim($fullTag) . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($foundInFile) {
|
|
||||||
$report .= str_repeat("-", 40) . "\n\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo $report;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<?php
|
|
||||||
// generate_prompt.php - Script pour extraire le code de HouseHub
|
|
||||||
// À lancer via le navigateur (http://localhost:8082/generate_prompt.php) ou en ligne de commande.
|
|
||||||
|
|
||||||
$rootDir = __DIR__;
|
|
||||||
$outputFile = $rootDir . '/househub_source.md';
|
|
||||||
|
|
||||||
// Dossiers à ignorer (pour ne pas polluer le cerveau de l'IA avec des choses inutiles)
|
|
||||||
$ignoredDirs = ['.git', '.devtools', '.gitea', 'assets'];
|
|
||||||
|
|
||||||
// Extensions autorisées (on ne veut que du code, pas d'images)
|
|
||||||
$allowedExtensions = ['php', 'css', 'js', 'html', 'sh'];
|
|
||||||
|
|
||||||
$output = "# 🦙 Source Code HouseHub\n\n";
|
|
||||||
$output .= "> *Généré le " . date('Y-m-d H:i:s') . "*\n\n";
|
|
||||||
|
|
||||||
$iterator = new RecursiveIteratorIterator(
|
|
||||||
new RecursiveDirectoryIterator($rootDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
||||||
RecursiveIteratorIterator::SELF_FIRST
|
|
||||||
);
|
|
||||||
|
|
||||||
$fileCount = 0;
|
|
||||||
|
|
||||||
foreach ($iterator as $file) {
|
|
||||||
if ($file->isFile()) {
|
|
||||||
// Chemin relatif propre
|
|
||||||
$relativePath = str_replace($rootDir . DIRECTORY_SEPARATOR, '', $file->getPathname());
|
|
||||||
$relativePath = str_replace('\\', '/', $relativePath); // Uniformiser pour Windows
|
|
||||||
|
|
||||||
// Vérification des dossiers ignorés
|
|
||||||
$skip = false;
|
|
||||||
foreach ($ignoredDirs as $ignored) {
|
|
||||||
if (strpos($relativePath, $ignored) === 0) {
|
|
||||||
$skip = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($skip) continue;
|
|
||||||
|
|
||||||
// Vérification de l'extension
|
|
||||||
$ext = strtolower($file->getExtension());
|
|
||||||
if (!in_array($ext, $allowedExtensions)) continue;
|
|
||||||
|
|
||||||
// On ignore ce script lui-même
|
|
||||||
if ($file->getFilename() === 'generate_prompt.php') continue;
|
|
||||||
|
|
||||||
// Lecture du contenu
|
|
||||||
$content = file_get_contents($file->getPathname());
|
|
||||||
|
|
||||||
// Détermination du langage pour le bloc Markdown
|
|
||||||
$lang = $ext === 'php' ? 'php' : ($ext === 'js' ? 'javascript' : $ext);
|
|
||||||
|
|
||||||
// Formatage pour l'IA
|
|
||||||
$output .= "### 📄 Fichier : `" . $relativePath . "`\n";
|
|
||||||
$output .= "```" . $lang . "\n";
|
|
||||||
$output .= $content . "\n";
|
|
||||||
$output .= "```\n\n";
|
|
||||||
$output .= "---\n\n";
|
|
||||||
|
|
||||||
$fileCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file_put_contents($outputFile, $output);
|
|
||||||
|
|
||||||
echo "<div style='font-family:sans-serif; padding:20px; background:#f0fdf4; color:#16a34a; border-radius:8px; border:1px solid #bbf7d0;'>";
|
|
||||||
echo "<h2>✅ Succès !</h2>";
|
|
||||||
echo "<p>Le fichier <strong>househub_source.md</strong> a été généré à la racine de ton projet.</p>";
|
|
||||||
echo "<p><strong>$fileCount fichiers</strong> de code ont été fusionnés.</p>";
|
|
||||||
echo "<p><em>Tu peux maintenant donner ce fichier à ta Gem HouseHub !</em></p>";
|
|
||||||
echo "</div>";
|
|
||||||
?>
|
|
||||||
+53
@@ -1250,3 +1250,56 @@ p {
|
|||||||
[data-theme="dark"] .ts-thumb {
|
[data-theme="dark"] .ts-thumb {
|
||||||
transform: translateX(16px);
|
transform: translateX(16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === ACCORDÉONS NATIFS (DETAILS/SUMMARY) === */
|
||||||
|
.pf-accordion {
|
||||||
|
background: var(--bg-page);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.pf-accordion-summary {
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none; /* Masque la flèche native Firefox/Chrome */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: var(--bg-page);
|
||||||
|
transition:
|
||||||
|
background 0.2s,
|
||||||
|
color 0.2s;
|
||||||
|
}
|
||||||
|
.pf-accordion-summary::-webkit-details-marker {
|
||||||
|
display: none; /* Masque la flèche native Safari */
|
||||||
|
}
|
||||||
|
.pf-accordion-summary:hover {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
.pf-accordion-summary::after {
|
||||||
|
content: "▼";
|
||||||
|
font-size: 0.7rem;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
details[open].pf-accordion > .pf-accordion-summary {
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
details[open].pf-accordion > .pf-accordion-summary::after {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
.pf-accordion-content {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -156,6 +156,20 @@ return [
|
|||||||
'mod_planka_desc' => 'Gestioneu els vostres projectes en mode Kanban amb Planka.',
|
'mod_planka_desc' => 'Gestioneu els vostres projectes en mode Kanban amb Planka.',
|
||||||
'menu_planka' => 'Planka',
|
'menu_planka' => 'Planka',
|
||||||
|
|
||||||
|
// --- SETTINGS : ENFANTS ---
|
||||||
|
'set_kids_title' => 'Nens de la llar',
|
||||||
|
'set_kids_desc' => 'Gestioneu la llista de nens per associar-los a l\'estalvi, als regals, etc.',
|
||||||
|
'set_add_kid_placeholder' => 'Nom del nen',
|
||||||
|
'set_btn_add_kid' => 'Afegir',
|
||||||
|
'set_err_kid_name' => 'El nom és obligatori.',
|
||||||
|
'set_confirm_del_kid' => 'Vols eliminar aquest nen? (El seu historial financer es conservarà als arxius).',
|
||||||
|
'set_no_kids' => 'Cap nen configurat per a aquesta llar.',
|
||||||
|
'set_family_config' => 'Configuració de la llar',
|
||||||
|
'set_global_params' => 'Paràmetres globals',
|
||||||
|
'set_care_modes' => 'Modalitats de cura habituals',
|
||||||
|
'set_add_care_mode' => 'Afegir una modalitat',
|
||||||
|
'btn_save' => 'Desar',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// FAMILY CALENDAR
|
// FAMILY CALENDAR
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -208,6 +222,9 @@ return [
|
|||||||
'leg_centre' => 'Casal',
|
'leg_centre' => 'Casal',
|
||||||
'leg_avis' => 'Avís',
|
'leg_avis' => 'Avís',
|
||||||
'leg_pep_sick' => 'Pep Malalt',
|
'leg_pep_sick' => 'Pep Malalt',
|
||||||
|
'care_nounou' => 'Kangur',
|
||||||
|
'care_centre' => 'Casal',
|
||||||
|
'care_avis' => 'Avis',
|
||||||
|
|
||||||
'vac_toussaint' => 'Vacances de Tots Sants',
|
'vac_toussaint' => 'Vacances de Tots Sants',
|
||||||
'vac_noel' => 'Vacances de Nadal',
|
'vac_noel' => 'Vacances de Nadal',
|
||||||
@@ -385,6 +402,7 @@ return [
|
|||||||
'cat_fixed' => 'Despeses Fixes',
|
'cat_fixed' => 'Despeses Fixes',
|
||||||
'cat_others' => 'Altres / Imprevistos',
|
'cat_others' => 'Altres / Imprevistos',
|
||||||
'cat_savings' => 'Estalvis',
|
'cat_savings' => 'Estalvis',
|
||||||
|
'budget_tab_kids' => 'Nens',
|
||||||
|
|
||||||
// --- BUDGET : SUIVI ---
|
// --- BUDGET : SUIVI ---
|
||||||
'bud_rem_school' => 'Escola (Resta estimada)',
|
'bud_rem_school' => 'Escola (Resta estimada)',
|
||||||
@@ -565,6 +583,24 @@ return [
|
|||||||
'bud_prev_confirm_copy' => "Vols copiar les dades de %s a %t?\n\n⚠️ Això sobreescriurà tots els valors ja presents per a %t.",
|
'bud_prev_confirm_copy' => "Vols copiar les dades de %s a %t?\n\n⚠️ Això sobreescriurà tots els valors ja presents per a %t.",
|
||||||
'bud_prev_confirm_transfers' => "Confirmes que %p ha fet totes les transferències per a %m?\n\nAixò actualitzarà l'Estalvi automàticament.",
|
'bud_prev_confirm_transfers' => "Confirmes que %p ha fet totes les transferències per a %m?\n\nAixò actualitzarà l'Estalvi automàticament.",
|
||||||
|
|
||||||
|
// --- MODULE BUDGET : AVANCES & TRICOUNT ---
|
||||||
|
'bud_adv_title' => 'Avançaments i Tricount Familiar',
|
||||||
|
'bud_adv_add' => 'Afegir un avançament',
|
||||||
|
'bud_adv_edit' => 'Modificar l\'avançament',
|
||||||
|
'bud_adv_who_paid' => 'Qui ha pagat?',
|
||||||
|
'bud_adv_payer' => 'Pagador',
|
||||||
|
'bud_adv_has_advanced' => '%s ha avançat:',
|
||||||
|
'bud_adv_cc_label' => 'al compte comú',
|
||||||
|
'bud_adv_livret_label' => 'des del seu llibre d\'estalvis',
|
||||||
|
'bud_adv_cc_balance_title' => 'Equilibri del Compte Comú',
|
||||||
|
'bud_adv_owed_to' => 'S\'han de pagar **%s €** a **%s**',
|
||||||
|
'bud_adv_balanced' => 'Els comptes estan perfectament equilibrats!',
|
||||||
|
'bud_adv_saved_badge' => 'Estalvis',
|
||||||
|
'bud_adv_already_saved' => 'Aquests diners provenen d\'un llibre d\'estalvis',
|
||||||
|
'bud_adv_ph_desc' => 'Ex: Compres, Factura d\'electricitat...',
|
||||||
|
'bud_adv_confirm_resolve' => 'Confirmar el reemborsament complet d\'aquest avançament?',
|
||||||
|
'bud_adv_confirm_delete' => 'Eliminar definitivament aquest avançament?',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CADEAUX (GIFTS)
|
// CADEAUX (GIFTS)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -157,6 +157,20 @@ return [
|
|||||||
'mod_planka_desc' => 'Manage your projects in Kanban mode with Planka.',
|
'mod_planka_desc' => 'Manage your projects in Kanban mode with Planka.',
|
||||||
'menu_planka' => 'Planka',
|
'menu_planka' => 'Planka',
|
||||||
|
|
||||||
|
// --- SETTINGS : KIDS ---
|
||||||
|
'set_kids_title' => 'Household Kids',
|
||||||
|
'set_kids_desc' => 'Manage the list of kids to associate them with savings, gifts, etc.',
|
||||||
|
'set_add_kid_placeholder' => 'Kid\'s first name',
|
||||||
|
'set_btn_add_kid' => 'Add',
|
||||||
|
'set_err_kid_name' => 'The first name is required.',
|
||||||
|
'set_confirm_del_kid' => 'Delete this kid? (Their financial history will remain visible in the archives).',
|
||||||
|
'set_no_kids' => 'No kids configured for this household.',
|
||||||
|
'set_family_config' => 'Family Structure',
|
||||||
|
'set_global_params' => 'Global settings',
|
||||||
|
'set_care_modes' => 'Common childcare arrangements',
|
||||||
|
'set_add_care_mode' => 'Ajouter un modeAdd a mode',
|
||||||
|
'btn_save' => 'Save',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// FAMILY CALENDAR
|
// FAMILY CALENDAR
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -209,6 +223,9 @@ return [
|
|||||||
'leg_centre' => 'Centre',
|
'leg_centre' => 'Centre',
|
||||||
'leg_avis' => 'Notice',
|
'leg_avis' => 'Notice',
|
||||||
'leg_pep_sick' => 'Pep sick',
|
'leg_pep_sick' => 'Pep sick',
|
||||||
|
'care_nounou' => 'Nanny',
|
||||||
|
'care_centre' => 'Daycare',
|
||||||
|
'care_avis' => 'Grandparents',
|
||||||
|
|
||||||
'vac_toussaint' => 'All Saints\' holidays',
|
'vac_toussaint' => 'All Saints\' holidays',
|
||||||
'vac_noel' => 'Christmas holidays',
|
'vac_noel' => 'Christmas holidays',
|
||||||
@@ -383,6 +400,7 @@ return [
|
|||||||
'cat_fixed' => 'Fixed costs',
|
'cat_fixed' => 'Fixed costs',
|
||||||
'cat_others' => 'Other / Unexpected',
|
'cat_others' => 'Other / Unexpected',
|
||||||
'cat_savings' => 'Savings',
|
'cat_savings' => 'Savings',
|
||||||
|
'budget_tab_kids' => 'Kids',
|
||||||
|
|
||||||
// --- BUDGET: TRACKING ---
|
// --- BUDGET: TRACKING ---
|
||||||
'bud_rem_school' => 'School (estimated remaining)',
|
'bud_rem_school' => 'School (estimated remaining)',
|
||||||
@@ -548,6 +566,30 @@ return [
|
|||||||
'bud_prev_confirm_copy' => "Do you want to copy data from %s to %t?\n\n⚠️ This will overwrite all values already present for %t.",
|
'bud_prev_confirm_copy' => "Do you want to copy data from %s to %t?\n\n⚠️ This will overwrite all values already present for %t.",
|
||||||
'bud_prev_confirm_transfers' => "Confirm that %p has made all their transfers for %m?\n\nThis will automatically update Savings.",
|
'bud_prev_confirm_transfers' => "Confirm that %p has made all their transfers for %m?\n\nThis will automatically update Savings.",
|
||||||
'bud_prev_label_name' => 'Name',
|
'bud_prev_label_name' => 'Name',
|
||||||
|
'bud_adv_title' => 'Advances & Family Tricount',
|
||||||
|
'bud_adv_add' => 'Add an Advance',
|
||||||
|
'bud_adv_edit' => 'Edit Advance',
|
||||||
|
'bud_adv_who_paid' => 'Who paid?',
|
||||||
|
'bud_adv_payer' => 'Payer',
|
||||||
|
'bud_adv_has_advanced' => '%s advanced:',
|
||||||
|
'bud_adv_cc_label' => 'on the joint account',
|
||||||
|
'bud_adv_livret_label' => 'from their savings account',
|
||||||
|
'bud_adv_cc_balance_title' => 'Joint Account Balance',
|
||||||
|
'bud_adv_owed_to' => '<strong>%s €</strong> are owed to <strong>%s</strong>',
|
||||||
|
'bud_adv_balanced' => 'Accounts are perfectly balanced!',
|
||||||
|
'bud_adv_saved_badge' => 'Savings',
|
||||||
|
'bud_adv_already_saved' => 'This money comes from a savings account',
|
||||||
|
'bud_adv_ph_desc' => 'e.g., Groceries, Electricity bill...',
|
||||||
|
'bud_adv_confirm_resolve' => 'Confirm the complete reimbursement of this advance?',
|
||||||
|
'bud_adv_confirm_delete' => 'Permanently delete this advance?',
|
||||||
|
'date' => 'Date',
|
||||||
|
'bud_label_name' => 'Description',
|
||||||
|
'bud_amount' => 'Amount',
|
||||||
|
'actions' => 'Actions',
|
||||||
|
'edit' => 'Edit',
|
||||||
|
'delete' => 'Delete',
|
||||||
|
'btn_cancel' => 'Cancel',
|
||||||
|
'btn_save' => 'Save',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// GIFTS
|
// GIFTS
|
||||||
|
|||||||
@@ -156,6 +156,20 @@ return [
|
|||||||
'mod_planka_desc' => 'Gérez vos projets en mode Kanban avec Planka.',
|
'mod_planka_desc' => 'Gérez vos projets en mode Kanban avec Planka.',
|
||||||
'menu_planka' => 'Planka',
|
'menu_planka' => 'Planka',
|
||||||
|
|
||||||
|
// --- SETTINGS : ENFANTS ---
|
||||||
|
'set_kids_title' => 'Enfants du foyer',
|
||||||
|
'set_kids_desc' => 'Gérez la liste des enfants pour les associer à l\'épargne, aux cadeaux, etc.',
|
||||||
|
'set_add_kid_placeholder' => 'Prénom de l\'enfant',
|
||||||
|
'set_btn_add_kid' => 'Ajouter',
|
||||||
|
'set_err_kid_name' => 'Le prénom est requis.',
|
||||||
|
'set_confirm_del_kid' => 'Supprimer cet enfant ? (Son historique financier restera visible dans les archives).',
|
||||||
|
'set_no_kids' => 'Aucun enfant configuré pour ce foyer.',
|
||||||
|
'set_family_config' => 'Configuration du foyer',
|
||||||
|
'set_global_params' => 'Paramètres globaux',
|
||||||
|
'set_care_modes' => 'Modes de garde usuels',
|
||||||
|
'set_add_care_mode' => 'Ajouter un mode',
|
||||||
|
'btn_save_config' => 'Enregistrer',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// FAMILY CALENDAR
|
// FAMILY CALENDAR
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -199,6 +213,9 @@ return [
|
|||||||
'fc_err_action' => 'Erreur lors de l\'action : ',
|
'fc_err_action' => 'Erreur lors de l\'action : ',
|
||||||
'fc_today_short' => 'Auj.',
|
'fc_today_short' => 'Auj.',
|
||||||
'fc_today_title' => "Aujourd'hui",
|
'fc_today_title' => "Aujourd'hui",
|
||||||
|
'care_nounou' => 'Nounou',
|
||||||
|
'care_centre' => 'Centre de loisirs',
|
||||||
|
'care_avis' => 'Avis',
|
||||||
|
|
||||||
'leg_presence' => 'Présence Pep',
|
'leg_presence' => 'Présence Pep',
|
||||||
'leg_school_holidays' => 'Vacances Scolaires',
|
'leg_school_holidays' => 'Vacances Scolaires',
|
||||||
@@ -385,6 +402,7 @@ return [
|
|||||||
'cat_fixed' => 'Charges Fixes',
|
'cat_fixed' => 'Charges Fixes',
|
||||||
'cat_others' => 'Autres / Imprévus',
|
'cat_others' => 'Autres / Imprévus',
|
||||||
'cat_savings' => 'Épargne',
|
'cat_savings' => 'Épargne',
|
||||||
|
'budget_tab_kids' => 'Enfants',
|
||||||
|
|
||||||
// --- BUDGET : SUIVI ---
|
// --- BUDGET : SUIVI ---
|
||||||
'bud_rem_school' => 'École (Reste estimé)',
|
'bud_rem_school' => 'École (Reste estimé)',
|
||||||
@@ -563,6 +581,22 @@ return [
|
|||||||
'bud_sav_modal_title_add' => 'Saisir un mois',
|
'bud_sav_modal_title_add' => 'Saisir un mois',
|
||||||
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
|
'bud_sav_ph_name' => 'Nom (ex: Vacances)',
|
||||||
'bud_prev_label_name' => 'Nom',
|
'bud_prev_label_name' => 'Nom',
|
||||||
|
'bud_adv_title' => 'Avances & Tricount Familial',
|
||||||
|
'bud_adv_add' => 'Ajouter une avance',
|
||||||
|
'bud_adv_edit' => 'Modifier l\'avance',
|
||||||
|
'bud_adv_who_paid' => 'Qui a payé ?',
|
||||||
|
'bud_adv_payer' => 'Payeur',
|
||||||
|
'bud_adv_has_advanced' => '%s a avancé :',
|
||||||
|
'bud_adv_cc_label' => 'sur le compte commun',
|
||||||
|
'bud_adv_livret_label' => 'depuis son livret d\'épargne',
|
||||||
|
'bud_adv_cc_balance_title' => 'Équilibre du Compte Commun',
|
||||||
|
'bud_adv_owed_to' => '<strong>%s €</strong> sont dus à <strong>%s</strong>',
|
||||||
|
'bud_adv_balanced' => 'Les comptes sont parfaitement équilibrés !',
|
||||||
|
'bud_adv_saved_badge' => 'Épargne',
|
||||||
|
'bud_adv_already_saved' => 'Cet argent provient d\'un livret d\'épargne',
|
||||||
|
'bud_adv_ph_desc' => 'Ex: Courses, Facture Électricité...',
|
||||||
|
'bud_adv_confirm_resolve' => 'Confirmer le remboursement complet de cette avance ?',
|
||||||
|
'bud_adv_confirm_delete' => 'Supprimer définitivement cette avance ?',
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CADEAUX (GIFTS)
|
// CADEAUX (GIFTS)
|
||||||
|
|||||||
+49
-9
@@ -200,19 +200,59 @@ foreach ($families as $f) {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
echo "<li><strong>Table pf_alloc_categories (Récupération des données) :</strong> ";
|
echo "<li><strong>Table pf_alloc_categories (Récupération des données) :</strong> ";
|
||||||
try {
|
try {
|
||||||
$updated1 = $fam_pdo->exec("UPDATE pf_alloc_categories SET transfer_dest = target, target = '0' WHERE target LIKE 'vers %'");
|
// Vérifier si la colonne target est encore au format texte (VARCHAR)
|
||||||
|
$stmtCol = $fam_pdo->query("SHOW COLUMNS FROM pf_alloc_categories LIKE 'target'");
|
||||||
$updated2 = $fam_pdo->exec("UPDATE pf_alloc_categories SET transfer_dest = 'SYSTEM', target = '0' WHERE target = 'SYSTEM'");
|
$colInfo = $stmtCol->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
$fam_pdo->exec("UPDATE pf_alloc_categories SET target = '0' WHERE target NOT REGEXP '^[0-9]+(\.[0-9]+)?$'");
|
|
||||||
|
|
||||||
echo "<span style='color:green'>" . ($updated1 + $updated2) . " destinations récupérées et déplacées.</span></li>";
|
if ($colInfo && strpos(strtolower($colInfo['Type']), 'varchar') !== false) {
|
||||||
|
// 1. Déplacer les textes "vers..."
|
||||||
$fam_pdo->exec("ALTER TABLE pf_alloc_categories MODIFY target DECIMAL(10,2) DEFAULT 0.00");
|
$updated1 = $fam_pdo->exec("UPDATE pf_alloc_categories SET transfer_dest = target, target = '0' WHERE target LIKE 'vers %'");
|
||||||
echo "<li><span style='color:green'>Colonne 'target' re-sécurisée en format monétaire DECIMAL(10,2).</span></li>";
|
|
||||||
|
// 2. Gérer le mot 'SYSTEM'
|
||||||
|
$updated2 = $fam_pdo->exec("UPDATE pf_alloc_categories SET transfer_dest = 'SYSTEM', target = '0' WHERE target = 'SYSTEM'");
|
||||||
|
|
||||||
|
// 3. Sécurité absolue avant conversion
|
||||||
|
$fam_pdo->exec("UPDATE pf_alloc_categories SET target = '0' WHERE target NOT REGEXP '^[0-9]+(\\\\.[0-9]+)?$'");
|
||||||
|
|
||||||
|
echo "<span style='color:green'>" . ($updated1 + $updated2) . " destinations récupérées et déplacées.</span><br>";
|
||||||
|
|
||||||
|
// 4. Remettre la colonne en format numérique
|
||||||
|
$fam_pdo->exec("ALTER TABLE pf_alloc_categories MODIFY target DECIMAL(10,2) DEFAULT 0.00");
|
||||||
|
echo "<span style='color:green'>Colonne 'target' re-sécurisée en format monétaire DECIMAL(10,2).</span></li>";
|
||||||
|
} else {
|
||||||
|
echo "<span style='color:gray'>La colonne 'target' est déjà au format DECIMAL, transfert ignoré.</span></li>";
|
||||||
|
}
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
echo "<span style='color:red'>Erreur : " . $e->getMessage() . "</span></li>";
|
echo "<span style='color:red'>Erreur : " . $e->getMessage() . "</span></li>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Récupération de toutes les bases de données de familles via la base meta
|
||||||
|
$stmtFamilies = $meta_pdo->query("SELECT id, db_name FROM families WHERE is_active = 1");
|
||||||
|
while ($fam = $stmtFamilies->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
error_log("Migration de la base familiale : " . $fam['db_name']);
|
||||||
|
|
||||||
|
$fam_dsn = "mysql:host=" . DB_HOST . ";dbname=" . $fam['db_name'] . ";charset=utf8mb4";
|
||||||
|
$fam_pdo = new PDO($fam_dsn, DB_USER, DB_PASS, $options);
|
||||||
|
|
||||||
|
// Injection incrémentale sécurisée
|
||||||
|
$fam_pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS `pf_advances` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`advance_date` DATE NOT NULL,
|
||||||
|
`payer` VARCHAR(100) NOT NULL,
|
||||||
|
`description` VARCHAR(255) NOT NULL,
|
||||||
|
`amount` DECIMAL(10,2) DEFAULT 0.00,
|
||||||
|
`from_savings` TINYINT(1) DEFAULT 0,
|
||||||
|
`is_resolved` TINYINT(1) DEFAULT 0,
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
");
|
||||||
|
}
|
||||||
|
error_log("✅ Migration de la table pf_advances terminée sur tous les tenants.");
|
||||||
|
} catch (Exception $e) {
|
||||||
|
die("❌ Erreur critique lors de la migration : " . $e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ require __DIR__ . '/../../../../includes/auth.php';
|
|||||||
require __DIR__ . '/../../../../includes/db.php';
|
require __DIR__ . '/../../../../includes/db.php';
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
|
|
||||||
$action = $_POST['action'] ?? '';
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
@@ -42,7 +41,7 @@ if ($action === 'save_note') {
|
|||||||
|
|
||||||
// 1. MISE A JOUR TABLEAU SALAIRES (AJAX)
|
// 1. MISE A JOUR TABLEAU SALAIRES (AJAX)
|
||||||
if ($action === 'update_salary_config') {
|
if ($action === 'update_salary_config') {
|
||||||
header('Content-Type: application/json'); // On précise JSON ici
|
header('Content-Type: application/json');
|
||||||
$year = $_POST['year'];
|
$year = $_POST['year'];
|
||||||
$person = $_POST['person'];
|
$person = $_POST['person'];
|
||||||
$field = $_POST['field']; // salary, mensualite, etc.
|
$field = $_POST['field']; // salary, mensualite, etc.
|
||||||
@@ -50,7 +49,10 @@ if ($action === 'update_salary_config') {
|
|||||||
|
|
||||||
// Liste des champs autorisés pour éviter les injections
|
// Liste des champs autorisés pour éviter les injections
|
||||||
$allowed = ['salary', 'mensualite', 'frais_func', 'eco_perso', 'eco_family'];
|
$allowed = ['salary', 'mensualite', 'frais_func', 'eco_perso', 'eco_family'];
|
||||||
if (!in_array($field, $allowed)) { echo json_encode(['error'=>'Champ invalide']); exit; }
|
if (!in_array($field, $allowed)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Champ invalide']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_salary_config (year, person, $field) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE $field = VALUES($field)");
|
$stmt = $pdo->prepare("INSERT INTO pf_salary_config (year, person, $field) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE $field = VALUES($field)");
|
||||||
$stmt->execute([$year, $person, $value]);
|
$stmt->execute([$year, $person, $value]);
|
||||||
@@ -63,7 +65,7 @@ if ($action === 'update_allocation') {
|
|||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
$date = $_POST['month_date'];
|
$date = $_POST['month_date'];
|
||||||
$catId = (int)$_POST['cat_id'];
|
$catId = (int)$_POST['cat_id'];
|
||||||
$personId = (int)$_POST['person_id']; // 🟢 Reçoit l'ID de la personne directement !
|
$personId = (int)$_POST['person_id'];
|
||||||
$value = floatval($_POST['value']);
|
$value = floatval($_POST['value']);
|
||||||
|
|
||||||
if ($catId <= 0 || $personId <= 0) {
|
if ($catId <= 0 || $personId <= 0) {
|
||||||
@@ -272,4 +274,91 @@ if ($action === 'validate_transfers') {
|
|||||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
exit;
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// 7. GESTION DES AVANCES / TRICOUNT (pf_advances)
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
if ($action === 'save_advance') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
try {
|
||||||
|
$payer = trim($_POST['payer'] ?? '');
|
||||||
|
$advance_date = $_POST['advance_date'] ?? date('Y-m-d');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$amount = abs((float)($_POST['amount'] ?? 0));
|
||||||
|
$from_savings = isset($_POST['from_savings']) ? 1 : 0;
|
||||||
|
|
||||||
|
if (empty($payer) || empty($description) || $amount <= 0) {
|
||||||
|
throw new Exception("Champs obligatoires manquants ou invalides.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_advances (advance_date, payer, description, amount, from_savings) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$advance_date, $payer, $description, $amount, $from_savings]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'update_advance') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
try {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$payer = trim($_POST['payer'] ?? '');
|
||||||
|
$advance_date = $_POST['advance_date'] ?? date('Y-m-d');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$amount = abs((float)($_POST['amount'] ?? 0));
|
||||||
|
$from_savings = isset($_POST['from_savings']) ? 1 : 0;
|
||||||
|
|
||||||
|
if ($id <= 0 || empty($payer) || empty($description) || $amount <= 0) {
|
||||||
|
throw new Exception("Paramètres invalides fournis.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_advances SET advance_date = ?, payer = ?, description = ?, amount = ?, from_savings = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$advance_date, $payer, $description, $amount, $from_savings, $id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'resolve_advance') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
try {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) throw new Exception("ID invalide fourni.");
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE pf_advances SET is_resolved = 1 WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete_advance') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
try {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) throw new Exception("ID invalide.");
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM pf_advances WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
@@ -102,6 +102,41 @@ function getTranslatedMonthName($dateString) {
|
|||||||
$y = date('Y', strtotime($dateString));
|
$y = date('Y', strtotime($dateString));
|
||||||
return tr('month_' . $m) . ' ' . $y;
|
return tr('month_' . $m) . ' ' . $y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 🧠 PREPARATION DATA TRICOUNT / AVANCES ---
|
||||||
|
$stmtAdvancesList = $pdo->query("SELECT * FROM pf_advances WHERE is_resolved = 0 ORDER BY advance_date DESC");
|
||||||
|
$activeAdvances = $stmtAdvancesList->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$advTotal = [$p1_name => 0, $p2_name => 0];
|
||||||
|
$livretTotal = [$p1_name => 0, $p2_name => 0];
|
||||||
|
|
||||||
|
$labelsCC = [$p1_name => [], $p2_name => []];
|
||||||
|
$labelsLivret = [$p1_name => [], $p2_name => []];
|
||||||
|
|
||||||
|
foreach ($activeAdvances as $adv) {
|
||||||
|
$p = $adv['payer'];
|
||||||
|
$amt = (float)$adv['amount'];
|
||||||
|
$labelStr = htmlspecialchars($adv['description']) . ' (' . number_format($amt, 0, ',', ' ') . '€)';
|
||||||
|
|
||||||
|
if (!isset($advTotal[$p])) {
|
||||||
|
$advTotal[$p] = 0; $livretTotal[$p] = 0;
|
||||||
|
$labelsCC[$p] = []; $labelsLivret[$p] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($adv['from_savings']) {
|
||||||
|
$livretTotal[$p] += $amt;
|
||||||
|
$labelsLivret[$p][] = $labelStr;
|
||||||
|
} else {
|
||||||
|
$advTotal[$p] += $amt;
|
||||||
|
$labelsCC[$p][] = $labelStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$balanceDiff = abs(($advTotal[$p1_name] ?? 0) - ($advTotal[$p2_name] ?? 0));
|
||||||
|
$owedTo = '';
|
||||||
|
if (($advTotal[$p1_name] ?? 0) > ($advTotal[$p2_name] ?? 0)) $owedTo = $p1_name;
|
||||||
|
elseif (($advTotal[$p2_name] ?? 0) > ($advTotal[$p1_name] ?? 0)) $owedTo = $p2_name;
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="prev-container" style="--p1-main: <?= $parentMapping[0]['color'] ?>; --p2-main: <?= $parentMapping[1]['color'] ?? '#f59e0b' ?>;">
|
<div class="prev-container" style="--p1-main: <?= $parentMapping[0]['color'] ?>; --p2-main: <?= $parentMapping[1]['color'] ?? '#f59e0b' ?>;">
|
||||||
@@ -288,6 +323,125 @@ function getTranslatedMonthName($dateString) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 30px 0; background: var(--bg-panel); padding: 24px; border-radius: var(--radius); border: 1px solid var(--border-light); box-shadow: var(--shadow);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 12px;">
|
||||||
|
<h3 style="margin: 0; font-size: 1.3rem; font-weight: 800;"><?= tr('bud_adv_title') ?></h3>
|
||||||
|
<button type="button" class="pf-btn" onclick="document.getElementById('addAdvanceModal').classList.add('is-active')">
|
||||||
|
+ <?= tr('bud_adv_add') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 20px; margin-bottom: 24px; flex-wrap: wrap;">
|
||||||
|
<div style="flex: 1; min-width: 240px; background: rgba(8, 145, 178, 0.06); border: 1px solid rgba(8, 145, 178, 0.2); padding: 18px; border-radius: 12px;">
|
||||||
|
<div style="font-size: 0.85rem; color: #0891b2; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($p1_name)) ?></div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<div style="display: flex; align-items: baseline; gap: 6px;">
|
||||||
|
<span style="font-size: 1.5rem; font-weight: 800; color: #164e63; font-family: monospace;"><?= number_format($advTotal[$p1_name] ?? 0, 2, ',', ' ') ?> €</span>
|
||||||
|
</div>
|
||||||
|
<small style="color: var(--text-muted); font-size: 0.75rem; font-weight: 500;"><?= tr('bud_adv_cc_label') ?></small>
|
||||||
|
<?php if (!empty($labelsCC[$p1_name])): ?>
|
||||||
|
<div style="font-size: 0.75rem; color: #0e7490; margin-top: 6px; line-height: 1.4; font-style: italic;">
|
||||||
|
<?= implode(', ', $labelsCC[$p1_name]) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php if(($livretTotal[$p1_name] ?? 0) > 0): ?>
|
||||||
|
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed rgba(8, 145, 178, 0.3);">
|
||||||
|
<div style="font-size: 1.2rem; font-weight: 800; color: #4338ca; font-family: monospace;">+ <?= number_format($livretTotal[$p1_name], 2, ',', ' ') ?> €</div>
|
||||||
|
<small style="color: #4338ca; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
|
||||||
|
<?php if (!empty($labelsLivret[$p1_name])): ?>
|
||||||
|
<div style="font-size: 0.75rem; color: #3730a3; margin-top: 4px; line-height: 1.4;">
|
||||||
|
<?= implode(', ', $labelsLivret[$p1_name]) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex: 1; min-width: 240px; background: rgba(217, 119, 6, 0.06); border: 1px solid rgba(217, 119, 6, 0.2); padding: 18px; border-radius: 12px;">
|
||||||
|
<div style="font-size: 0.85rem; color: #d97706; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;"><?= sprintf(tr('bud_adv_has_advanced'), htmlspecialchars($p2_name)) ?></div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<div style="display: flex; align-items: baseline; gap: 6px;">
|
||||||
|
<span style="font-size: 1.5rem; font-weight: 800; color: #78350f; font-family: monospace;"><?= number_format($advTotal[$p2_name] ?? 0, 2, ',', ' ') ?> €</span>
|
||||||
|
</div>
|
||||||
|
<small style="color: var(--text-muted); font-size: 0.75rem; font-weight: 500;"><?= tr('bud_adv_cc_label') ?></small>
|
||||||
|
<?php if (!empty($labelsCC[$p2_name])): ?>
|
||||||
|
<div style="font-size: 0.75rem; color: #b45309; margin-top: 6px; line-height: 1.4; font-style: italic;">
|
||||||
|
<?= implode(', ', $labelsCC[$p2_name]) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php if(($livretTotal[$p2_name] ?? 0) > 0): ?>
|
||||||
|
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px dashed rgba(217, 119, 6, 0.3);">
|
||||||
|
<div style="font-size: 1.2rem; font-weight: 800; color: #b45309; font-family: monospace;">+ <?= number_format($livretTotal[$p2_name], 2, ',', ' ') ?> €</div>
|
||||||
|
<small style="color: #b45309; font-size: 0.75rem; font-weight: 600;"><?= tr('bud_adv_livret_label') ?></small>
|
||||||
|
<?php if (!empty($labelsLivret[$p2_name])): ?>
|
||||||
|
<div style="font-size: 0.75rem; color: #92400e; margin-top: 4px; line-height: 1.4;">
|
||||||
|
<?= implode(', ', $labelsLivret[$p2_name]) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex: 1; min-width: 240px; background: var(--bg-page); border: 1px solid var(--border-light); padding: 18px; border-radius: 12px; display: flex; flex-direction: column; justify-content: center;">
|
||||||
|
<div style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px;">
|
||||||
|
🏛️ <?= tr('bud_adv_cc_balance_title') ?>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 1.15rem; font-weight: 800;">
|
||||||
|
<?php if ($balanceDiff > 0.01): ?>
|
||||||
|
<?= sprintf(tr('bud_adv_owed_to'), number_format($balanceDiff, 2, ',', ' '), htmlspecialchars($owedTo)) ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color: var(--success); font-weight: bold;">✓ <?= tr('bud_adv_balanced') ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($activeAdvances)): ?>
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table class="pf-table" style="margin: 0; box-shadow: none; border: 1px solid var(--border-light);">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= tr('date') ?></th>
|
||||||
|
<th><?= tr('bud_adv_payer') ?></th>
|
||||||
|
<th><?= tr('bud_label_name') ?></th>
|
||||||
|
<th><?= tr('bud_amount') ?></th>
|
||||||
|
<th style="text-align: right;"><?= tr('actions') ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($activeAdvances as $adv): ?>
|
||||||
|
<tr>
|
||||||
|
<td style="color: var(--text-muted);"><?= date('d/m/Y', strtotime($adv['advance_date'])) ?></td>
|
||||||
|
<td style="font-weight: 700; color: <?= $adv['payer'] === $p1_name ? '#0891b2' : '#d97706' ?>;"><?= htmlspecialchars($adv['payer']) ?></td>
|
||||||
|
<td>
|
||||||
|
<?= htmlspecialchars($adv['description']) ?>
|
||||||
|
<?php if ($adv['from_savings']): ?>
|
||||||
|
<span style="background: var(--bg-soft); color: var(--primary); font-size: 0.7rem; padding: 2px 6px; border-radius: 4px; margin-left: 6px; font-weight: bold; border: 1px solid rgba(59, 130, 246, 0.2);"><?= tr('bud_adv_saved_badge') ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td style="font-weight: 700; font-family: monospace;"><?= number_format($adv['amount'], 2, ',', ' ') ?> €</td>
|
||||||
|
<td style="text-align: right; white-space: nowrap;">
|
||||||
|
<button class="btn-icon-action edit" title="<?= tr('edit') ?>"
|
||||||
|
data-id="<?= $adv['id'] ?>"
|
||||||
|
data-payer="<?= htmlspecialchars($adv['payer']) ?>"
|
||||||
|
data-date="<?= $adv['advance_date'] ?>"
|
||||||
|
data-desc="<?= htmlspecialchars($adv['description']) ?>"
|
||||||
|
data-amount="<?= $adv['amount'] ?>"
|
||||||
|
data-savings="<?= $adv['from_savings'] ?>"
|
||||||
|
onclick="triggerEditAdvanceModal(this)">✏️</button>
|
||||||
|
<button class="btn-icon-action delete" title="<?= tr('delete') ?>" onclick="executeDeleteAdvance(<?= $adv['id'] ?>)">🗑️</button>
|
||||||
|
<button class="pf-btn" style="padding: 4px 10px; font-size: 0.8rem; border-radius: 6px; width:auto; height:auto;" onclick="executeResolveAdvance(<?= $adv['id'] ?>)">✓</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$focusMonth = $months[0];
|
$focusMonth = $months[0];
|
||||||
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
|
$targetsOrder = ['vers commune', 'vers L.Pol', 'vers L.Pep', 'vers L.Perso'];
|
||||||
@@ -365,13 +519,13 @@ function getTranslatedMonthName($dateString) {
|
|||||||
<input type="text" name="name" class="pf-input" required>
|
<input type="text" name="name" class="pf-input" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="pf-label">Objectif Mensuel (€)</label>
|
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?? 'Objectif Mensuel (€)' ?></label>
|
||||||
<input type="number" step="1" name="target" class="pf-input" placeholder="Ex: 150">
|
<input type="number" step="1" name="target" class="pf-input" placeholder="Ex: 150">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="pf-label">Destination Virement (Optionnel)</label>
|
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?? 'Destination Virement (Optionnel)' ?></label>
|
||||||
<select name="transfer_dest" class="pf-input">
|
<select name="transfer_dest" class="pf-input">
|
||||||
<option value="" selected>-- Aucune --</option>
|
<option value="" selected>-- <?= tr('bud_prev_none') ?? 'Aucune' ?> --</option>
|
||||||
<option value="vers L.Pol">vers L.Pol</option>
|
<option value="vers L.Pol">vers L.Pol</option>
|
||||||
<option value="vers L.Pep">vers L.Pep</option>
|
<option value="vers L.Pep">vers L.Pep</option>
|
||||||
<option value="vers L.Perso">vers L.Perso</option>
|
<option value="vers L.Perso">vers L.Perso</option>
|
||||||
@@ -409,13 +563,13 @@ function getTranslatedMonthName($dateString) {
|
|||||||
<input type="text" name="name" id="edit_cat_name" class="pf-input" required>
|
<input type="text" name="name" id="edit_cat_name" class="pf-input" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="pf-label">Objectif Mensuel (€)</label>
|
<label class="pf-label"><?= tr('bud_prev_monthly_target') ?? 'Objectif Mensuel (€)' ?></label>
|
||||||
<input type="number" step="1" name="target" id="edit_cat_target" class="pf-input" placeholder="Ex: 150">
|
<input type="number" step="1" name="target" id="edit_cat_target" class="pf-input" placeholder="Ex: 150">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="pf-label">Destination Virement (Optionnel)</label>
|
<label class="pf-label"><?= tr('bud_prev_transfer_dest') ?? 'Destination Virement (Optionnel)' ?></label>
|
||||||
<select name="transfer_dest" id="edit_cat_transfer_dest" class="pf-input">
|
<select name="transfer_dest" id="edit_cat_transfer_dest" class="pf-input">
|
||||||
<option value="">-- Aucune --</option>
|
<option value="">-- <?= tr('bud_prev_none') ?? 'Aucune' ?> --</option>
|
||||||
<option value="vers L.Pol">vers L.Pol</option>
|
<option value="vers L.Pol">vers L.Pol</option>
|
||||||
<option value="vers L.Pep">vers L.Pep</option>
|
<option value="vers L.Pep">vers L.Pep</option>
|
||||||
<option value="vers L.Perso">vers L.Perso</option>
|
<option value="vers L.Perso">vers L.Perso</option>
|
||||||
@@ -439,6 +593,85 @@ function getTranslatedMonthName($dateString) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="addAdvanceModal" class="pf-modal">
|
||||||
|
<div class="pf-modal-content">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
|
||||||
|
<h3 class="pf-modal-title" style="margin:0; border:none; padding:0;">+ <?= tr('bud_adv_add') ?></h3>
|
||||||
|
<button type="button" onclick="closeSuiviModal('addAdvanceModal')" style="background:none; border:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">×</button>
|
||||||
|
</div>
|
||||||
|
<form action="/modules/budget/includes/api/save-budget.php" method="POST" id="formAddAdvance" onsubmit="handleAdvanceSubmit(event, this)">
|
||||||
|
<input type="hidden" name="action" value="save_advance">
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('bud_adv_who_paid') ?></label>
|
||||||
|
<select name="payer" class="pf-input" required>
|
||||||
|
<option value="<?= htmlspecialchars($p1_name) ?>"><?= htmlspecialchars($p1_name) ?></option>
|
||||||
|
<option value="<?= htmlspecialchars($p2_name) ?>"><?= htmlspecialchars($p2_name) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('date') ?></label>
|
||||||
|
<input type="date" name="advance_date" class="pf-input" value="<?= date('Y-m-d') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('bud_label_name') ?></label>
|
||||||
|
<input type="text" name="description" class="pf-input" placeholder="<?= tr('bud_adv_ph_desc') ?>" required autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('bud_amount') ?></label>
|
||||||
|
<input type="number" step="0.01" min="0.01" name="amount" class="pf-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group" style="display:flex; align-items:center; gap:8px; margin-top:10px;">
|
||||||
|
<input type="checkbox" name="from_savings" id="add_from_savings" value="1" class="pf-checkbox-lg">
|
||||||
|
<label for="add_from_savings" style="margin:0; cursor:pointer; font-weight:600; color:var(--primary);"><?= tr('bud_adv_already_saved') ?></label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" onclick="closeSuiviModal('addAdvanceModal')" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
||||||
|
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="editAdvanceModal" class="pf-modal">
|
||||||
|
<div class="pf-modal-content">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
|
||||||
|
<h3 class="pf-modal-title" style="margin:0; border:none; padding:0;">✏️ <?= tr('bud_adv_edit') ?></h3>
|
||||||
|
<button type="button" onclick="closeSuiviModal('editAdvanceModal')" style="background:none; border:none; font-size:1.5rem; cursor:pointer; color:var(--text-muted);">×</button>
|
||||||
|
</div>
|
||||||
|
<form action="/modules/budget/includes/api/save-budget.php" method="POST" id="formEditAdvance" onsubmit="handleAdvanceSubmit(event, this)">
|
||||||
|
<input type="hidden" name="action" value="update_advance">
|
||||||
|
<input type="hidden" name="id" id="edit_adv_id">
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('bud_adv_who_paid') ?></label>
|
||||||
|
<select name="payer" id="edit_adv_payer" class="pf-input" required>
|
||||||
|
<option value="<?= htmlspecialchars($p1_name) ?>"><?= htmlspecialchars($p1_name) ?></option>
|
||||||
|
<option value="<?= htmlspecialchars($p2_name) ?>"><?= htmlspecialchars($p2_name) ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('date') ?></label>
|
||||||
|
<input type="date" name="advance_date" id="edit_adv_date" class="pf-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('bud_label_name') ?></label>
|
||||||
|
<input type="text" name="description" id="edit_adv_desc" class="pf-input" required autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group">
|
||||||
|
<label class="pf-label"><?= tr('bud_amount') ?></label>
|
||||||
|
<input type="number" step="0.01" min="0.01" name="amount" id="edit_adv_amount" class="pf-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="pf-form-group" style="display:flex; align-items:center; gap:8px; margin-top:10px;">
|
||||||
|
<input type="checkbox" name="from_savings" id="edit_adv_from_savings" value="1" class="pf-checkbox-lg">
|
||||||
|
<label for="edit_adv_from_savings" style="margin:0; cursor:pointer; font-weight:600; color:var(--primary);"><?= tr('bud_adv_already_saved') ?></label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" onclick="closeSuiviModal('editAdvanceModal')" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
||||||
|
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()">
|
<button id="fabSumMode" class="pf-fab-sum" onclick="toggleSumMode()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="8" y1="12" x2="16" y2="12"></line><line x1="12" y1="8" x2="12" y2="16"></line></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="8" y1="12" x2="16" y2="12"></line><line x1="12" y1="8" x2="12" y2="16"></line></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -451,12 +684,17 @@ function getTranslatedMonthName($dateString) {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||||
|
|
||||||
|
// Dictionnaire I18N injecté proprement depuis PHP
|
||||||
window.I18N = {
|
window.I18N = {
|
||||||
...(window.I18N || {}),
|
...(window.I18N || {}),
|
||||||
'bud_prev_label_name': <?= json_encode(tr('bud_prev_label_name')) ?>,
|
'bud_prev_label_name': <?= json_encode(tr('bud_prev_label_name')) ?>,
|
||||||
'bud_prev_err_no_history': <?= json_encode(tr('bud_prev_err_no_history')) ?>,
|
'bud_prev_err_no_history': <?= json_encode(tr('bud_prev_err_no_history')) ?>,
|
||||||
'bud_prev_confirm_copy': <?= json_encode(tr('bud_prev_confirm_copy')) ?>,
|
'bud_prev_confirm_copy': <?= json_encode(tr('bud_prev_confirm_copy')) ?>,
|
||||||
'bud_prev_confirm_transfers': <?= json_encode(tr('bud_prev_confirm_transfers')) ?>,
|
'bud_prev_confirm_transfers': <?= json_encode(tr('bud_prev_confirm_transfers')) ?>,
|
||||||
|
'bud_prev_confirm_del_line': <?= json_encode(tr('bud_prev_confirm_del_line')) ?>,
|
||||||
|
'bud_adv_confirm_resolve': <?= json_encode(tr('bud_adv_confirm_resolve') ?? 'Confirmer le remboursement ?') ?>,
|
||||||
|
'bud_adv_confirm_delete': <?= json_encode(tr('bud_adv_confirm_delete') ?? 'Supprimer définitivement ?') ?>,
|
||||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>
|
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -467,6 +705,8 @@ window.CONFIG.CURRENCY = '<?= defined('CURRENCY') ? CURRENCY : "€" ?>';
|
|||||||
const currentYear = <?= $currentYear ?>;
|
const currentYear = <?= $currentYear ?>;
|
||||||
const months = <?= json_encode($months) ?>;
|
const months = <?= json_encode($months) ?>;
|
||||||
|
|
||||||
|
/* --- LOGIQUE ALLOCATIONS & BUDGET --- */
|
||||||
|
|
||||||
function openEditModal(btn) {
|
function openEditModal(btn) {
|
||||||
document.getElementById('edit_cat_id').value = btn.getAttribute('data-id');
|
document.getElementById('edit_cat_id').value = btn.getAttribute('data-id');
|
||||||
document.getElementById('edit_cat_name').value = btn.getAttribute('data-name');
|
document.getElementById('edit_cat_name').value = btn.getAttribute('data-name');
|
||||||
@@ -486,7 +726,6 @@ function updateSalary(person, input) {
|
|||||||
const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0;
|
const ecoF = parseFloat(row.querySelector('[data-field="eco_family"]').value) || 0;
|
||||||
|
|
||||||
const restant = salary - (mens + frais + ecoP + ecoF);
|
const restant = salary - (mens + frais + ecoP + ecoF);
|
||||||
|
|
||||||
const parentMap = window.CONFIG.parentMapping.find(m => m.name === person);
|
const parentMap = window.CONFIG.parentMapping.find(m => m.name === person);
|
||||||
if(parentMap) {
|
if(parentMap) {
|
||||||
document.getElementById('restant_' + parentMap.css).innerText = Math.round(restant).toLocaleString(window.appLang) + ' €';
|
document.getElementById('restant_' + parentMap.css).innerText = Math.round(restant).toLocaleString(window.appLang) + ' €';
|
||||||
@@ -644,11 +883,14 @@ function updateSummaryTable() {
|
|||||||
function saveData(action, data) {
|
function saveData(action, data) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('action', action);
|
formData.append('action', action);
|
||||||
|
formData.append('ajax', '1'); // <-- Sécurisation pour pachaFetch / backend
|
||||||
for (const key in data) formData.append(key, data[key]);
|
for (const key in data) formData.append(key, data[key]);
|
||||||
fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
|
|
||||||
|
// On utilise fetch ici en mode aveugle pour ne pas bloquer l'UI lors de la frappe rapide
|
||||||
|
fetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateTransfers(personCss, month) {
|
async function validateTransfers(personCss, month) {
|
||||||
const parentMap = window.CONFIG.parentMapping.find(m => m.css === personCss);
|
const parentMap = window.CONFIG.parentMapping.find(m => m.css === personCss);
|
||||||
if (!parentMap) return;
|
if (!parentMap) return;
|
||||||
|
|
||||||
@@ -659,30 +901,30 @@ function validateTransfers(personCss, month) {
|
|||||||
formData.append('action', 'validate_transfers');
|
formData.append('action', 'validate_transfers');
|
||||||
formData.append('person_id', parentMap.id);
|
formData.append('person_id', parentMap.id);
|
||||||
formData.append('month_date', month);
|
formData.append('month_date', month);
|
||||||
|
formData.append('ajax', '1');
|
||||||
|
|
||||||
fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData })
|
try {
|
||||||
.then(r => r.json())
|
const result = await pachaFetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
|
||||||
.then(data => {
|
if(result.success) {
|
||||||
if(data.success) window.location.reload();
|
window.location.reload();
|
||||||
else alert("Erreur: " + data.error);
|
} else {
|
||||||
})
|
alert(window.I18N['bud_err_tech'] + " : " + result.error);
|
||||||
.catch(e => alert(window.I18N['bud_err_tech']));
|
}
|
||||||
|
} catch(e) {
|
||||||
|
alert(window.I18N['bud_err_tech']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveGenericNote(noteType, refId, content) {
|
async function saveGenericNote(noteType, refId, content) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('action', 'save_note');
|
formData.append('action', 'save_note');
|
||||||
formData.append('note_type', noteType);
|
formData.append('note_type', noteType);
|
||||||
formData.append('reference_id', refId);
|
formData.append('reference_id', refId);
|
||||||
formData.append('content', content);
|
formData.append('content', content);
|
||||||
|
formData.append('ajax', '1');
|
||||||
|
|
||||||
fetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData })
|
try {
|
||||||
.then(async r => {
|
const data = await pachaFetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
|
||||||
const text = await r.text();
|
|
||||||
if (!r.ok) throw new Error(`Erreur HTTP ${r.status}`);
|
|
||||||
return JSON.parse(text);
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
if(data.success) {
|
if(data.success) {
|
||||||
const indicator = document.getElementById('note-save-indicator');
|
const indicator = document.getElementById('note-save-indicator');
|
||||||
if(indicator) {
|
if(indicator) {
|
||||||
@@ -690,12 +932,107 @@ function saveGenericNote(noteType, refId, content) {
|
|||||||
setTimeout(() => indicator.style.opacity = '0', 2000);
|
setTimeout(() => indicator.style.opacity = '0', 2000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert("Erreur: " + data.error);
|
alert(window.I18N['bud_err_tech'] + " : " + data.error);
|
||||||
}
|
}
|
||||||
})
|
} catch(e) {
|
||||||
.catch(e => alert(e.message));
|
alert(window.I18N['bud_err_tech']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteCategory(id) {
|
||||||
|
if (!confirm(window.I18N['bud_prev_confirm_del_line'])) return;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'delete_category');
|
||||||
|
formData.append('id', id);
|
||||||
|
formData.append('ajax', '1');
|
||||||
|
try {
|
||||||
|
const result = await pachaFetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
|
||||||
|
if (result.success) window.location.reload();
|
||||||
|
} catch(e) { console.error(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- LOGIQUE TRICOUNT / AVANCES --- */
|
||||||
|
|
||||||
|
function closeSuiviModal(modalId) {
|
||||||
|
document.getElementById(modalId).classList.remove('is-active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSuiviModal(modalId) {
|
||||||
|
document.getElementById(modalId).classList.add('is-active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerEditAdvanceModal(btn) {
|
||||||
|
document.getElementById('edit_adv_id').value = btn.getAttribute('data-id');
|
||||||
|
document.getElementById('edit_adv_payer').value = btn.getAttribute('data-payer');
|
||||||
|
document.getElementById('edit_adv_date').value = btn.getAttribute('data-date');
|
||||||
|
document.getElementById('edit_adv_desc').value = btn.getAttribute('data-desc');
|
||||||
|
document.getElementById('edit_adv_amount').value = btn.getAttribute('data-amount');
|
||||||
|
document.getElementById('edit_adv_from_savings').checked = (btn.getAttribute('data-savings') === '1');
|
||||||
|
openSuiviModal('editAdvanceModal');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAdvanceSubmit(event, form) {
|
||||||
|
event.preventDefault();
|
||||||
|
const btnSubmit = form.querySelector('button[type="submit"]');
|
||||||
|
const oldText = btnSubmit.innerHTML;
|
||||||
|
btnSubmit.disabled = true;
|
||||||
|
btnSubmit.innerHTML = '...';
|
||||||
|
|
||||||
|
const endpoint = form.getAttribute('action');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
formData.append('ajax', '1'); // <-- Correction de l'erreur d'empty string
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Utilisation robuste de pachaFetch
|
||||||
|
const result = await pachaFetch(endpoint, { method: 'POST', body: formData });
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert((window.I18N['bud_err_tech'] || "Erreur") + " : " + (result.error || "Opération échouée"));
|
||||||
|
btnSubmit.disabled = false;
|
||||||
|
btnSubmit.innerHTML = oldText;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("AJAX Error:", err);
|
||||||
|
alert(window.I18N['bud_err_tech'] || "Une erreur technique est survenue.");
|
||||||
|
btnSubmit.disabled = false;
|
||||||
|
btnSubmit.innerHTML = oldText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeResolveAdvance(id) {
|
||||||
|
if (!confirm(window.I18N['bud_adv_confirm_resolve'])) return;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'resolve_advance');
|
||||||
|
fd.append('id', id);
|
||||||
|
fd.append('ajax', '1');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: fd });
|
||||||
|
window.location.reload();
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeDeleteAdvance(id) {
|
||||||
|
if (!confirm(window.I18N['bud_adv_confirm_delete'])) return;
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('action', 'delete_advance');
|
||||||
|
fd.append('id', id);
|
||||||
|
fd.append('ajax', '1');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pachaFetch('/modules/budget/includes/api/save-budget.php', { method: 'POST', body: fd });
|
||||||
|
window.location.reload();
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- LOGIQUE MODE SOMME AUTOMATIQUE --- */
|
||||||
|
|
||||||
let isSumModeActive = false;
|
let isSumModeActive = false;
|
||||||
let selectedElementsForSum = new Set();
|
let selectedElementsForSum = new Set();
|
||||||
|
|
||||||
@@ -730,17 +1067,7 @@ function updateSumResult() {
|
|||||||
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
document.getElementById('sumResultValue').innerText = Math.round(total).toLocaleString(window.appLang) + ' ' + window.CONFIG.CURRENCY;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCategory(id) {
|
/* --- INITIALISATIONS & ECOUTEURS --- */
|
||||||
if (!confirm(window.I18N['bud_prev_confirm_del_line'])) return;
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('action', 'delete_category');
|
|
||||||
formData.append('id', id);
|
|
||||||
formData.append('ajax', '1');
|
|
||||||
try {
|
|
||||||
const result = await pachaFetch('modules/budget/includes/api/save-budget.php', { method: 'POST', body: formData });
|
|
||||||
if (result.success) window.location.reload();
|
|
||||||
} catch(e) { console.error(e); }
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
if (!isSumModeActive) return;
|
if (!isSumModeActive) return;
|
||||||
@@ -760,6 +1087,7 @@ document.addEventListener('click', function(e) {
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', recalcAllAllocations);
|
document.addEventListener('DOMContentLoaded', recalcAllAllocations);
|
||||||
|
|
||||||
|
// Soumission standard des modales de catégories avec pachaFetch
|
||||||
document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => {
|
document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form => {
|
||||||
form.addEventListener('submit', async (e) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -769,9 +1097,11 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
|
|||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.innerText = '⏳ ...';
|
submitBtn.innerText = '⏳ ...';
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
formData.append('ajax', '1');
|
formData.append('ajax', '1'); // Toujours sécuriser
|
||||||
const actionUrl = form.getAttribute('action');
|
const actionUrl = form.getAttribute('action');
|
||||||
|
|
||||||
const result = await pachaFetch(actionUrl, { method: 'POST', body: formData });
|
const result = await pachaFetch(actionUrl, { method: 'POST', body: formData });
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
form.closest('.pf-modal').style.display = 'none';
|
form.closest('.pf-modal').style.display = 'none';
|
||||||
document.body.classList.remove('no-scroll');
|
document.body.classList.remove('no-scroll');
|
||||||
@@ -780,7 +1110,7 @@ document.querySelectorAll('#addCatModal form, #editCatModal form').forEach(form
|
|||||||
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
alert((window.I18N['bud_err_tech'] || 'Erreur') + " : " + (result.error || "Inconnue"));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("Une erreur technique est survenue.");
|
alert(window.I18N['bud_err_tech'] || "Une erreur technique est survenue.");
|
||||||
} finally {
|
} finally {
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
submitBtn.innerText = originalText;
|
submitBtn.innerText = originalText;
|
||||||
|
|||||||
@@ -1,9 +1,32 @@
|
|||||||
<?php
|
<?php
|
||||||
// modules/budget/views/epargne.php
|
// modules/budget/views/epargne.php
|
||||||
|
|
||||||
$requestedOwner = $_GET['owner'] ?? 'Nens';
|
// --- 🧠 CONFIGURATION AGNOSTIQUE DES MEMBRES (MULTI-TENANT) ---
|
||||||
$ownersToDisplay = ($requestedOwner === 'Nens') ? ['Pol', 'Pep'] : [$requestedOwner];
|
$stmtPeople = $pdo->query("SELECT name, role FROM pf_people ORDER BY id ASC");
|
||||||
|
$familyParents = [];
|
||||||
|
$familyKids = [];
|
||||||
|
|
||||||
|
while ($row = $stmtPeople->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$role = strtolower(trim($row['role'] ?? ''));
|
||||||
|
|
||||||
|
if ($role === 'parent') {
|
||||||
|
$familyParents[] = $row['name'];
|
||||||
|
} elseif ($role === 'nounou') {
|
||||||
|
// On ignore la nounou dans l'épargne
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// Fallback : Si c'est 'enfant', 'user', ou vide, ça va dans l'onglet Enfants
|
||||||
|
$familyKids[] = $row['name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sécurité anti-page blanche si la BDD est mal configurée
|
||||||
|
if (empty($familyParents)) $familyParents = ['Parent 1', 'Parent 2'];
|
||||||
|
|
||||||
|
$requestedOwner = $_GET['owner'] ?? ($familyParents[0] ?? 'Nens');
|
||||||
|
$ownersToDisplay = ($requestedOwner === 'Nens') ? $familyKids : [$requestedOwner];
|
||||||
|
|
||||||
|
// --- RÉCUPÉRATION CONFIGURATION DES MOIS ---
|
||||||
$cycleConfigs = [];
|
$cycleConfigs = [];
|
||||||
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
|
$stmtNotes = $pdo->query("SELECT reference_id, content FROM pf_notes WHERE note_type = 'month_config'");
|
||||||
while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
|
while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
|
||||||
@@ -14,7 +37,6 @@ while ($row = $stmtNotes->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupération sécurisée du nom des mois (utilise les clés globales existantes)
|
|
||||||
function getMonthName($dateString) {
|
function getMonthName($dateString) {
|
||||||
$m = date('m', strtotime($dateString));
|
$m = date('m', strtotime($dateString));
|
||||||
$y = date('Y', strtotime($dateString));
|
$y = date('Y', strtotime($dateString));
|
||||||
@@ -25,9 +47,19 @@ 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">
|
||||||
<a href="?tab=epargne&owner=Alex" class="owner-tab <?= $requestedOwner === 'Alex' ? 'active' : '' ?>">Alex</a>
|
<!-- Boucle dynamique sur les parents -->
|
||||||
<a href="?tab=epargne&owner=Laia" class="owner-tab <?= $requestedOwner === 'Laia' ? 'active' : '' ?>">Laia</a>
|
<?php foreach ($familyParents as $p): ?>
|
||||||
<a href="?tab=epargne&owner=Nens" class="owner-tab <?= $requestedOwner === 'Nens' ? 'active' : '' ?>">Nens 👶</a>
|
<a href="?tab=epargne&owner=<?= urlencode($p) ?>" class="owner-tab <?= $requestedOwner === $p ? 'active' : '' ?>">
|
||||||
|
<?= htmlspecialchars($p) ?>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<!-- Onglet Enfants -->
|
||||||
|
<?php if (!empty($familyKids)): ?>
|
||||||
|
<a href="?tab=epargne&owner=Nens" class="owner-tab <?= $requestedOwner === 'Nens' ? 'active' : '' ?>">
|
||||||
|
<?= tr('budget_tab_kids') ?? 'Nens 👶' ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -52,10 +84,7 @@ function getMonthName($dateString) {
|
|||||||
sort($allCategories);
|
sort($allCategories);
|
||||||
|
|
||||||
// Définition de la classe couleur selon le propriétaire
|
// Définition de la classe couleur selon le propriétaire
|
||||||
$ownerTextClass = '';
|
$ownerTextClass = 'txt-global';
|
||||||
if ($currentOwner === 'Alex') $ownerTextClass = 'txt-alex';
|
|
||||||
elseif ($currentOwner === 'Laia') $ownerTextClass = 'txt-laia';
|
|
||||||
else $ownerTextClass = 'txt-global'; // Pour Pol et Pep
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; margin-top: <?= ($requestedOwner === 'Nens' && $currentOwner !== 'Pol') ? '40px' : '0' ?>;">
|
||||||
|
|||||||
@@ -81,18 +81,20 @@ $totalRevenus = 0;
|
|||||||
$realSum = $realTotals[$item['id']];
|
$realSum = $realTotals[$item['id']];
|
||||||
$hasMatchingExpense = true;
|
$hasMatchingExpense = true;
|
||||||
}
|
}
|
||||||
// B. Correspondance par catégorie système (École, Essence, FMCG)
|
// B. Correspondance par catégorie système (Multi-tenant via Mots-clés)
|
||||||
else {
|
else {
|
||||||
$catKey = null;
|
$catKey = null;
|
||||||
if (trim($item['name']) === 'Estimacio escola') $catKey = 'School';
|
if (!empty($item['mapping_keywords'])) {
|
||||||
elseif (trim($item['name']) === 'Estimation gasolina') $catKey = 'Essence';
|
if (stripos($item['mapping_keywords'], 'School') !== false) $catKey = 'School';
|
||||||
elseif (trim($item['name']) === 'Estimacio F&B & beauty') $catKey = 'FMCG';
|
elseif (stripos($item['mapping_keywords'], 'Essence') !== false) $catKey = 'Essence';
|
||||||
|
elseif (stripos($item['mapping_keywords'], 'FMCG') !== false) $catKey = 'FMCG';
|
||||||
|
}
|
||||||
|
|
||||||
if ($catKey && isset($catTotals[$catKey])) {
|
if ($catKey && isset($catTotals[$catKey])) {
|
||||||
$realSum = $catTotals[$catKey];
|
$realSum = $catTotals[$catKey];
|
||||||
$hasMatchingExpense = true;
|
$hasMatchingExpense = true;
|
||||||
}
|
}
|
||||||
// C. Correspondance par mots-clés (seulement sur les dépenses non liées)
|
// C. Correspondance par mots-clés classiques (sur les dépenses non liées)
|
||||||
elseif (!empty($item['mapping_keywords'])) {
|
elseif (!empty($item['mapping_keywords'])) {
|
||||||
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
$keywords = array_map('trim', explode(',', $item['mapping_keywords']));
|
||||||
foreach ($unlinkedExpenses as $uexp) {
|
foreach ($unlinkedExpenses as $uexp) {
|
||||||
|
|||||||
@@ -221,10 +221,15 @@ while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
$pending_charges[] = ['name' => $name, 'amount' => $absAmount];
|
$pending_charges[] = ['name' => $name, 'amount' => $absAmount];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($name === 'Estimacio F&B & beauty') $budget_fmcg = $amt;
|
if (!empty($item['mapping_keywords'])) {
|
||||||
elseif ($name === 'Estimacio escola') $budget_school = $amt;
|
if (stripos($item['mapping_keywords'], 'FMCG') !== false) $budget_fmcg += $amt;
|
||||||
elseif ($name === 'Estimation gasolina') $budget_essence = $amt;
|
if (stripos($item['mapping_keywords'], 'School') !== false) $budget_school += $amt;
|
||||||
elseif ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') { $budget_frais += $absAmount; }
|
if (stripos($item['mapping_keywords'], 'Essence') !== false) $budget_essence += $amt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)$item['is_estimate'] === 0 && $item['type'] === 'Mensuel' && $item['category'] === 'expense') {
|
||||||
|
$budget_frais += $absAmount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,8 +585,13 @@ $monthName = $monthNames[(int)$viewM] . ' ' . $viewY;
|
|||||||
<div class="form-group" id="blockInputSelect" style="margin-bottom:15px; display:none;">
|
<div class="form-group" id="blockInputSelect" style="margin-bottom:15px; display:none;">
|
||||||
<label class="pf-label"><?= tr('bud_beneficiary') ?></label>
|
<label class="pf-label"><?= tr('bud_beneficiary') ?></label>
|
||||||
<select name="label_select" id="schoolSelect" class="pf-input">
|
<select name="label_select" id="schoolSelect" class="pf-input">
|
||||||
<option value="Ecole Pol">Ecole Pol</option>
|
<?php
|
||||||
<option value="Carole">Carole</option>
|
$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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+11
-10
@@ -165,16 +165,17 @@ CREATE TABLE IF NOT EXISTS pf_import_rules (
|
|||||||
budget_item_id INT(11) NULL DEFAULT NULL
|
budget_item_id INT(11) NULL DEFAULT NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_advances (
|
CREATE TABLE IF NOT EXISTS `pf_advances` (
|
||||||
id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
advance_date date NOT NULL,
|
`advance_date` date NOT NULL,
|
||||||
payer varchar(50) NOT NULL,
|
`payer` varchar(50) NOT NULL,
|
||||||
description varchar(255) NOT NULL,
|
`description` varchar(255) NOT NULL,
|
||||||
amount decimal(10,2) DEFAULT 0.00,
|
`amount` decimal(10,2) DEFAULT 0.00,
|
||||||
from_savings tinyint(1) DEFAULT 0,
|
`from_savings` tinyint(1) DEFAULT 0,
|
||||||
is_resolved tinyint(1) DEFAULT 0,
|
`is_resolved` tinyint(1) DEFAULT 0,
|
||||||
created_at datetime DEFAULT current_timestamp()
|
`created_at` datetime DEFAULT current_timestamp(),
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
-- ─── Notes / Memo ─────────────────────────────────────────────────────────────
|
||||||
CREATE TABLE IF NOT EXISTS pf_notes (
|
CREATE TABLE IF NOT EXISTS pf_notes (
|
||||||
|
|||||||
+467
-249
@@ -31,224 +31,263 @@ CREATE TABLE IF NOT EXISTS user_calendar_integrations (
|
|||||||
// ─── Actions ──────────────────────────────────────────────────────────────────
|
// ─── Actions ──────────────────────────────────────────────────────────────────
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
if (!verify_csrf($_POST['csrf_token'] ?? null)) {
|
if (!verify_csrf($_POST['csrf_token'] ?? null)) {
|
||||||
|
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Session invalide (CSRF). Rechargez la page.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
$error = "Session invalide (CSRF). Rechargez la page.";
|
$error = "Session invalide (CSRF). Rechargez la page.";
|
||||||
} else {
|
} else {
|
||||||
$action = $_POST['action'] ?? '';
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
if ($action === 'update_family_info') {
|
// 🟢 SAUVEGARDE GLOBALE : CONFIGURATION DU FOYER (Unifiée)
|
||||||
require_once __DIR__ . '/includes/db.php';
|
if ($action === 'update_family_info') {
|
||||||
|
header('Content-Type: application/json');
|
||||||
$foyer = $pdo->query("SELECT currency, zone_scolaire FROM pf_foyer_settings WHERE id = 1")->fetch();
|
try {
|
||||||
$currency = isset($_POST['currency']) ? trim($_POST['currency']) : ($foyer['currency'] ?? '€');
|
require_once __DIR__ . '/includes/db.php';
|
||||||
$zone = isset($_POST['zone_scolaire']) ? trim($_POST['zone_scolaire']) : ($foyer['zone_scolaire'] ?? 'C');
|
$pdo->beginTransaction();
|
||||||
$stmtSave = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?, zone_scolaire = ? WHERE id = 1");
|
|
||||||
$stmtSave->execute([$currency, $zone]);
|
|
||||||
$success = "Paramètres du foyer mis à jour avec succès.";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'set_modules' && $family_id) {
|
// 1. Sauvegarde des paramètres globaux
|
||||||
$all = ['calendar', 'budget', 'holidays', 'gifts', 'garage', 'memo', 'todo', 'liste', 'calendar_ios', 'printvault', 'planka'];
|
$currency = trim($_POST['currency'] ?? '€');
|
||||||
$enabled = array_values(array_filter($all, fn($m) => isset($_POST['mod_' . $m])));
|
$zone = trim($_POST['zone_scolaire'] ?? 'C');
|
||||||
if (empty($enabled)) {
|
$careModesJson = $_POST['custom_care_modes'] ?? '[]';
|
||||||
$error = "Vous devez garder au moins un module actif.";
|
|
||||||
} else {
|
|
||||||
$meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?")
|
|
||||||
->execute([json_encode($enabled), $family_id]);
|
|
||||||
$_SESSION['enabled_modules'] = $enabled;
|
|
||||||
$success = "Modules mis à jour.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'set_lang') {
|
$stmtSave = $pdo->prepare("UPDATE pf_foyer_settings SET currency = ?, zone_scolaire = ?, care_modes = ? WHERE id = 1");
|
||||||
$lang = $_POST['lang'] ?? 'fr';
|
$stmtSave->execute([$currency, $zone, $careModesJson]);
|
||||||
if (in_array($lang, ['fr', 'ca', 'en'])) {
|
|
||||||
$_SESSION['app_lang'] = $lang;
|
|
||||||
$meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?")
|
|
||||||
->execute([$lang, $user_id]);
|
|
||||||
$success = "Language updated.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🟢 ENRICHISSEMENT : Prise en compte de la couleur du profil
|
// 2. Gestion des enfants
|
||||||
if ($action === 'update_profile') {
|
$kids = $_POST['kids'] ?? [];
|
||||||
$display_name = trim($_POST['display_name'] ?? '');
|
$deleted = json_decode($_POST['deleted_kids'] ?? '[]', true);
|
||||||
$color = trim($_POST['color'] ?? '#0891b2');
|
|
||||||
|
|
||||||
if (!$display_name) {
|
if (!empty($deleted)) {
|
||||||
$error = "Le prénom ne peut pas être vide.";
|
$in = str_repeat('?,', count($deleted) - 1) . '?';
|
||||||
} else {
|
$stmtDel = $pdo->prepare("DELETE FROM pf_people WHERE id IN ($in) AND role = 'enfant'");
|
||||||
// MÀJ dans la base Meta (users)
|
$stmtDel->execute($deleted);
|
||||||
$meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?")
|
|
||||||
->execute([$display_name, $user_id]);
|
|
||||||
|
|
||||||
// MÀJ dans la base locale Foyer (pf_people) via le user_id mappé
|
|
||||||
require_once __DIR__ . '/includes/db.php';
|
|
||||||
if (isset($pdo)) {
|
|
||||||
$stmtPeopleColor = $pdo->prepare("UPDATE pf_people SET color = ? WHERE user_id = ?");
|
|
||||||
$stmtPeopleColor->execute([$color, $user_id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$_SESSION['user']['display_name'] = $display_name;
|
|
||||||
$success = "Profil mis à jour.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'change_password') {
|
|
||||||
$current = $_POST['current_password'] ?? '';
|
|
||||||
$new = $_POST['new_password'] ?? '';
|
|
||||||
$confirm = $_POST['confirm_password'] ?? '';
|
|
||||||
|
|
||||||
$row = $meta_pdo->prepare("SELECT password_hash FROM users WHERE id = ?");
|
|
||||||
$row->execute([$user_id]);
|
|
||||||
$row = $row->fetch();
|
|
||||||
|
|
||||||
if (!password_verify($current, $row['password_hash'])) {
|
|
||||||
$error = "Mot de passe actuel incorrect.";
|
|
||||||
} elseif (strlen($new) < 6) {
|
|
||||||
$error = "Le nouveau mot de passe doit faire au moins 6 caractères.";
|
|
||||||
} elseif ($new !== $confirm) {
|
|
||||||
$error = "Les mots de passe ne correspondent pas.";
|
|
||||||
} else {
|
|
||||||
$meta_pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?")
|
|
||||||
->execute([password_hash($new, PASSWORD_BCRYPT), $user_id]);
|
|
||||||
$success = "Mot de passe modifié avec succès.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'update_family_name' && $family_id) {
|
|
||||||
$name = trim($_POST['family_name'] ?? '');
|
|
||||||
if ($name) {
|
|
||||||
$meta_pdo->prepare("UPDATE families SET name = ? WHERE id = ?")
|
|
||||||
->execute([$name, $family_id]);
|
|
||||||
$success = "Nom de l'espace mis à jour.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'regen_invite' && $family_id) {
|
|
||||||
$new_code = bin2hex(random_bytes(8));
|
|
||||||
$meta_pdo->prepare("UPDATE families SET invite_code = ? WHERE id = ?")
|
|
||||||
->execute([$new_code, $family_id]);
|
|
||||||
$success = "Nouveau code d'invitation généré.";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'upload_home_bg' && $family_id) {
|
|
||||||
$upload_dir = '/uploads/';
|
|
||||||
if (!is_dir($upload_dir)) @mkdir($upload_dir, 0755, true);
|
|
||||||
$file = $_FILES['home_bg'] ?? null;
|
|
||||||
if ($file && $file['error'] === 0) {
|
|
||||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
|
||||||
if (!in_array($ext, ['jpg','jpeg','png','webp'])) {
|
|
||||||
$error = "Format non supporté (jpg, png, webp).";
|
|
||||||
} else {
|
|
||||||
$dest = $upload_dir . 'home_bg_' . $family_id . '.' . $ext;
|
|
||||||
foreach (glob($upload_dir . 'home_bg_' . $family_id . '.*') as $old) @unlink($old);
|
|
||||||
if (move_uploaded_file($file['tmp_name'], $dest)) {
|
|
||||||
$success = "Image d'accueil mise à jour.";
|
|
||||||
} else {
|
|
||||||
$error = "Erreur lors de l'upload.";
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'reset_home_bg' && $family_id) {
|
$stmtInsert = $pdo->prepare("INSERT INTO pf_people (name, role, care_modes) VALUES (?, 'enfant', ?)");
|
||||||
foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $old) @unlink($old);
|
$stmtUpdate = $pdo->prepare("UPDATE pf_people SET name = ?, care_modes = ? WHERE id = ? AND role = 'enfant'");
|
||||||
$success = "Image d'accueil réinitialisée.";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'grocery_history_max' && $family_id) {
|
foreach ($kids as $kidId => $kidData) {
|
||||||
require_once __DIR__ . '/includes/db.php';
|
$name = trim($kidData['name'] ?? '');
|
||||||
if (!isset($pdo)) {
|
if (empty($name)) continue;
|
||||||
$error = "Base famille indisponible.";
|
|
||||||
} else {
|
|
||||||
$n = (int) ($_POST['history_max'] ?? 20);
|
|
||||||
$n = max(1, min(50, $n));
|
|
||||||
try {
|
|
||||||
$pdo->prepare(
|
|
||||||
"INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('grocery_settings','history_max',?) ON DUPLICATE KEY UPDATE content=VALUES(content)"
|
|
||||||
)->execute([(string) $n]);
|
|
||||||
$success = tr('groceries_settings_saved');
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$error = tr('groceries_settings_error') . ' ' . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'calendar_ios_save') {
|
|
||||||
$username = trim($_POST['icloud_username'] ?? '');
|
|
||||||
$appPassword = hh_normalize_apple_app_password((string) ($_POST['icloud_app_password'] ?? ''));
|
|
||||||
$calendarUrl = trim($_POST['icloud_calendar_url'] ?? '');
|
|
||||||
|
|
||||||
if (!$username || !$appPassword || !$calendarUrl) {
|
$modes = $kidData['modes'] ?? [];
|
||||||
$error = "Merci de renseigner identifiant iCloud, mot de passe d'app et URL CalDAV.";
|
$modesJson = json_encode(array_values($modes));
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
$resolvedUrl = hh_icloud_resolve_calendar_url_if_needed($username, $appPassword, $calendarUrl);
|
|
||||||
$encrypted = hh_encrypt_secret($appPassword);
|
|
||||||
$meta_pdo->prepare("
|
|
||||||
INSERT INTO user_calendar_integrations (user_id, provider, username, secret_encrypted, calendar_url, status, updated_at)
|
|
||||||
VALUES (?, 'icloud_caldav', ?, ?, ?, 'connected', NOW())
|
|
||||||
ON DUPLICATE KEY UPDATE username=VALUES(username), secret_encrypted=VALUES(secret_encrypted), calendar_url=VALUES(calendar_url), status='connected', updated_at=NOW()
|
|
||||||
")->execute([$user_id, $username, $encrypted, $resolvedUrl]);
|
|
||||||
$msg = "Connexion calendrier iOS enregistrée.";
|
|
||||||
if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) {
|
|
||||||
$msg .= " URL du calendrier détectée automatiquement (tu avais mis la racine iCloud).";
|
|
||||||
}
|
|
||||||
$success = $msg;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$error = "Impossible d'enregistrer la connexion iOS: " . $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'calendar_ios_test') {
|
if (strpos($kidId, 'new_') === 0) {
|
||||||
$row = $meta_pdo->prepare("SELECT id, username, secret_encrypted, calendar_url FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'");
|
$stmtInsert->execute([$name, $modesJson]);
|
||||||
$row->execute([$user_id]);
|
} else {
|
||||||
$integration = $row->fetch();
|
$stmtUpdate->execute([$name, $modesJson, (int)$kidId]);
|
||||||
if (!$integration) {
|
|
||||||
$error = "Aucune connexion iOS configurée.";
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
$pwd = hh_normalize_apple_app_password(hh_decrypt_secret($integration['secret_encrypted']));
|
|
||||||
$calendarUrl = trim($integration['calendar_url']);
|
|
||||||
$resolvedUrl = hh_icloud_resolve_calendar_url_if_needed($integration['username'], $pwd, $calendarUrl);
|
|
||||||
$code = hh_caldav_test_calendar_collection($integration['username'], $pwd, $resolvedUrl);
|
|
||||||
if (in_array($code, [200, 207], true)) {
|
|
||||||
if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) {
|
|
||||||
$meta_pdo->prepare("UPDATE user_calendar_integrations SET calendar_url = ? WHERE id = ?")
|
|
||||||
->execute([$resolvedUrl, $integration['id']]);
|
|
||||||
}
|
}
|
||||||
$success = "Connexion iCloud CalDAV valide (HTTP $code).";
|
|
||||||
} else {
|
|
||||||
$error = "Test connexion échoué (HTTP $code). Vérifie identifiant Apple, mot de passe d’app (16 caractères) et que le compte iCloud a le Calendrier activé.";
|
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$error = "Test connexion impossible: " . $e->getMessage();
|
$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) {
|
||||||
|
$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])));
|
||||||
|
if (empty($enabled)) {
|
||||||
|
$error = "Vous devez garder au moins un module actif.";
|
||||||
|
} else {
|
||||||
|
$meta_pdo->prepare("UPDATE families SET enabled_modules = ? WHERE id = ?")
|
||||||
|
->execute([json_encode($enabled), $family_id]);
|
||||||
|
$_SESSION['enabled_modules'] = $enabled;
|
||||||
|
$success = "Modules mis à jour.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($action === 'calendar_ios_disconnect') {
|
if ($action === 'set_lang') {
|
||||||
$meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]);
|
$lang = $_POST['lang'] ?? 'fr';
|
||||||
$success = "Connexion iOS supprimée.";
|
if (in_array($lang, ['fr', 'ca', 'en'])) {
|
||||||
}
|
$_SESSION['app_lang'] = $lang;
|
||||||
|
$meta_pdo->prepare("UPDATE users SET lang = ? WHERE id = ?")
|
||||||
|
->execute([$lang, $user_id]);
|
||||||
|
$success = "Language updated.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'update_profile') {
|
||||||
|
$display_name = trim($_POST['display_name'] ?? '');
|
||||||
|
$color = trim($_POST['color'] ?? '#0891b2');
|
||||||
|
|
||||||
|
if (!$display_name) {
|
||||||
|
$error = "Le prénom ne peut pas être vide.";
|
||||||
|
} else {
|
||||||
|
$meta_pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?")
|
||||||
|
->execute([$display_name, $user_id]);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/db.php';
|
||||||
|
if (isset($pdo)) {
|
||||||
|
$stmtPeopleColor = $pdo->prepare("UPDATE pf_people SET color = ? WHERE user_id = ?");
|
||||||
|
$stmtPeopleColor->execute([$color, $user_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['user']['display_name'] = $display_name;
|
||||||
|
$success = "Profil mis à jour.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'change_password') {
|
||||||
|
$current = $_POST['current_password'] ?? '';
|
||||||
|
$new = $_POST['new_password'] ?? '';
|
||||||
|
$confirm = $_POST['confirm_password'] ?? '';
|
||||||
|
|
||||||
|
$row = $meta_pdo->prepare("SELECT password_hash FROM users WHERE id = ?");
|
||||||
|
$row->execute([$user_id]);
|
||||||
|
$row = $row->fetch();
|
||||||
|
|
||||||
|
if (!password_verify($current, $row['password_hash'])) {
|
||||||
|
$error = "Mot de passe actuel incorrect.";
|
||||||
|
} elseif (strlen($new) < 6) {
|
||||||
|
$error = "Le nouveau mot de passe doit faire au moins 6 caractères.";
|
||||||
|
} elseif ($new !== $confirm) {
|
||||||
|
$error = "Les mots de passe ne correspondent pas.";
|
||||||
|
} else {
|
||||||
|
$meta_pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?")
|
||||||
|
->execute([password_hash($new, PASSWORD_BCRYPT), $user_id]);
|
||||||
|
$success = "Mot de passe modifié avec succès.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'update_family_name' && $family_id) {
|
||||||
|
$name = trim($_POST['family_name'] ?? '');
|
||||||
|
if ($name) {
|
||||||
|
$meta_pdo->prepare("UPDATE families SET name = ? WHERE id = ?")
|
||||||
|
->execute([$name, $family_id]);
|
||||||
|
$success = "Nom de l'espace mis à jour.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'regen_invite' && $family_id) {
|
||||||
|
$new_code = bin2hex(random_bytes(8));
|
||||||
|
$meta_pdo->prepare("UPDATE families SET invite_code = ? WHERE id = ?")
|
||||||
|
->execute([$new_code, $family_id]);
|
||||||
|
$success = "Nouveau code d'invitation généré.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'upload_home_bg' && $family_id) {
|
||||||
|
$upload_dir = '/uploads/';
|
||||||
|
if (!is_dir($upload_dir)) @mkdir($upload_dir, 0755, true);
|
||||||
|
$file = $_FILES['home_bg'] ?? null;
|
||||||
|
if ($file && $file['error'] === 0) {
|
||||||
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
|
if (!in_array($ext, ['jpg','jpeg','png','webp'])) {
|
||||||
|
$error = "Format non supporté (jpg, png, webp).";
|
||||||
|
} else {
|
||||||
|
$dest = $upload_dir . 'home_bg_' . $family_id . '.' . $ext;
|
||||||
|
foreach (glob($upload_dir . 'home_bg_' . $family_id . '.*') as $old) @unlink($old);
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $dest)) {
|
||||||
|
$success = "Image d'accueil mise à jour.";
|
||||||
|
} else {
|
||||||
|
$error = "Erreur lors de l'upload.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'reset_home_bg' && $family_id) {
|
||||||
|
foreach (glob('/uploads/home_bg_' . $family_id . '.*') as $old) @unlink($old);
|
||||||
|
$success = "Image d'accueil réinitialisée.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'grocery_history_max' && $family_id) {
|
||||||
|
require_once __DIR__ . '/includes/db.php';
|
||||||
|
if (!isset($pdo)) {
|
||||||
|
$error = "Base famille indisponible.";
|
||||||
|
} else {
|
||||||
|
$n = (int) ($_POST['history_max'] ?? 20);
|
||||||
|
$n = max(1, min(50, $n));
|
||||||
|
try {
|
||||||
|
$pdo->prepare(
|
||||||
|
"INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('grocery_settings','history_max',?) ON DUPLICATE KEY UPDATE content=VALUES(content)"
|
||||||
|
)->execute([(string) $n]);
|
||||||
|
$success = tr('groceries_settings_saved');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$error = tr('groceries_settings_error') . ' ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'calendar_ios_save') {
|
||||||
|
$username = trim($_POST['icloud_username'] ?? '');
|
||||||
|
$appPassword = hh_normalize_apple_app_password((string) ($_POST['icloud_app_password'] ?? ''));
|
||||||
|
$calendarUrl = trim($_POST['icloud_calendar_url'] ?? '');
|
||||||
|
|
||||||
|
if (!$username || !$appPassword || !$calendarUrl) {
|
||||||
|
$error = "Merci de renseigner identifiant iCloud, mot de passe d'app et URL CalDAV.";
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$resolvedUrl = hh_icloud_resolve_calendar_url_if_needed($username, $appPassword, $calendarUrl);
|
||||||
|
$encrypted = hh_encrypt_secret($appPassword);
|
||||||
|
$meta_pdo->prepare("
|
||||||
|
INSERT INTO user_calendar_integrations (user_id, provider, username, secret_encrypted, calendar_url, status, updated_at)
|
||||||
|
VALUES (?, 'icloud_caldav', ?, ?, ?, 'connected', NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE username=VALUES(username), secret_encrypted=VALUES(secret_encrypted), calendar_url=VALUES(calendar_url), status='connected', updated_at=NOW()
|
||||||
|
")->execute([$user_id, $username, $encrypted, $resolvedUrl]);
|
||||||
|
$msg = "Connexion calendrier iOS enregistrée.";
|
||||||
|
if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) {
|
||||||
|
$msg .= " URL du calendrier détectée automatiquement.";
|
||||||
|
}
|
||||||
|
$success = $msg;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$error = "Impossible d'enregistrer la connexion iOS: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'calendar_ios_test') {
|
||||||
|
$row = $meta_pdo->prepare("SELECT id, username, secret_encrypted, calendar_url FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'");
|
||||||
|
$row->execute([$user_id]);
|
||||||
|
$integration = $row->fetch();
|
||||||
|
if (!$integration) {
|
||||||
|
$error = "Aucune connexion iOS configurée.";
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$pwd = hh_normalize_apple_app_password(hh_decrypt_secret($integration['secret_encrypted']));
|
||||||
|
$calendarUrl = trim($integration['calendar_url']);
|
||||||
|
$resolvedUrl = hh_icloud_resolve_calendar_url_if_needed($integration['username'], $pwd, $calendarUrl);
|
||||||
|
$code = hh_caldav_test_calendar_collection($integration['username'], $pwd, $resolvedUrl);
|
||||||
|
if (in_array($code, [200, 207], true)) {
|
||||||
|
if (rtrim($resolvedUrl, '/') !== rtrim($calendarUrl, '/')) {
|
||||||
|
$meta_pdo->prepare("UPDATE user_calendar_integrations SET calendar_url = ? WHERE id = ?")
|
||||||
|
->execute([$resolvedUrl, $integration['id']]);
|
||||||
|
}
|
||||||
|
$success = "Connexion iCloud CalDAV valide (HTTP $code).";
|
||||||
|
} else {
|
||||||
|
$error = "Test connexion échoué (HTTP $code). Vérifiez vos identifiants.";
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$error = "Test connexion impossible: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'calendar_ios_disconnect') {
|
||||||
|
$meta_pdo->prepare("DELETE FROM user_calendar_integrations WHERE user_id = ? AND provider='icloud_caldav'")->execute([$user_id]);
|
||||||
|
$success = "Connexion iOS supprimée.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Chargement données ───────────────────────────────────────────────────────
|
// ─── Chargement données ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
// 1. On charge la base locale AVANT pour que son $user n'écrase pas le nôtre
|
|
||||||
if ($family_id) {
|
if ($family_id) {
|
||||||
require_once __DIR__ . '/includes/db.php';
|
require_once __DIR__ . '/includes/db.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Maintenant on peut charger ton profil en toute sécurité
|
|
||||||
$stmtUser = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
|
$stmtUser = $meta_pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
$stmtUser->execute([$user_id]);
|
$stmtUser->execute([$user_id]);
|
||||||
$user = $stmtUser->fetch();
|
$user = $stmtUser->fetch();
|
||||||
|
|
||||||
// 3. Récupération de la couleur actuelle depuis pf_people
|
$user_color = '#0891b2';
|
||||||
$user_color = '#0891b2'; // Fallback par défaut
|
|
||||||
if ($family_id && isset($pdo)) {
|
if ($family_id && isset($pdo)) {
|
||||||
$stmtColorFetch = $pdo->prepare("SELECT color FROM pf_people WHERE user_id = ?");
|
$stmtColorFetch = $pdo->prepare("SELECT color FROM pf_people WHERE user_id = ?");
|
||||||
$stmtColorFetch->execute([$user_id]);
|
$stmtColorFetch->execute([$user_id]);
|
||||||
@@ -258,6 +297,27 @@ 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 = [];
|
||||||
if ($family_id) {
|
if ($family_id) {
|
||||||
@@ -275,18 +335,13 @@ $calendarIntegration->execute([$user_id]);
|
|||||||
$calendarIntegration = $calendarIntegration->fetch();
|
$calendarIntegration = $calendarIntegration->fetch();
|
||||||
|
|
||||||
$groceryHistoryMaxSetting = 20;
|
$groceryHistoryMaxSetting = 20;
|
||||||
if ($family_id) {
|
if ($family_id && isset($pdo)) {
|
||||||
require_once __DIR__ . '/includes/db.php';
|
try {
|
||||||
if (isset($pdo)) {
|
$gv = $pdo->query("SELECT content FROM pf_notes WHERE note_type='grocery_settings' AND reference_id='history_max'")->fetchColumn();
|
||||||
try {
|
if ($gv !== false && $gv !== null && $gv !== '') {
|
||||||
$gv = $pdo->query("SELECT content FROM pf_notes WHERE note_type='grocery_settings' AND reference_id='history_max'")->fetchColumn();
|
$groceryHistoryMaxSetting = max(1, min(50, (int) $gv));
|
||||||
if ($gv !== false && $gv !== null && $gv !== '') {
|
|
||||||
$groceryHistoryMaxSetting = max(1, min(50, (int) $gv));
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
}
|
} catch (\Throwable $e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
$pageTitle = "Paramètres — HouseHub";
|
$pageTitle = "Paramètres — HouseHub";
|
||||||
@@ -363,7 +418,7 @@ require __DIR__ . '/header.php';
|
|||||||
</label>
|
</label>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="pf-btn">Enregistrer</button>
|
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -385,20 +440,12 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
<button type="submit" class="pf-btn"><?= htmlspecialchars(tr('btn_save')) ?></button>
|
<button type="submit" class="pf-btn"><?= htmlspecialchars(tr('btn_save')) ?></button>
|
||||||
</form>
|
</form>
|
||||||
<p class="pf-muted-note" style="margin-top:1rem">
|
|
||||||
<a href="/liste.php" style="color:var(--primary);font-weight:600;"><?= htmlspecialchars(tr('mod_liste_name')) ?></a>
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<section class="pf-panel-card">
|
<section class="pf-panel-card">
|
||||||
<h2 class="pf-card-h2 pf-card-h2--tight">📱 Intégration Calendrier iOS (CalDAV)</h2>
|
<h2 class="pf-card-h2 pf-card-h2--tight">📱 Intégration Calendrier iOS (CalDAV)</h2>
|
||||||
<p class="pf-muted-note">Configurez ici votre calendrier iCloud pour synchroniser les événements créés dans HouseHub.</p>
|
<p class="pf-muted-note">Configurez ici votre calendrier iCloud pour synchroniser les événements créés dans HouseHub.</p>
|
||||||
<p class="pf-muted-note" style="margin-top:8px;">
|
|
||||||
Pour voir et gérer les événements : ouvrez le module
|
|
||||||
<a href="/calendar-ios.php" style="color:var(--primary);font-weight:600;">Calendrier iOS</a>
|
|
||||||
(menu du haut ou burger « Calendrier iOS », ou carte sur l’accueil). Le module doit être coché dans « Modules actifs » ci-dessus.
|
|
||||||
</p>
|
|
||||||
<form method="post" class="pf-stack-md">
|
<form method="post" class="pf-stack-md">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||||
<input type="hidden" name="action" value="calendar_ios_save">
|
<input type="hidden" name="action" value="calendar_ios_save">
|
||||||
@@ -415,7 +462,7 @@ require __DIR__ . '/header.php';
|
|||||||
<input type="url" name="icloud_calendar_url" class="pf-input" value="<?= htmlspecialchars($calendarIntegration['calendar_url'] ?? '') ?>" placeholder="https://caldav.icloud.com/..." required>
|
<input type="url" name="icloud_calendar_url" class="pf-input" value="<?= htmlspecialchars($calendarIntegration['calendar_url'] ?? '') ?>" placeholder="https://caldav.icloud.com/..." required>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-flex-gap-8">
|
<div class="pf-flex-gap-8">
|
||||||
<button type="submit" class="pf-btn">Enregistrer</button>
|
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -431,13 +478,6 @@ require __DIR__ . '/header.php';
|
|||||||
<button type="submit" class="pf-btn btn-secondary">Déconnecter</button>
|
<button type="submit" class="pf-btn btn-secondary">Déconnecter</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<p class="pf-muted-note" style="margin-top:10px;">
|
|
||||||
Après enregistrement, utilisez « Tester la connexion » : un message vert confirme que l’URL CalDAV répond avec tes identifiants.
|
|
||||||
Sur la page Calendrier iOS, le bouton « Synchroniser » envoie les événements vers iCloud et récupère ceux créés sur l’iPhone.
|
|
||||||
</p>
|
|
||||||
<?php if (!empty($calendarIntegration['last_sync_at'])): ?>
|
|
||||||
<p class="pf-muted-note">Dernière synchro: <?= htmlspecialchars($calendarIntegration['last_sync_at']) ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<?php if ($family_id):
|
<?php if ($family_id):
|
||||||
@@ -479,47 +519,89 @@ require __DIR__ . '/header.php';
|
|||||||
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$enabledMods = $_SESSION['enabled_modules'] ?? [];
|
$showCurrency = count(array_intersect(['budget', 'holidays', 'gifts', 'garage'], $_SESSION['enabled_modules'] ?? [])) > 0;
|
||||||
|
$showZone = in_array('calendar', $_SESSION['enabled_modules'] ?? []);
|
||||||
$showCurrency = count(array_intersect(['budget', 'holidays', 'gifts', 'garage'], $enabledMods)) > 0;
|
|
||||||
$showZone = in_array('calendar', $enabledMods);
|
|
||||||
|
|
||||||
if ($family_id && ($showCurrency || $showZone)):
|
if ($family_id):
|
||||||
$currentCurrency = defined('CURRENCY') ? CURRENCY : '€';
|
$currentCurrency = defined('CURRENCY') ? CURRENCY : '€';
|
||||||
$currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C';
|
$currentZone = defined('ZONE_SCOLAIRE') ? ZONE_SCOLAIRE : 'C';
|
||||||
?>
|
?>
|
||||||
<section class="pf-panel-card">
|
<section class="pf-panel-card">
|
||||||
<h2 class="pf-card-h2">👪 Configuration du foyer</h2>
|
<h2 class="pf-card-h2" style="font-size: 1.25rem;">👪 <?= tr('set_family_config') ?></h2>
|
||||||
<p class="pf-muted-note">Configurez les indicateurs partagés par votre foyer.</p>
|
<p class="pf-muted-note">Configurez les indicateurs et les membres partagés par votre foyer.</p>
|
||||||
|
|
||||||
<form method="post" class="pf-stack-md">
|
<form id="unifiedFamilyForm" style="margin-top: 20px; display: flex; flex-direction: column;">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
|
||||||
<input type="hidden" name="action" value="update_family_info">
|
<input type="hidden" name="action" value="update_family_info">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrf_token()) ?>">
|
||||||
<div class="form-row">
|
<input type="hidden" name="custom_care_modes" id="customCareModes" value="<?= htmlspecialchars(json_encode($familyCareModes)) ?>">
|
||||||
|
<input type="hidden" name="deleted_kids" id="deletedKids" value="[]">
|
||||||
<?php if ($showCurrency): ?>
|
|
||||||
<div class="pf-form-group">
|
|
||||||
<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): ?>
|
<!-- ACCORDÉON 1 : PARAMÈTRES GLOBAUX -->
|
||||||
<div class="pf-form-group">
|
<?php if ($showCurrency || $showZone): ?>
|
||||||
<label class="pf-label">Zone de vacances scolaires (France)</label>
|
<details class="pf-accordion">
|
||||||
<select name="zone_scolaire" class="pf-input" style="background:var(--bg-panel);">
|
<summary class="pf-accordion-summary">🌍 <?= tr('set_global_params') ?></summary>
|
||||||
<option value="A" <?= $currentZone === 'A' ? 'selected' : '' ?>>Zone A</option>
|
<div class="pf-accordion-content">
|
||||||
<option value="B" <?= $currentZone === 'B' ? 'selected' : '' ?>>Zone B</option>
|
<div class="form-row" style="margin-bottom: 0;">
|
||||||
<option value="C" <?= $currentZone === 'C' ? 'selected' : '' ?>>Zone C</option>
|
<?php if ($showCurrency): ?>
|
||||||
<option value="Autre" <?= $currentZone === 'Autre' ? 'selected' : '' ?>>Hors France / Autre</option>
|
<div class="pf-form-group" style="margin-bottom: 0;">
|
||||||
</select>
|
<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>
|
</div>
|
||||||
<?php endif; ?>
|
</details>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
|
||||||
|
<!-- ACCORDÉON 2 : MODES DE GARDE -->
|
||||||
<button type="submit" class="pf-btn">Enregistrer les paramètres du foyer</button>
|
<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>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -547,7 +629,7 @@ require __DIR__ . '/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="pf-btn">Enregistrer</button>
|
<button type="submit" class="pf-btn"><?= tr('btn_save') ?></button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -655,6 +737,142 @@ function copyCode() {
|
|||||||
setTimeout(() => msg.classList.remove('is-visible'), 2000);
|
setTimeout(() => msg.classList.remove('is-visible'), 2000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🟢 LOGIQUE FRONT-END UNIFIÉE (Vanilla JS) : Configuration Foyer
|
||||||
|
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', () => {
|
||||||
|
if (document.getElementById('careModesContainer')) renderCareModes();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php require __DIR__ . '/footer.php'; ?>
|
<?php require __DIR__ . '/footer.php'; ?>
|
||||||
Reference in New Issue
Block a user