Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
536f479423 | ||
|
|
685c649e7c | ||
|
|
074d8e12d2 | ||
|
|
f748330a1b | ||
|
|
ebdbee71db | ||
|
|
659b286967 | ||
|
|
ac8ebf7328 | ||
|
|
49d4ec7c35 | ||
|
|
c48a6ba116 | ||
|
|
20d6c9b7b0 | ||
|
|
d856d0a409 | ||
|
|
86ef3ccbf2 | ||
|
|
3e98de9c18 |
+7
-1
@@ -59,12 +59,18 @@ require __DIR__ . '/header.php';
|
||||
<span class="tab-icon">📊</span>
|
||||
<span><?= tr('budget_tab_recap') ?></span>
|
||||
</a>
|
||||
|
||||
<a href="?tab=provisions" class="tab-item <?= $tab == 'provisions' ? 'active' : '' ?>">
|
||||
<span class="tab-icon">🧮</span>
|
||||
<span><?= tr('budget_tab_provisions') ?></span>
|
||||
</a>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<section class="pf-section">
|
||||
<?php
|
||||
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev'];
|
||||
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev', 'provisions'];
|
||||
|
||||
if (in_array($tab, $allowedTabs)) {
|
||||
$viewPath = __DIR__ . "/modules/budget/views/" . $tab . ".php";
|
||||
|
||||
@@ -172,6 +172,18 @@ CREATE TABLE IF NOT EXISTS pf_expenses (
|
||||
salary_id INT DEFAULT NULL -- 🔥 AJOUT : Lien vers le salaire configuré
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pf_expected_expenses` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(150) NOT NULL,
|
||||
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
|
||||
`expected_date` DATE NOT NULL,
|
||||
`is_paid` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_expected_date` (`expected_date`),
|
||||
INDEX `idx_is_paid` (`is_paid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
@@ -297,7 +309,8 @@ CREATE TABLE IF NOT EXISTS pf_holidays (
|
||||
budget_extra DECIMAL(10,2) DEFAULT 0,
|
||||
notes TEXT DEFAULT NULL,
|
||||
vehicle_id INT DEFAULT NULL,
|
||||
return_step_id INT DEFAULT NULL, -- 🔥 NOUVEAU : Point de bascule du retour
|
||||
return_step_id INT DEFAULT NULL,
|
||||
image_url VARCHAR(500) DEFAULT NULL,
|
||||
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
|
||||
+6
-5
@@ -441,10 +441,9 @@ require __DIR__ . '/header.php';
|
||||
<span style="font-size:1.1rem; line-height:1; margin-right:6px;">🏫</span>
|
||||
<span><?= htmlspecialchars($mode) ?></span>
|
||||
</div>
|
||||
<?php else:
|
||||
$hue = ($index * 137) % 360; ?>
|
||||
<?php else: ?>
|
||||
<div class="pf-legend-item">
|
||||
<div class="pf-legend-color" style="background: hsl(<?= $hue ?>, 70%, 50%);"></div>
|
||||
<div class="pf-legend-color" style="background: var(--primary);"></div>
|
||||
<span><?= htmlspecialchars($mode) ?></span>
|
||||
</div>
|
||||
<?php endif;
|
||||
@@ -452,7 +451,8 @@ require __DIR__ . '/header.php';
|
||||
|
||||
<!-- Enfants Malades (Dynamique avec couleur BDD) -->
|
||||
<?php foreach ($kids as $kid):
|
||||
$color = !empty($kid['color']) ? $kid['color'] : 'var(--danger)';
|
||||
// Fallback aligné exactement sur celui du JS (#e11d48)
|
||||
$color = !empty($kid['color']) ? $kid['color'] : '#e11d48';
|
||||
?>
|
||||
<div class="pf-legend-item">
|
||||
<div class="pf-legend-color" style="background: <?= htmlspecialchars($color) ?>; opacity: 0.8;"></div>
|
||||
@@ -477,5 +477,6 @@ window.FAMILY_CONFIG = {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script src="/modules/family-calendar/family-calendar.js"></script>
|
||||
|
||||
<script src="/modules/family-calendar/family-calendar.js?v=<?= time() ?>"></script>
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
+13
-2
@@ -34,7 +34,18 @@ if ($tab === 'holiday_detail' && isset($_GET['id'])) {
|
||||
require __DIR__ . '/modules/holidays/views/list.php';
|
||||
}
|
||||
|
||||
// 4. Inclusion du JS global du module (Pont i18n déjà géré dans le header)
|
||||
echo '<script src="/modules/holidays/holidays.js"></script>';
|
||||
// 4. Inclusion des librairies globales du module (Flatpickr)
|
||||
?>
|
||||
<!-- Flatpickr (Vanilla JS Date Range) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/dark.css" id="flatpickr-dark-theme" disabled>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/fr.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/cat.js"></script>
|
||||
|
||||
<?php
|
||||
$jsPath = __DIR__ . '/modules/holidays/holidays.js';
|
||||
$jsVersion = file_exists($jsPath) ? filemtime($jsPath) : time();
|
||||
echo '<script src="/modules/holidays/holidays.js?v=' . $jsVersion . '"></script>';
|
||||
|
||||
require __DIR__ . '/footer.php';
|
||||
+45
-38
@@ -1,52 +1,59 @@
|
||||
<?php
|
||||
// includes/db.php
|
||||
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
require_once __DIR__ . '/config.php';
|
||||
}
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$host = getenv('DB_HOST') ?: 'househub-db';
|
||||
$user = getenv('DB_USER') ?: 'househub';
|
||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
||||
$db = $_SESSION['family_db'] ?? null;
|
||||
$envHost = getenv('DB_HOST');
|
||||
|
||||
if (!$db) {
|
||||
$current = basename($_SERVER['PHP_SELF'] ?? '');
|
||||
if (!in_array($current, ['login.php', 'register.php'])) {
|
||||
header('Location: /login.php');
|
||||
exit;
|
||||
}
|
||||
return;
|
||||
// 1. Détection stricte de l'environnement
|
||||
if ($envHost === 'househub-db') {
|
||||
// Docker
|
||||
$host = 'househub-db';
|
||||
$user = getenv('DB_USER') ?: 'househub';
|
||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
||||
} else {
|
||||
// XAMPP
|
||||
$host = '127.0.0.1'; // Plus stable que 'localhost' sous XAMPP
|
||||
$user = 'root';
|
||||
$pass = '';
|
||||
}
|
||||
|
||||
// 2. Multi-tenant strict : cibler la DB de la famille courante
|
||||
$db = 'househub_meta'; // Fallback par défaut
|
||||
if (!empty($_SESSION['user']['family_id'])) {
|
||||
$db = 'househub_f' . (int)$_SESSION['user']['family_id'];
|
||||
}
|
||||
|
||||
$charset = 'utf8mb4';
|
||||
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
||||
PDO::ATTR_TIMEOUT => 30,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
"mysql:host=$host;dbname=$db;charset=utf8mb4",
|
||||
$user, $pass,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
||||
PDO::ATTR_TIMEOUT => 30,
|
||||
]
|
||||
);
|
||||
$pdo->exec("SET collation_connection = utf8mb4_general_ci");
|
||||
$pdo = new PDO($dsn, $user, $pass, $options);
|
||||
|
||||
try {
|
||||
$foyer = $pdo->query("SELECT currency, zone_scolaire FROM pf_foyer_settings WHERE id = 1")->fetch();
|
||||
if (!defined('CURRENCY')) {
|
||||
define('CURRENCY', $foyer['currency'] ?? '€');
|
||||
}
|
||||
if (!defined('ZONE_SCOLAIRE')) {
|
||||
define('ZONE_SCOLAIRE', $foyer['zone_scolaire'] ?? 'C');
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
if (!defined('CURRENCY')) define('CURRENCY', '€');
|
||||
if (!defined('ZONE_SCOLAIRE')) define('ZONE_SCOLAIRE', 'C');
|
||||
}
|
||||
// ------------------------------------------------
|
||||
// Forcer la collation
|
||||
$pdo->exec("SET collation_connection = utf8mb4_general_ci");
|
||||
$pdo->exec("SET collation_database = utf8mb4_general_ci");
|
||||
$pdo->exec("SET collation_server = utf8mb4_general_ci");
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
if (!headers_sent()) { header('Content-Type: application/json'); http_response_code(500); }
|
||||
die(json_encode(['ok' => false, 'error' => 'Erreur BDD : ' . $e->getMessage()]));
|
||||
die("Erreur de connexion BDD (Host: $host, DB: $db) : " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
@@ -775,6 +775,27 @@ return [
|
||||
'bud_sav_selection' => 'Selecció:',
|
||||
'bud_sav_ph_name' => 'Nom del compte',
|
||||
|
||||
|
||||
// --- Module Budget : Provisions & Optimisation ---
|
||||
'budget_provisions_title' => 'Provisions & Optimisation de Trésorerie',
|
||||
'btn_optimize_cashflow' => 'Optimiser mes Économies',
|
||||
'add_new_provision' => 'Anticiper une grosse dépense à venir',
|
||||
'provision_label' => 'Libellé de la dépense',
|
||||
'provision_placeholder_wood' => 'Ex: Commande de bois, Révision de l\'auto...',
|
||||
'amount' => 'Montant',
|
||||
'expected_date' => 'Date prévue',
|
||||
'btn_add' => 'Ajouter',
|
||||
'upcoming_provisions_list' => 'Dépenses prévues au calendrier',
|
||||
'optimization_assistant_title' => 'Assistant d\'Optimisation des Quinzaines',
|
||||
'error_generic' => 'Une erreur est survenue',
|
||||
'success_add_provision' => 'Dépense prévisionnelle ajoutée !',
|
||||
'no_provisions' => 'Aucune dépense prévisionnelle enregistrée pour le moment.',
|
||||
'btn_delete' => 'Supprimer',
|
||||
'savings_this_month' => 'Économies globales à placer ce mois-ci',
|
||||
'calculate' => 'Calculer la stratégie optimale',
|
||||
'date_past_warning' => 'La date sélectionnée est déjà passée !',
|
||||
'budget_tab_provisions' => 'Provisions',
|
||||
|
||||
// JS & Errors
|
||||
'bud_sav_confirm_delete_month' => 'Estàs segur que vols suprimir totes les dades de %m per a %o?',
|
||||
'bud_sav_prompt_duplicate' => "Vols duplicar les dades de %s cap a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:",
|
||||
|
||||
@@ -824,6 +824,26 @@ return [
|
||||
'bud_prev_target_ph' => 'E.g., 150',
|
||||
'bud_prev_transfer_to' => 'To',
|
||||
|
||||
// --- Module Budget : Provisions & Optimisation ---
|
||||
'budget_provisions_title' => 'Provisions & Cashflow Optimization',
|
||||
'btn_optimize_cashflow' => 'Optimize My Savings',
|
||||
'add_new_provision' => 'Anticipate an upcoming major expense',
|
||||
'provision_label' => 'Expense description',
|
||||
'provision_placeholder_wood' => 'E.g., Firewood order, Car service...',
|
||||
'amount' => 'Amount',
|
||||
'expected_date' => 'Expected date',
|
||||
'btn_add' => 'Add',
|
||||
'upcoming_provisions_list' => 'Scheduled expenses',
|
||||
'optimization_assistant_title' => 'Fortnightly Interest Optimization Assistant',
|
||||
'error_generic' => 'An error occurred',
|
||||
'success_add_provision' => 'Forecasted expense added successfully!',
|
||||
'no_provisions' => 'No forecasted expenses registered at the moment.',
|
||||
'btn_delete' => 'Delete',
|
||||
'savings_this_month' => 'Total savings to allocate this month',
|
||||
'calculate' => 'Calculate optimal strategy',
|
||||
'date_past_warning' => 'The selected date has already passed!',
|
||||
'budget_tab_provisions' => 'Forecasts',
|
||||
|
||||
// ==========================================
|
||||
// MODULE: GIFTS (gift-list)
|
||||
// ==========================================
|
||||
|
||||
@@ -773,6 +773,34 @@ return [
|
||||
'bud_sav_ph_name' => 'Nom du compte',
|
||||
|
||||
|
||||
// --- BUDGET : PREVISION ---
|
||||
|
||||
'budget_provisions_title' => 'Provisions & Optimisation de Trésorerie',
|
||||
'btn_optimize_cashflow' => 'Optimiser mes Économies',
|
||||
'add_new_provision' => 'Anticiper une grosse dépense à venir',
|
||||
'provision_label' => 'Libellé de la dépense',
|
||||
'provision_placeholder_wood' => 'Ex: Commande bois de chauffage, Révision voiture...',
|
||||
'amount' => 'Montant',
|
||||
'expected_date' => 'Date prévue',
|
||||
'btn_add' => 'Ajouter',
|
||||
'upcoming_provisions_list' => 'Dépenses prévues au calendrier',
|
||||
'optimization_assistant_title' => 'Assistant d\'Optimisation des Quinzaines',
|
||||
'error_generic' => 'Une erreur est survenue',
|
||||
'success_add_provision' => 'Dépense prévisionnelle ajoutée !',
|
||||
'no_provisions' => 'Aucune dépense prévisionnelle enregistrée pour le moment.',
|
||||
'btn_delete' => 'Supprimer',
|
||||
'savings_this_month' => 'Économies globales à placer ce mois-ci',
|
||||
'calculate' => 'Calculer la stratégie optimale',
|
||||
'date_past_warning' => 'La date sélectionnée est déjà passée !',
|
||||
'budget_tab_provisions' => 'Provisions',
|
||||
'budget_opti_base' => 'Base',
|
||||
'budget_opti_debt' => 'Dette compensée',
|
||||
'budget_opti_no_config' => 'Aucune configuration de salaire trouvée pour cette année.',
|
||||
'budget_opti_recap_apports' => 'Résumé des apports (Clearing) :',
|
||||
'budget_opti_total_disp' => 'Total disponible pour l\'optimisation :',
|
||||
'budget_opti_must_pay' => 'doit verser',
|
||||
|
||||
|
||||
// JS & Erreurs
|
||||
'bud_sav_confirm_delete_month' => 'Voulez-vous vraiment supprimer toutes les données de %m pour %o ?',
|
||||
'bud_sav_prompt_duplicate' => "Voulez-vous dupliquer les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
|
||||
|
||||
+20
-4
@@ -1,9 +1,23 @@
|
||||
<?php
|
||||
// includes/meta_db.php
|
||||
// Connexion à la base meta (gestion des familles et utilisateurs)
|
||||
$meta_host = getenv('DB_HOST') ?: 'househub-db';
|
||||
$meta_db = 'househub_meta';
|
||||
$meta_user = getenv('DB_USER') ?: 'househub';
|
||||
$meta_pass = getenv('DB_PASS') ?: 'changeme';
|
||||
|
||||
$envHost = getenv('DB_HOST');
|
||||
|
||||
// Détection stricte : Docker injecte toujours 'househub-db' via le docker-compose
|
||||
if ($envHost === 'househub-db') {
|
||||
// Environnement Docker (Ton collègue ou la Prod)
|
||||
$meta_host = 'househub-db';
|
||||
$meta_user = getenv('DB_USER') ?: 'househub';
|
||||
$meta_pass = getenv('DB_PASS') ?: 'changeme';
|
||||
} else {
|
||||
// Environnement Local XAMPP (Toi)
|
||||
$meta_host = '127.0.0.1'; // Plus stable que 'localhost' sous XAMPP
|
||||
$meta_user = 'root';
|
||||
$meta_pass = '';
|
||||
}
|
||||
|
||||
$meta_db = 'househub_meta';
|
||||
|
||||
try {
|
||||
$meta_pdo = new PDO(
|
||||
@@ -14,8 +28,10 @@ try {
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
||||
]
|
||||
);
|
||||
} catch (\PDOException $e) {
|
||||
die(json_encode(['error' => 'Meta DB unavailable: ' . $e->getMessage()]));
|
||||
}
|
||||
?>
|
||||
+28
-29
@@ -1,49 +1,48 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/meta_db.php';
|
||||
|
||||
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||
// Récupération des identifiants de BDD depuis l'environnement (fallback par défaut)
|
||||
$db_host = getenv('DB_HOST') ?: 'househub-db';
|
||||
$db_user = getenv('DB_USER') ?: 'househub';
|
||||
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
|
||||
$db_pass = getenv('DB_PASS') ?: 'changeme';
|
||||
|
||||
echo "<h1>🗺️ Migration : Refonte Modèle Voyages</h1><ul>";
|
||||
echo "<h1>🛠️ HouseHub - Correction AUTO_INCREMENT Voyages</h1>";
|
||||
|
||||
try {
|
||||
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
|
||||
// On récupère toutes les familles actives
|
||||
$stmt = $meta_pdo->query("SELECT db_name, name FROM families");
|
||||
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($families as $family) {
|
||||
$dbName = $family['db_name'];
|
||||
foreach ($families as $f) {
|
||||
$dbName = $f['db_name'];
|
||||
echo "<h3>Famille : {$f['name']} ($dbName)</h3><ul>";
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
$pdo = new PDO("mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", $db_user, $db_pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||
]);
|
||||
|
||||
// 1. Mise à jour pf_holidays
|
||||
$pdo->exec("ALTER TABLE pf_holidays ADD COLUMN return_step_id INT DEFAULT NULL");
|
||||
// 1. On vérifie si la clé primaire existe déjà pour éviter de faire planter le script
|
||||
$hasPrimaryKey = $pdo->query("SHOW KEYS FROM pf_holidays WHERE Key_name = 'PRIMARY'")->fetch();
|
||||
|
||||
// 2. Mise à jour pf_holidays_items
|
||||
$pdo->exec("ALTER TABLE pf_holidays_items ADD COLUMN step_type VARCHAR(20) DEFAULT 'stop'");
|
||||
$pdo->exec("ALTER TABLE pf_holidays_items ADD COLUMN expense_context VARCHAR(20) DEFAULT NULL");
|
||||
if (!$hasPrimaryKey) {
|
||||
$pdo->exec("ALTER TABLE pf_holidays ADD PRIMARY KEY (id)");
|
||||
echo "<li>✅ Clé primaire restaurée sur la colonne `id`.</li>";
|
||||
}
|
||||
|
||||
// 3. Migration des données (is_return -> return_step_id)
|
||||
// On cherche la première étape cochée "is_return" et on l'assigne au voyage
|
||||
$pdo->exec("
|
||||
UPDATE pf_holidays h
|
||||
SET return_step_id = (
|
||||
SELECT id FROM pf_holidays_items i
|
||||
WHERE i.holiday_id = h.id AND i.is_return = 1 AND i.location_name IS NOT NULL
|
||||
LIMIT 1
|
||||
)
|
||||
");
|
||||
// 2. On restaure la propriété AUTO_INCREMENT
|
||||
$pdo->exec("ALTER TABLE pf_holidays MODIFY id INT(11) NOT NULL AUTO_INCREMENT");
|
||||
echo "<li>✅ Propriété AUTO_INCREMENT restaurée.</li>";
|
||||
|
||||
// 4. Nettoyage de l'ancienne colonne
|
||||
$pdo->exec("ALTER TABLE pf_holidays_items DROP COLUMN is_return");
|
||||
|
||||
echo "<li>✅ {$family['name']} : Schéma Voyages mis à jour !</li>";
|
||||
} catch (\PDOException $e) {
|
||||
echo "<li>⚠️ {$family['name']} : " . $e->getMessage() . "</li>";
|
||||
// On attrape l'erreur si la modif a déjà été faite
|
||||
echo "<li>ℹ️ Action ignorée ou table déjà à jour : " . $e->getMessage() . "</li>";
|
||||
}
|
||||
echo "</ul>";
|
||||
}
|
||||
echo "</ul><h2>🎉 Migration terminée !</h2>";
|
||||
echo "<h2>🚀 Correction terminée ! Tu peux réessayer de créer un voyage.</h2>";
|
||||
|
||||
} catch (Exception $e) {
|
||||
die("Erreur fatale : " . $e->getMessage());
|
||||
die("Erreur fatale de connexion à la Meta DB : " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
@@ -1798,3 +1798,121 @@ body.sum-mode-active .sum-target {
|
||||
padding: 12px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
14. PROVISIONS & OPTIMISATION (CASHFLOW)
|
||||
========================================================================== */
|
||||
|
||||
/* Formulaire d'ajout */
|
||||
.provisions-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr auto;
|
||||
gap: 15px;
|
||||
align-items: end;
|
||||
}
|
||||
.provisions-form-grid .pf-form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.provisions-form-grid .pf-btn {
|
||||
height: 42px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Tableau des provisions */
|
||||
.budget-table-card {
|
||||
background: var(--bg-panel);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-light);
|
||||
overflow: hidden;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.provisions-table th {
|
||||
text-align: center;
|
||||
}
|
||||
.provisions-table th:first-child {
|
||||
text-align: left;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.provisions-table th:last-child {
|
||||
text-align: right;
|
||||
padding-right: 15px;
|
||||
}
|
||||
.provisions-table td {
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.prov-title-cell {
|
||||
font-weight: 500;
|
||||
color: var(--text-main);
|
||||
padding-left: 15px;
|
||||
}
|
||||
.prov-amount-cell {
|
||||
color: #2563eb;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.prov-date-cell {
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Modale d'optimisation */
|
||||
.provisions-modal-content {
|
||||
max-width: 600px;
|
||||
width: 95%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.provision-person-card {
|
||||
background: var(--bg-subtle);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-light);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.provision-person-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.provision-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.input-amount-highlight {
|
||||
font-weight: bold;
|
||||
color: #2563eb !important;
|
||||
}
|
||||
.input-amount-success {
|
||||
font-weight: bold;
|
||||
color: var(--success) !important;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
.currency-symbol {
|
||||
font-weight: bold;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.optimization-hint {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.pf-divider {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border-light);
|
||||
margin: 20px 0;
|
||||
}
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* --- RESPONSIVE --- */
|
||||
@media (max-width: 768px) {
|
||||
.provisions-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
// On charge l'authentification et l'init PDO de l'espace familial courant automatiquement via tes includes globaux
|
||||
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||
require_once __DIR__ . '/../../../../includes/db.php';
|
||||
require_once __DIR__ . '/../../../../includes/i18n.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$amount = floatval($_POST['amount'] ?? 0);
|
||||
$expected_date = $_POST['expected_date'] ?? '';
|
||||
|
||||
if (empty($title) || $amount <= 0 || empty($expected_date)) {
|
||||
echo json_encode(['success' => false, 'message' => tr('error_generic')]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Grâce au multi-tenant par BDD, $pdo pointe déjà sur la base de la famille connectée
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_expected_expenses (title, amount, expected_date) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$title, $amount, $expected_date]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => tr('success_add_provision')]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||
require_once __DIR__ . '/../../../../includes/db.php';
|
||||
|
||||
$id = intval($_POST['id'] ?? 0);
|
||||
|
||||
if (!$id) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid ID']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_expected_expenses WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||
require_once __DIR__ . '/../../../../includes/db.php';
|
||||
require_once __DIR__ . '/../../../../includes/i18n.php';
|
||||
|
||||
try {
|
||||
// Récupérer les provisions non payées, classées par date
|
||||
$stmt = $pdo->query("SELECT id, title, amount, expected_date FROM pf_expected_expenses WHERE is_paid = 0 ORDER BY expected_date ASC");
|
||||
$provisions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($provisions)) {
|
||||
$html = '<p class="pf-text-muted">' . tr('no_provisions') . '</p>';
|
||||
} else {
|
||||
$html = '<table class="pf-table" style="width:100%; border-collapse: collapse;">';
|
||||
$html .= '<thead style="border-bottom: 2px solid var(--border-color); text-align: left;">';
|
||||
$html .= '<tr><th>' . tr('provision_label') . '</th><th>' . tr('amount') . '</th><th>' . tr('expected_date') . '</th><th style="text-align:right;">Actions</th></tr>';
|
||||
$html .= '</thead><tbody>';
|
||||
|
||||
foreach ($provisions as $p) {
|
||||
$dateFormated = date('d/m/Y', strtotime($p['expected_date']));
|
||||
$html .= '<tr style="border-bottom: 1px solid var(--border-color); height: 45px;">';
|
||||
$html .= '<td>' . htmlspecialchars($p['title']) . '</td>';
|
||||
$html .= '<td>' . number_format($p['amount'], 2, ',', ' ') . ' €</td>';
|
||||
$html .= '<td>' . $dateFormated . '</td>';
|
||||
$html .= '<td style="text-align:right;"><button class="pf-btn pf-btn-danger btn-delete-provision" data-id="' . $p['id'] . '">' . tr('btn_delete') . '</button></td>';
|
||||
$html .= '</tr>';
|
||||
}
|
||||
$html .= '</tbody></table>';
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'html' => $html]);
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../../../../includes/auth.php';
|
||||
require_once __DIR__ . '/../../../../includes/db.php';
|
||||
require_once __DIR__ . '/../../../../includes/i18n.php';
|
||||
|
||||
$savings_inputs = $_POST['savings'] ?? [];
|
||||
$current_year = date('Y');
|
||||
$current_month = date('m');
|
||||
|
||||
try {
|
||||
// 1. Récupération des dépenses prévues
|
||||
$stmtExpenses = $pdo->prepare("
|
||||
SELECT title, amount, expected_date
|
||||
FROM pf_expected_expenses
|
||||
WHERE is_paid = 0
|
||||
AND YEAR(expected_date) = ?
|
||||
AND MONTH(expected_date) = ?
|
||||
ORDER BY expected_date ASC
|
||||
");
|
||||
$stmtExpenses->execute([$current_year, $current_month]);
|
||||
$expenses = $stmtExpenses->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$q1_expenses_total = 0;
|
||||
$q2_expenses_total = 0;
|
||||
|
||||
$detail_html = '<ul style="padding-left: 1.2rem; margin-bottom: 1rem; color: var(--text-muted); font-size:0.9rem;">';
|
||||
foreach ($expenses as $e) {
|
||||
$day = intval(date('d', strtotime($e['expected_date'])));
|
||||
if ($day <= 15) {
|
||||
$q1_expenses_total += $e['amount'];
|
||||
} else {
|
||||
$q2_expenses_total += $e['amount'];
|
||||
}
|
||||
$detail_html .= '<li>' . htmlspecialchars($e['title']) . ' (' . number_format($e['amount'], 2, ',', ' ') . ' € le ' . date('d/m', strtotime($e['expected_date'])) . ')</li>';
|
||||
}
|
||||
$detail_html .= '</ul>';
|
||||
|
||||
$total_expenses = $q1_expenses_total + $q2_expenses_total;
|
||||
|
||||
// 2. Récupération des dettes
|
||||
$stmtDebts = $pdo->query("SELECT payer, SUM(amount) as total_debt FROM pf_advances WHERE is_resolved = 0 GROUP BY payer");
|
||||
$debts = $stmtDebts->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
// 3. NOUVELLE LOGIQUE : Clearing intelligent (Compensation partielle ou totale)
|
||||
$total_base_inputs = array_sum(array_map('floatval', $savings_inputs));
|
||||
$surplus_for_clearing = $total_base_inputs - $total_expenses;
|
||||
$monthly_savings = 0; // Le vrai total qui sera viré après déduction des dettes
|
||||
|
||||
$contributions_html = '<ul style="padding-left: 1.2rem; margin-bottom: 0.5rem; color: var(--text-main); font-size:0.9rem; line-height: 1.6;">';
|
||||
|
||||
foreach ($savings_inputs as $person => $amount) {
|
||||
$base_amount = floatval($amount);
|
||||
$debt = $debts[$person] ?? 0;
|
||||
|
||||
// Formatage bancaire propre pour les textes
|
||||
$base_fmt = number_format($base_amount, 2, ',', ' ');
|
||||
|
||||
if ($debt > 0) {
|
||||
if ($surplus_for_clearing > 0) {
|
||||
$compensation = min($debt, $base_amount, $surplus_for_clearing);
|
||||
$net_amount = $base_amount - $compensation;
|
||||
$surplus_for_clearing -= $compensation;
|
||||
$monthly_savings += $net_amount;
|
||||
|
||||
$comp_fmt = number_format($compensation, 2, ',', ' ');
|
||||
$net_fmt = number_format($net_amount, 2, ',', ' ');
|
||||
|
||||
$reste_dette = $debt - $compensation;
|
||||
$txt_reste = ($reste_dette > 0)
|
||||
? ' <span style="color:var(--danger); font-size:0.8rem; margin-left: 5px;">(Reste ' . number_format($reste_dette, 2, ',', ' ') . ' € de dette)</span>'
|
||||
: ' <span style="color:var(--success); font-weight:bold; font-size:0.85rem; margin-left: 5px;">(Dette soldée 🎉)</span>';
|
||||
|
||||
// Harmonisation en bleu (#2563eb) pour le montant net à verser
|
||||
$contributions_html .= '<li style="margin-bottom: 5px;"><strong>' . htmlspecialchars($person) . '</strong> : Base ' . $base_fmt . ' € − Remboursé ' . $comp_fmt . ' € = <strong style="color:#2563eb;">' . $net_fmt . ' € à verser</strong>' . $txt_reste . '</li>';
|
||||
} else {
|
||||
$monthly_savings += $base_amount;
|
||||
$contributions_html .= '<li style="margin-bottom: 5px;"><strong>' . htmlspecialchars($person) . '</strong> : <strong style="color:#2563eb;">' . $base_fmt . ' € à verser</strong> <span style="color:var(--danger); font-size:0.8rem; margin-left: 5px;">(Dette gelée, liquidités insuffisantes)</span></li>';
|
||||
}
|
||||
} else {
|
||||
$monthly_savings += $base_amount;
|
||||
$contributions_html .= '<li style="margin-bottom: 5px;"><strong>' . htmlspecialchars($person) . '</strong> : <strong style="color:#2563eb;">' . $base_fmt . ' € à verser</strong></li>';
|
||||
}
|
||||
}
|
||||
$contributions_html .= '</ul>';
|
||||
$contributions_html .= '<p style="color: var(--text-main); font-weight: bold; margin-top:5px; font-size:0.95rem;">Total net atterrissant sur les comptes : ' . number_format($monthly_savings, 2, ',', ' ') . ' €</p>';
|
||||
|
||||
if(empty($expenses)) {
|
||||
$detail_html = '<p style="color: var(--success); font-weight:bold; margin-bottom:1rem; font-size:0.9rem;">🎉 Aucune grosse dépense enregistrée sur ce mois.</p>';
|
||||
}
|
||||
|
||||
// --- MOTEUR DE RÈGLES DES QUINZAINES (Remplacement des ** par <strong>) ---
|
||||
$instructions = [];
|
||||
$remaining_to_allocate = $monthly_savings;
|
||||
|
||||
// Règle 1 : Première quinzaine
|
||||
if ($q1_expenses_total > 0) {
|
||||
if ($remaining_to_allocate >= $q1_expenses_total) {
|
||||
$remaining_to_allocate -= $q1_expenses_total;
|
||||
$instructions[] = "💼 <strong>Dès le 1er</strong> : Laissez <strong>" . number_format($q1_expenses_total, 2, ',', ' ') . " €</strong> sur le compte commun pour honorer les dépenses de la 1ère quinzaine.";
|
||||
} else {
|
||||
$deficit = $q1_expenses_total - $remaining_to_allocate;
|
||||
$remaining_to_allocate = 0;
|
||||
$instructions[] = "💼 <strong>Dès le 1er</strong> : Gardez l'intégralité des apports sur le compte commun.";
|
||||
$instructions[] = "⚠️ <strong>Retrait requis</strong> : Retirez <strong>" . number_format($deficit, 2, ',', ' ') . " €</strong> depuis le Livret A vers le commun pour couvrir la 1ère quinzaine.";
|
||||
}
|
||||
}
|
||||
|
||||
// Règle 2 : Deuxième quinzaine
|
||||
if ($q2_expenses_total > 0) {
|
||||
if ($remaining_to_allocate >= $q2_expenses_total) {
|
||||
$remaining_to_allocate -= $q2_expenses_total;
|
||||
$instructions[] = "📈 <strong>Dès le 1er</strong> : Placez <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> sur le Livret A pour générer des intérêts sur la 1ère quinzaine.";
|
||||
$instructions[] = "🔄 <strong>Le 16 du mois</strong> : Transférez ces <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> sur le compte commun pour payer la dépense.";
|
||||
} else {
|
||||
if ($remaining_to_allocate > 0) {
|
||||
$instructions[] = "📈 <strong>Dès le 1er</strong> : Placez le reste des apports (<strong>" . number_format($remaining_to_allocate, 2, ',', ' ') . " €</strong>) sur le Livret A.";
|
||||
$instructions[] = "🔄 <strong>Le 16 du mois</strong> : Retirez <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> global du Livret A pour payer la fin de mois.";
|
||||
$remaining_to_allocate = 0;
|
||||
} else {
|
||||
$instructions[] = "🔄 <strong>Le 16 du mois</strong> : Retirez <strong>" . number_format($q2_expenses_total, 2, ',', ' ') . " €</strong> du Livret A vers le commun. <em>Ne le faites pas avant le 16 !</em>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Solde résiduel long terme
|
||||
if ($remaining_to_allocate > 0) {
|
||||
$instructions[] = "💰 <strong>Épargne stable</strong> : Placez les <strong>" . number_format($remaining_to_allocate, 2, ',', ' ') . " €</strong> restants sur votre Livret A dès le 1er.";
|
||||
}
|
||||
|
||||
// --- CONSTITUTION DU RENDU HTML FINAL ---
|
||||
$html = '<div style="border-top: 1px dashed var(--border-color); padding-top:0.8rem;">';
|
||||
|
||||
// Titre de l'état du Clearing simplifié et clair
|
||||
$html .= '<h4 style="margin:0 0 0.4rem 0; font-size:0.95rem;">🤝 Résumé des apports et remboursements internes :</h4>';
|
||||
$html .= '<div style="background: var(--bg-main); padding: 8px; border-radius: 6px; margin-bottom: 1rem; border: 1px solid var(--border-color);">';
|
||||
$html .= $contributions_html;
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<h4 style="margin:0 0 0.4rem 0; font-size:0.95rem;">Analyse des dépenses du mois :</h4>';
|
||||
$html .= $detail_html;
|
||||
|
||||
$html .= '<div style="background: var(--bg-main); padding: 8px; border-radius: 6px; border-left: 4px solid var(--accent-color);">';
|
||||
$html .= '<h4 style="margin:0 0 0.5rem 0; color: var(--accent-color); font-size:0.95rem;">📋 Plan d\'action recommandé :</h4>';
|
||||
|
||||
foreach ($instructions as $ins) {
|
||||
$html .= '<p style="margin:0 0 0.4rem 0; font-size: 0.9rem; line-height: 1.4;">' . $ins . '</p>';
|
||||
}
|
||||
|
||||
$html .= '</div></div>';
|
||||
|
||||
echo json_encode(['success' => true, 'html' => $html]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
echo json_encode(['success' => false, 'message' => 'Calcul impossible : ' . $e->getMessage()]);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
// modules/budget/views/budget_prev.php
|
||||
|
||||
// 1. Chargement dynamique des ADULTES de la famille
|
||||
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color FROM pf_people WHERE role NOT IN ('enfant', 'nounou') AND is_active = 1 ORDER BY id ASC");
|
||||
// 1. Chargement dynamique des ADULTES de la famille (Exclusion stricte des rôles secondaires)
|
||||
$stmtPeople = $pdo->query("SELECT id, name, user_id, role, color FROM pf_people WHERE role IN ('parent') AND is_active = 1 ORDER BY id ASC");
|
||||
$budgetParents = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Sécurité : au cas où aucun adulte n'est trouvé, on évite un crash
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
// modules/budget/views/provisions.php
|
||||
|
||||
// --- RÉCUPÉRATION DES DÉPENSES PRÉVUES (MULTI-TENANT PDO) ---
|
||||
$stmt = $pdo->query("SELECT id, title, amount, expected_date FROM pf_expected_expenses WHERE is_paid = 0 ORDER BY expected_date ASC");
|
||||
$provisions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// --- RÉCUPÉRATION DES APPORTS ET DETTES (CLEARING) ---
|
||||
$currentYear = date('Y');
|
||||
|
||||
// 1. Apports de base (Eco Family) des parents configurés pour l'année
|
||||
$stmtConfig = $pdo->prepare("SELECT person, eco_family FROM pf_salary_config WHERE year = ?");
|
||||
$stmtConfig->execute([$currentYear]);
|
||||
$configs = $stmtConfig->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
// 2. Dettes non résolues par personne (issues des Avances & Tricount)
|
||||
$stmtDebts = $pdo->query("SELECT payer, SUM(amount) as total_debt FROM pf_advances WHERE is_resolved = 0 GROUP BY payer");
|
||||
$debts = $stmtDebts->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
function formatDate($dateString) {
|
||||
if (!$dateString) return '';
|
||||
return date('d/m/Y', strtotime($dateString));
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="budget-view">
|
||||
<div class="view-header">
|
||||
<h2><?= tr('budget_provisions_title') ?></h2>
|
||||
|
||||
<button class="pf-btn pf-btn-primary" onclick="openOptimizeModal()">
|
||||
✨ <?= tr('btn_optimize_cashflow') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pf-card">
|
||||
<h3 class="pf-card-h2"><?= tr('add_new_provision') ?></h3>
|
||||
|
||||
<form id="form-add-provision" data-action="/modules/budget/includes/api/add-provision.php">
|
||||
<div class="provisions-form-grid">
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label" for="prov-title"><?= tr('provision_label') ?></label>
|
||||
<input type="text" id="prov-title" name="title" class="pf-input" placeholder="<?= tr('provision_placeholder_wood') ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label" for="prov-amount"><?= tr('amount') ?> (€)</label>
|
||||
<input type="number" id="prov-amount" name="amount" class="pf-input no-spinners input-amount-highlight" step="0.01" min="0.01" required>
|
||||
</div>
|
||||
|
||||
<div class="pf-form-group">
|
||||
<label class="pf-label" for="prov-date"><?= tr('expected_date') ?></label>
|
||||
<input type="date" id="prov-date" name="expected_date" class="pf-input" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="pf-btn pf-btn-secondary">
|
||||
<?= tr('btn_add') ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="budget-table-card table-responsive">
|
||||
<?php if (empty($provisions)): ?>
|
||||
<div class="optimization-hint" style="padding: 30px;">
|
||||
<p><?= tr('no_provisions') ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<table class="pf-table savings-table provisions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= tr('provision_label') ?></th>
|
||||
<th style="width: 150px;"><?= tr('amount') ?></th>
|
||||
<th style="width: 150px;"><?= tr('expected_date') ?></th>
|
||||
<th style="width: 100px;"><?= tr('actions') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($provisions as $p): ?>
|
||||
<tr>
|
||||
<td class="prov-title-cell">
|
||||
<?= htmlspecialchars($p['title']) ?>
|
||||
</td>
|
||||
<td class="prov-amount-cell">
|
||||
<?= number_format($p['amount'], 2, ',', ' ') ?> €
|
||||
</td>
|
||||
<td class="prov-date-cell">
|
||||
<?= formatDate($p['expected_date']) ?>
|
||||
</td>
|
||||
<td class="text-right" style="padding-right: 15px;">
|
||||
<button class="btn-icon-action delete btn-safe-click" title="<?= tr('btn_delete') ?>" onclick="deleteProvision(<?= $p['id'] ?>)">
|
||||
🗑️
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="optimizeModal" class="pf-modal">
|
||||
<div class="pf-modal-content provisions-modal-content">
|
||||
|
||||
<div class="pf-modal-header">
|
||||
<h3 class="pf-modal-title">🧠 <?= tr('optimization_assistant_title') ?></h3>
|
||||
<button type="button" class="pf-modal-close" onclick="closeOptimizeModal()">×</button>
|
||||
</div>
|
||||
|
||||
<form id="form-optimize" data-action="/modules/budget/includes/api/optimize-cashflow.php">
|
||||
<div id="dynamic-savings-inputs">
|
||||
<?php if (empty($configs)): ?>
|
||||
<p class="text-danger font-bold pf-muted-tiny"><?= tr('budget_opti_no_config') ?></p>
|
||||
<input type="number" step="0.01" min="0" name="savings[Global]" class="pf-input no-spinners" required>
|
||||
<?php else: ?>
|
||||
<?php foreach ($configs as $person => $ecoBase):
|
||||
$debt = $debts[$person] ?? 0;
|
||||
?>
|
||||
<div class="pf-form-group provision-person-card">
|
||||
<label class="pf-label provision-person-header">
|
||||
<strong class="text-main"><?= htmlspecialchars($person) ?></strong>
|
||||
<span class="pf-muted-note">
|
||||
<?php if ($debt > 0): ?>
|
||||
<span class="text-danger font-bold">(Dette en attente : <?= (float)$debt ?> €)</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</label>
|
||||
<div class="provision-input-group">
|
||||
<input type="number" step="0.01" min="0" name="savings[<?= htmlspecialchars($person) ?>]" class="pf-input no-spinners input-amount-success" value="<?= (float)$ecoBase ?>" required>
|
||||
<span class="currency-symbol">€</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="pf-btn pf-btn-primary btn-block">
|
||||
<?= tr('calculate') ?>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr class="pf-divider">
|
||||
|
||||
<div id="optimization-results">
|
||||
<p class="optimization-hint">
|
||||
Vérifiez vos apports théoriques ci-dessus pour calculer la stratégie de répartition (Clearing automatique si aucune dépense).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||
window.I18N = {
|
||||
...(window.I18N || {}),
|
||||
'btn_delete': <?= json_encode(tr('btn_delete')) ?>,
|
||||
'error_generic': <?= json_encode(tr('error_generic')) ?>,
|
||||
'bud_err_tech': <?= json_encode(tr('bud_err_tech')) ?>,
|
||||
'bud_err_server': <?= json_encode(tr('bud_err_server')) ?>
|
||||
};
|
||||
|
||||
// --- 2. GESTION DU FORMULAIRE D'AJOUT ---
|
||||
const formAdd = document.getElementById('form-add-provision');
|
||||
if (formAdd) {
|
||||
formAdd.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerText;
|
||||
submitBtn.innerText = '⏳';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const result = await pachaFetch(this.getAttribute('data-action'), {
|
||||
method: 'POST',
|
||||
body: new FormData(this)
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(result.message || window.I18N['error_generic']);
|
||||
submitBtn.innerText = originalText;
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
alert(window.I18N['bud_err_tech']);
|
||||
submitBtn.innerText = originalText;
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. GESTION DE LA SUPPRESSION ---
|
||||
async function deleteProvision(id) {
|
||||
if (!confirm(window.I18N['btn_delete'] + ' ?')) return;
|
||||
try {
|
||||
const result = await pachaFetch('/modules/budget/includes/api/delete-provision.php', {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({ id: id })
|
||||
});
|
||||
if (result.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(result.message);
|
||||
}
|
||||
} catch(err) {
|
||||
alert(window.I18N['bud_err_tech']);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. CONTROL DE LA MODALE D'OPTIMISATION ---
|
||||
function openOptimizeModal() {
|
||||
document.getElementById('optimization-results').innerHTML = `
|
||||
<p class="optimization-hint">
|
||||
Vérifiez vos apports théoriques ci-dessus pour calculer la stratégie de répartition (Clearing automatique si aucune dépense).
|
||||
</p>`;
|
||||
document.getElementById('optimizeModal').classList.add('open');
|
||||
document.body.classList.add('no-scroll');
|
||||
}
|
||||
|
||||
function closeOptimizeModal() {
|
||||
document.getElementById('optimizeModal').classList.remove('open');
|
||||
document.body.classList.remove('no-scroll');
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('optimizeModal');
|
||||
if (event.target == modal) {
|
||||
closeOptimizeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 5. EXECUTION ET SUBMIT DE L'ALGORITHME (formOptimize) ---
|
||||
const formOptimize = document.getElementById('form-optimize');
|
||||
if (formOptimize) {
|
||||
formOptimize.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const resultsContainer = document.getElementById('optimization-results');
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
resultsContainer.innerHTML = '<div class="optimization-hint">⏳ Analyse des quinzaines bancaires...</div>';
|
||||
|
||||
try {
|
||||
const result = await pachaFetch(this.getAttribute('data-action'), {
|
||||
method: 'POST',
|
||||
body: new FormData(this)
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
resultsContainer.innerHTML = result.html;
|
||||
} else {
|
||||
resultsContainer.innerHTML = `<p class="text-danger font-bold text-center">${result.message}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur d'optimisation :", error);
|
||||
resultsContainer.innerHTML = `<p class="text-danger font-bold text-center">${window.I18N['bud_err_tech']}</p>`;
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1289,46 +1289,78 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
|
||||
generateMonthSummaryHTML(year, month) {
|
||||
const stats = { off: 0, extra: 0, sick: 0, presence: 0 };
|
||||
// 1. Identifier UNIQUEMENT les enfants qui ont le mode de garde "Nounou"
|
||||
const nounouKids = (window.FAMILY_CONFIG.kids || []).filter(
|
||||
(k) => k.modes && k.modes.some((m) => m.toLowerCase() === "nounou"),
|
||||
);
|
||||
|
||||
// S'il n'y a pas d'enfant avec ce mode, on ne retourne rien pour ce bloc
|
||||
if (nounouKids.length === 0) return "";
|
||||
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
let html = "";
|
||||
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dateObj = new Date(year, month, d);
|
||||
const dayOfWeek = dateObj.getDay();
|
||||
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
|
||||
// 2. Boucler sur chaque enfant concerné pour un calcul individuel
|
||||
nounouKids.forEach((kid) => {
|
||||
let workingDays = 0;
|
||||
let off = 0;
|
||||
let extra = 0;
|
||||
let sick = 0;
|
||||
|
||||
const iso = this.getLocalIsoDate(dateObj);
|
||||
const dayEvents = this.events.filter((e) => e.date === iso);
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dateObj = new Date(year, month, d);
|
||||
const dayOfWeek = dateObj.getDay();
|
||||
|
||||
dayEvents.forEach((e) => {
|
||||
const dur = parseFloat(e.duration) || 1;
|
||||
if (e.type === "HELPER_OFF") stats.off += dur;
|
||||
if (e.type === "HELPER_EXTRA") stats.extra += dur;
|
||||
if (e.type === "CHILD_SICK") stats.sick += dur;
|
||||
});
|
||||
// Exclure les week-ends des jours ouvrés (0 = Dimanche, 6 = Samedi)
|
||||
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
|
||||
|
||||
const iso = this.getLocalIsoDate(dateObj);
|
||||
|
||||
// Exclure les jours fériés des jours ouvrés
|
||||
if (this.publicHolidayDates.has(iso)) continue;
|
||||
|
||||
// C'est un jour ouvré valide
|
||||
workingDays += 1;
|
||||
|
||||
const dayEvents = this.events.filter((e) => e.date === iso);
|
||||
|
||||
if (
|
||||
!this.publicHolidayDates.has(iso) &&
|
||||
!dayEvents.some((e) => e.type === "VACANCES_SCOLAIRES")
|
||||
) {
|
||||
let dayAbsence = 0;
|
||||
dayEvents.forEach((e) => {
|
||||
if (["HELPER_OFF", "HELPER_EXTRA", "CHILD_SICK"].includes(e.type)) {
|
||||
dayAbsence += parseFloat(e.duration) || 1;
|
||||
const dur = parseFloat(e.duration) || 1;
|
||||
|
||||
if (e.type === "HELPER_OFF") off += dur;
|
||||
if (e.type === "HELPER_EXTRA") extra += dur;
|
||||
|
||||
// On vérifie que la maladie concerne BIEN cet enfant précis !
|
||||
if (
|
||||
e.type === "CHILD_SICK" &&
|
||||
Number(e.person_id) === Number(kid.id)
|
||||
) {
|
||||
sick += dur;
|
||||
}
|
||||
});
|
||||
stats.presence += Math.max(0, 1 - dayAbsence);
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="fc-month-summary-inline" style="display:flex; justify-content:space-around; gap:10px; margin-top:8px; font-size:0.75rem; background: var(--bg-panel); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);">
|
||||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>Off</span> <strong style="color:var(--text-main); font-size:0.95rem;">${parseFloat(stats.off.toFixed(1))} j</strong></div>
|
||||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>Extra</span> <strong style="color:var(--text-main); font-size:0.95rem;">${parseFloat(stats.extra.toFixed(1))} j</strong></div>
|
||||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>Maladie</span> <strong style="color:var(--text-main); font-size:0.95rem;">${parseFloat(stats.sick.toFixed(1))} j</strong></div>
|
||||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>Présence</span> <strong style="color:var(--primary); font-size:0.95rem;">${parseFloat(stats.presence.toFixed(1))} j</strong></div>
|
||||
// Calcul final strict : Jours ouvrés - Maladie (de l'enfant) - Extra - Off
|
||||
const presence = Math.max(0, workingDays - sick - extra - off);
|
||||
|
||||
// Rendu HTML individuel
|
||||
html += `
|
||||
<div style="margin-top: 10px;">
|
||||
<div style="font-size: 0.8rem; font-weight: bold; color: var(--text-muted); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.05em;">
|
||||
👧👦 Présence ${kid.name} (Nounou)
|
||||
</div>
|
||||
<div class="fc-month-summary-inline" style="display:flex; justify-content:space-around; gap:10px; font-size:0.75rem; background: var(--bg-panel); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);">
|
||||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>Ouvrés</span> <strong style="color:var(--text-main); font-size:0.95rem;">${workingDays} j</strong></div>
|
||||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>Off</span> <strong style="color:var(--text-main); font-size:0.95rem;">${parseFloat(off.toFixed(1))} j</strong></div>
|
||||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>Extra</span> <strong style="color:var(--text-main); font-size:0.95rem;">${parseFloat(extra.toFixed(1))} j</strong></div>
|
||||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>Maladie</span> <strong style="color:var(--danger); font-size:0.95rem;">${parseFloat(sick.toFixed(1))} j</strong></div>
|
||||
<div class="fc-summ-pill" style="display:flex; flex-direction:column; align-items:center; flex:1;"><span>Présence</span> <strong style="color:var(--primary); font-size:0.95rem;">${parseFloat(presence.toFixed(1))} j</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
scrollToCurrentMonth() {
|
||||
|
||||
+1003
-1089
File diff suppressed because it is too large
Load Diff
+1256
-324
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,20 @@ require_login();
|
||||
// ---------------------------------------------------------------------------
|
||||
// INTERCEPTIONS AJAX (Exécution ultra rapide sans rechargement lourd)
|
||||
// ---------------------------------------------------------------------------
|
||||
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item'])) {
|
||||
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item', 'update_holiday_note'])) {
|
||||
header('Content-Type: application/json');
|
||||
session_write_close(); // 🚀 LIBÈRE LA SESSION : Permet d'autres requêtes simultanées sans bloquer le navigateur
|
||||
session_write_close();
|
||||
|
||||
try {
|
||||
if ($_POST['action'] === 'update_holiday_note') {
|
||||
$hId = (int)$_POST['holiday_id'];
|
||||
$notes = $_POST['notes'] ?? '';
|
||||
$stmt = $pdo->prepare("UPDATE pf_holidays SET notes = ? WHERE id = ?");
|
||||
$stmt->execute([$notes, $hId]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_POST['action'] === 'add_single_item') {
|
||||
$holiday_id = (int)$_POST['holiday_id'];
|
||||
$sort_order = (int)$_POST['sort_order'];
|
||||
@@ -25,16 +34,43 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime
|
||||
$date = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
||||
$time = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
||||
|
||||
// Récupération sécurisée du contexte
|
||||
$context = !empty($_POST['expense_context']) ? $_POST['expense_context'] : (!empty($_POST['context']) ? $_POST['context'] : 'local');
|
||||
$amount = (float)$_POST['amount'];
|
||||
$name = $_POST['name'] ?? 'Trajet';
|
||||
$category = $_POST['category'] ?? 'transport';
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 🛡️ ANTI-DOUBLON ABSOLU
|
||||
if ($context === 'transit') {
|
||||
$stmtCheck = $pdo->prepare("SELECT id FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? AND expense_context = 'transit'");
|
||||
$stmtCheck->execute([$holiday_id, $sort_order]);
|
||||
$existingId = $stmtCheck->fetchColumn();
|
||||
|
||||
if ($existingId) {
|
||||
// Mise à jour de la ligne existante
|
||||
$stmtUp = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ?, amount = ?, duration = ?, name = ? WHERE id = ?");
|
||||
$stmtUp->execute([$date, $time, $amount, $dur, $name, $existingId]);
|
||||
$pdo->commit();
|
||||
echo json_encode(['success' => true, 'id' => $existingId]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Insertion si aucun doublon n'existe
|
||||
$ins = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, step_start_date, step_end_date, step_type, expense_context, duration, item_date, item_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$ins->execute([
|
||||
$holiday_id, $_POST['category'], $_POST['name'], (float)$_POST['amount'], 0,
|
||||
$holiday_id, $category, $name, $amount, 0,
|
||||
$stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
|
||||
$sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
|
||||
$stepInfo['step_type'], $_POST['context'], $dur, $date, $time
|
||||
$stepInfo['step_type'], $context, $dur, $date, $time
|
||||
]);
|
||||
|
||||
$newId = $pdo->lastInsertId();
|
||||
$pdo->commit();
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
echo json_encode(['success' => true, 'id' => $newId]);
|
||||
exit;
|
||||
}
|
||||
echo json_encode(['success' => false, 'error' => 'Etape introuvable']);
|
||||
@@ -61,6 +97,7 @@ if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) { $pdo->rollBack(); }
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
@@ -92,10 +129,19 @@ if ($holiday_id > 0 && !empty($location_name)) {
|
||||
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ?")->execute([$holiday_id, $old_sort_order]);
|
||||
$target_order = $old_sort_order;
|
||||
} else {
|
||||
$stmtMax = $pdo->prepare("SELECT MAX(sort_order) FROM pf_holidays_items WHERE holiday_id = ?");
|
||||
$stmtMax->execute([$holiday_id]);
|
||||
$max = $stmtMax->fetchColumn();
|
||||
$target_order = ($max !== null) ? (int)$max + 1 : 0;
|
||||
// NOUVEAU : Logique d'intercalage d'étape
|
||||
$insert_after = $_POST['insert_after'] ?? 'end';
|
||||
|
||||
if ($insert_after === 'end') {
|
||||
$stmtMax = $pdo->prepare("SELECT MAX(sort_order) FROM pf_holidays_items WHERE holiday_id = ?");
|
||||
$stmtMax->execute([$holiday_id]);
|
||||
$max = $stmtMax->fetchColumn();
|
||||
$target_order = ($max !== null) ? (int)$max + 1 : 0;
|
||||
} else {
|
||||
$target_order = (int)$insert_after + 1;
|
||||
// On décale toutes les étapes suivantes vers le bas
|
||||
$pdo->prepare("UPDATE pf_holidays_items SET sort_order = sort_order + 1 WHERE holiday_id = ? AND sort_order >= ?")->execute([$holiday_id, $target_order]);
|
||||
}
|
||||
}
|
||||
|
||||
$step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null;
|
||||
@@ -124,7 +170,11 @@ if ($holiday_id > 0 && !empty($location_name)) {
|
||||
$dur = !empty($_POST['items']['duration'][$i]) ? (int)$_POST['items']['duration'][$i] : 1;
|
||||
$context = !empty($_POST['items']['context'][$i]) ? $_POST['items']['context'][$i] : 'local';
|
||||
|
||||
$stmt->execute([$holiday_id, $cat, $name, $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $step_type, $context]);
|
||||
// Récupération des coordonnées spécifiques à l'activité (ou fallback sur l'étape)
|
||||
$itemLat = !empty($_POST['items']['lat'][$i]) ? (float)$_POST['items']['lat'][$i] : $lat;
|
||||
$itemLng = !empty($_POST['items']['lng'][$i]) ? (float)$_POST['items']['lng'][$i] : $lng;
|
||||
|
||||
$stmt->execute([$holiday_id, $cat, $name, $amount, $paid, $location_name, $itemLat, $itemLng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $step_type, $context]);
|
||||
$validItemsCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,16 +28,51 @@ $vehicle_id = !empty($_POST['vehicle_id']) ? (int)$_POST['vehicle_id'] : null;
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. Détection d'un changement de nom pour refaire l'appel API
|
||||
$image_url = null;
|
||||
$old_title = null;
|
||||
if ($id) {
|
||||
// UPDATE (avec vehicle_id)
|
||||
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=?, vehicle_id=? WHERE id=?";
|
||||
$stmtOld = $pdo->prepare("SELECT title, image_url FROM pf_holidays WHERE id = ?");
|
||||
$stmtOld->execute([$id]);
|
||||
if ($row = $stmtOld->fetch()) {
|
||||
$old_title = $row['title'];
|
||||
$image_url = $row['image_url'];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Appel à Pixabay si nouveau voyage ou si le titre a changé
|
||||
if (!$id || strcasecmp($old_title ?? '', $title) !== 0) {
|
||||
$apiKey = '56931941-a86fbea4e14b88712cc1e9ed9';
|
||||
$query = urlencode($title);
|
||||
// On force des photos horizontales HD
|
||||
$url = "https://pixabay.com/api/?key={$apiKey}&q={$query}&image_type=photo&orientation=horizontal&min_width=1280&per_page=3";
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 3); // Timeout de 3s max pour ne pas bloquer l'UI
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response) {
|
||||
$data = json_decode($response, true);
|
||||
if (!empty($data['hits'][0]['largeImageURL'])) {
|
||||
$image_url = $data['hits'][0]['largeImageURL'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Sauvegarde en Base
|
||||
if ($id) {
|
||||
// UPDATE
|
||||
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=?, vehicle_id=?, image_url=? WHERE id=?";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id, $id]);
|
||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id, $image_url, $id]);
|
||||
} else {
|
||||
// INSERT (avec vehicle_id)
|
||||
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes, vehicle_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
// INSERT
|
||||
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes, vehicle_id, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id]);
|
||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id, $image_url]);
|
||||
$id = $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
|
||||
+556
-352
File diff suppressed because it is too large
Load Diff
@@ -122,9 +122,10 @@ window.closeHolidayModal = window.closeHolidayModal || function() {
|
||||
if(modal) modal.style.display = 'none';
|
||||
document.body.classList.remove('no-scroll');
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<script src="/modules/holidays/holidays.js"></script>
|
||||
|
||||
<?php
|
||||
function renderHolidayCard($h, $pdo) {
|
||||
@@ -136,8 +137,9 @@ function renderHolidayCard($h, $pdo) {
|
||||
$dateDisplay = htmlspecialchars($h['period_hint'] ?? '');
|
||||
|
||||
if (empty($dateDisplay) && $h['start_date']) {
|
||||
$dateDisplay = date('d/m/Y', strtotime($h['start_date']));
|
||||
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($h['end_date']));
|
||||
// 💡 Format Jour/Mois uniquement
|
||||
$dateDisplay = date('d/m', strtotime($h['start_date']));
|
||||
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m', strtotime($h['end_date']));
|
||||
}
|
||||
|
||||
$statusClass = match($h['status']) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<div id="holidayModal" class="pf-modal">
|
||||
<div class="pf-modal-content hol-modal-content" style="max-width: 500px;"> <h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3>
|
||||
<div class="pf-modal-content hol-modal-content" style="max-width: 500px;">
|
||||
<h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3>
|
||||
|
||||
<form action="/modules/holidays/includes/api/save_holiday.php" method="POST" id="holidayForm">
|
||||
<input type="hidden" name="id" id="inp_id">
|
||||
@@ -23,23 +24,21 @@
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<div class="hol-flex-1">
|
||||
<label class="pf-label"><?= tr('hdl_label_period') ?></label>
|
||||
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="<?= tr('hdl_ph_period') ?>">
|
||||
</div>
|
||||
<div class="hol-date-range-group">
|
||||
<div class="hol-flex-1">
|
||||
<label class="pf-label"><?= tr('hdl_label_from') ?></label>
|
||||
<input type="date" name="start_date" id="inp_start" class="pf-input">
|
||||
</div>
|
||||
<div class="hol-flex-1">
|
||||
<label class="pf-label"><?= tr('hdl_label_to') ?></label>
|
||||
<input type="date" name="end_date" id="inp_end" class="pf-input">
|
||||
</div>
|
||||
<!-- DATES AVEC FLATPICKR -->
|
||||
<div class="hol-flex-2">
|
||||
<label class="pf-label">📅 Dates du voyage</label>
|
||||
<input type="text" id="hol_date_range" class="pf-input" placeholder="Sélectionnez les dates..." readonly style="cursor: pointer; background-color: var(--bg-panel); color: var(--primary); font-weight: bold; text-align: center; letter-spacing: 0.5px;">
|
||||
<!-- Vraies données pour save_holiday.php -->
|
||||
<input type="hidden" name="start_date" id="inp_start">
|
||||
<input type="hidden" name="end_date" id="inp_end">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 15px; padding: 10px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;">
|
||||
<div class="form-group" style="margin-top: 15px; padding: 10px; background: var(--bg-subtle); border-radius: 8px; border: 1px solid var(--border-light);">
|
||||
<label class="pf-label" style="margin-bottom: 5px;">🚗 Véhicule utilisé (Optionnel)</label>
|
||||
<select name="vehicle_id" id="inp_vehicle_id" class="pf-input">
|
||||
<option value="">-- Aucun / Autre transport --</option>
|
||||
@@ -52,21 +51,16 @@
|
||||
<hr class="hol-divider">
|
||||
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<div class="hol-flex-1">
|
||||
<label class="pf-label">🍔 <?= tr('hdl_label_budget_food') ?></label>
|
||||
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
|
||||
</div>
|
||||
<div>
|
||||
<div class="hol-flex-1">
|
||||
<label class="pf-label">🎁 <?= tr('hdl_label_budget_extras') ?></label>
|
||||
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 10px;">
|
||||
<label class="pf-label"><?= tr('hdl_label_notes') ?></label>
|
||||
<textarea name="notes" id="inp_notes" class="pf-input" rows="2" placeholder="<?= tr('hdl_ph_notes') ?>"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary hol-btn-delete"><?= tr('btn_delete') ?></button>
|
||||
<button type="button" onclick="closeHolidayModal()" class="pf-btn btn-secondary"><?= tr('btn_cancel') ?></button>
|
||||
|
||||
@@ -19,7 +19,7 @@ $grandTotal = $totalGeneral + $totalSteps;
|
||||
<h1 style="font-size: 2.5rem; color: #0f172a; margin-bottom: 10px;"><?= htmlspecialchars($holiday['title'] ?? $holiday['name'] ?? 'Mon Voyage') ?></h1>
|
||||
<h2 style="font-size: 1.5rem; color: #64748b; font-weight: normal; margin-top: 0;">
|
||||
<?php if (!empty($holiday['start_date']) && !empty($holiday['end_date'])): ?>
|
||||
Du <?= date('d/m/Y', strtotime($holiday['start_date'])) ?> au <?= date('d/m/Y', strtotime($holiday['end_date'])) ?>
|
||||
Du <?= date('d/m', strtotime($holiday['start_date'])) ?> au <?= date('d/m', strtotime($holiday['end_date'])) ?>
|
||||
<?php else: ?>
|
||||
Dates à définir
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -153,7 +153,7 @@ function renderAttachmentsView(atts,noteId){
|
||||
<div class="memo-att-title">🖼️ Images</div>
|
||||
<div class="att-img-grid">${images.map(a=>`
|
||||
<div class="att-img-item" onclick="openLightbox('${API}?action=file&id=${a.id}')">
|
||||
<img src="${API}?action=file&id=${a.id}" loading="lazy" alt="${escHtml(a.original_name||'')}">
|
||||
<img src="${API}?action=file&id=${a.id}" alt="${escHtml(a.original_name||'')}">
|
||||
<button class="att-del" onclick="event.stopPropagation();deleteAttachment(${a.id},${noteId})" title="Supprimer">✕</button>
|
||||
</div>`).join('')}</div></div>`;
|
||||
}
|
||||
|
||||
+6
-16
@@ -26,33 +26,23 @@ $stops = $params['stops'];
|
||||
$fuelL100 = (float)($params['fuel_l100'] ?? 7);
|
||||
$fuelPrice= (float)($params['fuel_price'] ?? 1.85);
|
||||
|
||||
// --- Load databases ---
|
||||
$tollsPath = __DIR__ . '/data/tolls.json';
|
||||
$gpsPath = __DIR__ . '/data/toll_gps.json';
|
||||
// --- Load pre-indexed PHP database ---
|
||||
$tollsDataPath = __DIR__ . '/data/tolls.data.php';
|
||||
$gpsPath = __DIR__ . '/data/toll_gps.json';
|
||||
|
||||
if (!file_exists($tollsPath)) {
|
||||
if (!file_exists($tollsDataPath)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Base de péages introuvable']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$tollsRaw = json_decode(file_get_contents($tollsPath), true);
|
||||
$tollData = $tollsRaw['data'] ?? [];
|
||||
// Grâce à OPcache, le fichier est exécuté et stocké en RAM partagée
|
||||
$tollIndex = require $tollsDataPath;
|
||||
|
||||
$gpsData = [];
|
||||
if (file_exists($gpsPath)) {
|
||||
$gpsData = json_decode(file_get_contents($gpsPath), true) ?? [];
|
||||
}
|
||||
|
||||
// --- Build lookup index: [op][entry][exit] => c1_price ---
|
||||
$tollIndex = [];
|
||||
foreach ($tollData as $row) {
|
||||
$op = $row['op'];
|
||||
$e = $row['e'];
|
||||
$x = $row['x'];
|
||||
$c1 = (float)$row['c1'];
|
||||
$tollIndex[$op][$e][$x] = $c1;
|
||||
}
|
||||
|
||||
// --- Helper: great-circle distance (km) ---
|
||||
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||
$R = 6371.0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user