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 class="tab-icon">📊</span>
|
||||||
<span><?= tr('budget_tab_recap') ?></span>
|
<span><?= tr('budget_tab_recap') ?></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="?tab=provisions" class="tab-item <?= $tab == 'provisions' ? 'active' : '' ?>">
|
||||||
|
<span class="tab-icon">🧮</span>
|
||||||
|
<span><?= tr('budget_tab_provisions') ?></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="pf-section">
|
<section class="pf-section">
|
||||||
<?php
|
<?php
|
||||||
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev'];
|
$allowedTabs = ['recap', 'suivi', 'epargne', 'budget_prev', 'provisions'];
|
||||||
|
|
||||||
if (in_array($tab, $allowedTabs)) {
|
if (in_array($tab, $allowedTabs)) {
|
||||||
$viewPath = __DIR__ . "/modules/budget/views/" . $tab . ".php";
|
$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é
|
salary_id INT DEFAULT NULL -- 🔥 AJOUT : Lien vers le salaire configuré
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) 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 (
|
CREATE TABLE IF NOT EXISTS pf_alloc_categories (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
@@ -297,7 +309,8 @@ CREATE TABLE IF NOT EXISTS pf_holidays (
|
|||||||
budget_extra DECIMAL(10,2) DEFAULT 0,
|
budget_extra DECIMAL(10,2) DEFAULT 0,
|
||||||
notes TEXT DEFAULT NULL,
|
notes TEXT DEFAULT NULL,
|
||||||
vehicle_id INT 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
|
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) 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 style="font-size:1.1rem; line-height:1; margin-right:6px;">🏫</span>
|
||||||
<span><?= htmlspecialchars($mode) ?></span>
|
<span><?= htmlspecialchars($mode) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<?php else:
|
<?php else: ?>
|
||||||
$hue = ($index * 137) % 360; ?>
|
|
||||||
<div class="pf-legend-item">
|
<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>
|
<span><?= htmlspecialchars($mode) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<?php endif;
|
<?php endif;
|
||||||
@@ -452,7 +451,8 @@ require __DIR__ . '/header.php';
|
|||||||
|
|
||||||
<!-- Enfants Malades (Dynamique avec couleur BDD) -->
|
<!-- Enfants Malades (Dynamique avec couleur BDD) -->
|
||||||
<?php foreach ($kids as $kid):
|
<?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-item">
|
||||||
<div class="pf-legend-color" style="background: <?= htmlspecialchars($color) ?>; opacity: 0.8;"></div>
|
<div class="pf-legend-color" style="background: <?= htmlspecialchars($color) ?>; opacity: 0.8;"></div>
|
||||||
@@ -477,5 +477,6 @@ window.FAMILY_CONFIG = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</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'; ?>
|
<?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';
|
require __DIR__ . '/modules/holidays/views/list.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Inclusion du JS global du module (Pont i18n déjà géré dans le header)
|
// 4. Inclusion des librairies globales du module (Flatpickr)
|
||||||
echo '<script src="/modules/holidays/holidays.js"></script>';
|
?>
|
||||||
|
<!-- 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';
|
require __DIR__ . '/footer.php';
|
||||||
+41
-34
@@ -1,52 +1,59 @@
|
|||||||
<?php
|
<?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) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
$host = getenv('DB_HOST') ?: 'househub-db';
|
$envHost = getenv('DB_HOST');
|
||||||
$user = getenv('DB_USER') ?: 'househub';
|
|
||||||
$pass = getenv('DB_PASS') ?: 'changeme';
|
|
||||||
$db = $_SESSION['family_db'] ?? null;
|
|
||||||
|
|
||||||
if (!$db) {
|
// 1. Détection stricte de l'environnement
|
||||||
$current = basename($_SERVER['PHP_SELF'] ?? '');
|
if ($envHost === 'househub-db') {
|
||||||
if (!in_array($current, ['login.php', 'register.php'])) {
|
// Docker
|
||||||
header('Location: /login.php');
|
$host = 'househub-db';
|
||||||
exit;
|
$user = getenv('DB_USER') ?: 'househub';
|
||||||
}
|
$pass = getenv('DB_PASS') ?: 'changeme';
|
||||||
return;
|
} else {
|
||||||
|
// XAMPP
|
||||||
|
$host = '127.0.0.1'; // Plus stable que 'localhost' sous XAMPP
|
||||||
|
$user = 'root';
|
||||||
|
$pass = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 2. Multi-tenant strict : cibler la DB de la famille courante
|
||||||
$pdo = new PDO(
|
$db = 'househub_meta'; // Fallback par défaut
|
||||||
"mysql:host=$host;dbname=$db;charset=utf8mb4",
|
if (!empty($_SESSION['user']['family_id'])) {
|
||||||
$user, $pass,
|
$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_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
||||||
PDO::ATTR_TIMEOUT => 30,
|
PDO::ATTR_TIMEOUT => 30,
|
||||||
]
|
];
|
||||||
);
|
|
||||||
$pdo->exec("SET collation_connection = utf8mb4_general_ci");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$foyer = $pdo->query("SELECT currency, zone_scolaire FROM pf_foyer_settings WHERE id = 1")->fetch();
|
$pdo = new PDO($dsn, $user, $pass, $options);
|
||||||
if (!defined('CURRENCY')) {
|
|
||||||
define('CURRENCY', $foyer['currency'] ?? '€');
|
// Forcer la collation
|
||||||
}
|
$pdo->exec("SET collation_connection = utf8mb4_general_ci");
|
||||||
if (!defined('ZONE_SCOLAIRE')) {
|
$pdo->exec("SET collation_database = utf8mb4_general_ci");
|
||||||
define('ZONE_SCOLAIRE', $foyer['zone_scolaire'] ?? 'C');
|
$pdo->exec("SET collation_server = utf8mb4_general_ci");
|
||||||
}
|
|
||||||
} catch (\PDOException $e) {
|
|
||||||
if (!defined('CURRENCY')) define('CURRENCY', '€');
|
|
||||||
if (!defined('ZONE_SCOLAIRE')) define('ZONE_SCOLAIRE', 'C');
|
|
||||||
}
|
|
||||||
// ------------------------------------------------
|
|
||||||
|
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
if (!headers_sent()) { header('Content-Type: application/json'); http_response_code(500); }
|
die("Erreur de connexion BDD (Host: $host, DB: $db) : " . $e->getMessage());
|
||||||
die(json_encode(['ok' => false, 'error' => 'Erreur BDD : ' . $e->getMessage()]));
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -775,6 +775,27 @@ return [
|
|||||||
'bud_sav_selection' => 'Selecció:',
|
'bud_sav_selection' => 'Selecció:',
|
||||||
'bud_sav_ph_name' => 'Nom del compte',
|
'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
|
// JS & Errors
|
||||||
'bud_sav_confirm_delete_month' => 'Estàs segur que vols suprimir totes les dades de %m per a %o?',
|
'bud_sav_confirm_delete_month' => 'Estàs segur que vols suprimir totes les dades de %m per a %o?',
|
||||||
'bud_sav_prompt_duplicate' => "Vols duplicar les dades de %s cap a %t1?\n\nIntrodueix el nou TOTAL al banc (€) per a %t2:",
|
'bud_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_target_ph' => 'E.g., 150',
|
||||||
'bud_prev_transfer_to' => 'To',
|
'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)
|
// MODULE: GIFTS (gift-list)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -773,6 +773,34 @@ return [
|
|||||||
'bud_sav_ph_name' => 'Nom du compte',
|
'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
|
// JS & Erreurs
|
||||||
'bud_sav_confirm_delete_month' => 'Voulez-vous vraiment supprimer toutes les données de %m pour %o ?',
|
'bud_sav_confirm_delete_month' => 'Voulez-vous vraiment supprimer toutes les données de %m pour %o ?',
|
||||||
'bud_sav_prompt_duplicate' => "Voulez-vous dupliquer les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
|
'bud_sav_prompt_duplicate' => "Voulez-vous dupliquer les données de %s vers %t1 ?\n\nSaisissez le nouveau TOTAL en banque (€) pour %t2 :",
|
||||||
|
|||||||
+19
-3
@@ -1,9 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// includes/meta_db.php
|
||||||
// Connexion à la base meta (gestion des familles et utilisateurs)
|
// Connexion à la base meta (gestion des familles et utilisateurs)
|
||||||
$meta_host = getenv('DB_HOST') ?: 'househub-db';
|
|
||||||
|
$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';
|
$meta_db = 'househub_meta';
|
||||||
$meta_user = getenv('DB_USER') ?: 'househub';
|
|
||||||
$meta_pass = getenv('DB_PASS') ?: 'changeme';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$meta_pdo = new PDO(
|
$meta_pdo = new PDO(
|
||||||
@@ -14,8 +28,10 @@ try {
|
|||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci",
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
die(json_encode(['error' => 'Meta DB unavailable: ' . $e->getMessage()]));
|
die(json_encode(['error' => 'Meta DB unavailable: ' . $e->getMessage()]));
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
+28
-29
@@ -1,49 +1,48 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/includes/meta_db.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_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 {
|
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);
|
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
foreach ($families as $family) {
|
foreach ($families as $f) {
|
||||||
$dbName = $family['db_name'];
|
$dbName = $f['db_name'];
|
||||||
|
echo "<h3>Famille : {$f['name']} ($dbName)</h3><ul>";
|
||||||
|
|
||||||
try {
|
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
|
// 1. On vérifie si la clé primaire existe déjà pour éviter de faire planter le script
|
||||||
$pdo->exec("ALTER TABLE pf_holidays ADD COLUMN return_step_id INT DEFAULT NULL");
|
$hasPrimaryKey = $pdo->query("SHOW KEYS FROM pf_holidays WHERE Key_name = 'PRIMARY'")->fetch();
|
||||||
|
|
||||||
// 2. Mise à jour pf_holidays_items
|
if (!$hasPrimaryKey) {
|
||||||
$pdo->exec("ALTER TABLE pf_holidays_items ADD COLUMN step_type VARCHAR(20) DEFAULT 'stop'");
|
$pdo->exec("ALTER TABLE pf_holidays ADD PRIMARY KEY (id)");
|
||||||
$pdo->exec("ALTER TABLE pf_holidays_items ADD COLUMN expense_context VARCHAR(20) DEFAULT NULL");
|
echo "<li>✅ Clé primaire restaurée sur la colonne `id`.</li>";
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Migration des données (is_return -> return_step_id)
|
// 2. On restaure la propriété AUTO_INCREMENT
|
||||||
// On cherche la première étape cochée "is_return" et on l'assigne au voyage
|
$pdo->exec("ALTER TABLE pf_holidays MODIFY id INT(11) NOT NULL AUTO_INCREMENT");
|
||||||
$pdo->exec("
|
echo "<li>✅ Propriété AUTO_INCREMENT restaurée.</li>";
|
||||||
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
|
|
||||||
)
|
|
||||||
");
|
|
||||||
|
|
||||||
// 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) {
|
} 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) {
|
} 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;
|
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
|
<?php
|
||||||
// modules/budget/views/budget_prev.php
|
// modules/budget/views/budget_prev.php
|
||||||
|
|
||||||
// 1. Chargement dynamique des ADULTES de la famille
|
// 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 NOT IN ('enfant', 'nounou') AND is_active = 1 ORDER BY id ASC");
|
$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);
|
$budgetParents = $stmtPeople->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// Sécurité : au cas où aucun adulte n'est trouvé, on évite un crash
|
// 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) {
|
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();
|
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
let html = "";
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
for (let d = 1; d <= daysInMonth; d++) {
|
for (let d = 1; d <= daysInMonth; d++) {
|
||||||
const dateObj = new Date(year, month, d);
|
const dateObj = new Date(year, month, d);
|
||||||
const dayOfWeek = dateObj.getDay();
|
const dayOfWeek = dateObj.getDay();
|
||||||
|
|
||||||
|
// Exclure les week-ends des jours ouvrés (0 = Dimanche, 6 = Samedi)
|
||||||
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
|
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
|
||||||
|
|
||||||
const iso = this.getLocalIsoDate(dateObj);
|
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);
|
const dayEvents = this.events.filter((e) => e.date === iso);
|
||||||
|
|
||||||
dayEvents.forEach((e) => {
|
dayEvents.forEach((e) => {
|
||||||
const dur = parseFloat(e.duration) || 1;
|
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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 (
|
if (
|
||||||
!this.publicHolidayDates.has(iso) &&
|
e.type === "CHILD_SICK" &&
|
||||||
!dayEvents.some((e) => e.type === "VACANCES_SCOLAIRES")
|
Number(e.person_id) === Number(kid.id)
|
||||||
) {
|
) {
|
||||||
let dayAbsence = 0;
|
sick += dur;
|
||||||
dayEvents.forEach((e) => {
|
|
||||||
if (["HELPER_OFF", "HELPER_EXTRA", "CHILD_SICK"].includes(e.type)) {
|
|
||||||
dayAbsence += parseFloat(e.duration) || 1;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
stats.presence += Math.max(0, 1 - dayAbsence);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
// Calcul final strict : Jours ouvrés - Maladie (de l'enfant) - Extra - Off
|
||||||
<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);">
|
const presence = Math.max(0, workingDays - sick - extra - off);
|
||||||
<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>
|
// Rendu HTML individuel
|
||||||
<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>
|
html += `
|
||||||
<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>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToCurrentMonth() {
|
scrollToCurrentMonth() {
|
||||||
|
|||||||
+1005
-1091
File diff suppressed because it is too large
Load Diff
+1175
-243
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)
|
// 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');
|
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 {
|
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') {
|
if ($_POST['action'] === 'add_single_item') {
|
||||||
$holiday_id = (int)$_POST['holiday_id'];
|
$holiday_id = (int)$_POST['holiday_id'];
|
||||||
$sort_order = (int)$_POST['sort_order'];
|
$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;
|
$date = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
||||||
$time = !empty($_POST['item_time']) ? $_POST['item_time'] : 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();
|
$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 = $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([
|
$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'],
|
$stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
|
||||||
$sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
|
$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();
|
$pdo->commit();
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
|
echo json_encode(['success' => true, 'id' => $newId]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
echo json_encode(['success' => false, 'error' => 'Etape introuvable']);
|
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]);
|
echo json_encode(['success' => true]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if ($pdo->inTransaction()) { $pdo->rollBack(); }
|
if ($pdo->inTransaction()) { $pdo->rollBack(); }
|
||||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
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]);
|
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ?")->execute([$holiday_id, $old_sort_order]);
|
||||||
$target_order = $old_sort_order;
|
$target_order = $old_sort_order;
|
||||||
} else {
|
} else {
|
||||||
|
// 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 = $pdo->prepare("SELECT MAX(sort_order) FROM pf_holidays_items WHERE holiday_id = ?");
|
||||||
$stmtMax->execute([$holiday_id]);
|
$stmtMax->execute([$holiday_id]);
|
||||||
$max = $stmtMax->fetchColumn();
|
$max = $stmtMax->fetchColumn();
|
||||||
$target_order = ($max !== null) ? (int)$max + 1 : 0;
|
$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;
|
$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;
|
$dur = !empty($_POST['items']['duration'][$i]) ? (int)$_POST['items']['duration'][$i] : 1;
|
||||||
$context = !empty($_POST['items']['context'][$i]) ? $_POST['items']['context'][$i] : 'local';
|
$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++;
|
$validItemsCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,16 +28,51 @@ $vehicle_id = !empty($_POST['vehicle_id']) ? (int)$_POST['vehicle_id'] : null;
|
|||||||
try {
|
try {
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// 1. Détection d'un changement de nom pour refaire l'appel API
|
||||||
|
$image_url = null;
|
||||||
|
$old_title = null;
|
||||||
if ($id) {
|
if ($id) {
|
||||||
// UPDATE (avec vehicle_id)
|
$stmtOld = $pdo->prepare("SELECT title, image_url FROM pf_holidays WHERE id = ?");
|
||||||
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=?, vehicle_id=? 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 = $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 {
|
} else {
|
||||||
// INSERT (avec vehicle_id)
|
// INSERT
|
||||||
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes, vehicle_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
$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 = $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();
|
$id = $pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+445
-241
@@ -16,11 +16,13 @@ $stmt = $pdo->prepare("
|
|||||||
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
|
(COALESCE(h.budget_food, 0) + COALESCE(h.budget_extra, 0) + COALESCE((SELECT SUM(amount) FROM pf_holidays_items WHERE holiday_id = h.id), 0)) as total_cost,
|
||||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1) as total_paid,
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND is_paid = 1) as total_paid,
|
||||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved,
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved,
|
||||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND expense_context = 'transit') as total_transit
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND (expense_context = 'transit' OR (category = 'transport' AND (name LIKE '%Essence%' OR name LIKE '%Carburant%')))) as total_fuel,
|
||||||
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND category = 'transport' AND (name LIKE '%Péage%' OR name LIKE '%Peage%')) as total_tolls
|
||||||
FROM pf_holidays h
|
FROM pf_holidays h
|
||||||
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
|
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
|
||||||
WHERE h.id = ?
|
WHERE h.id = ?
|
||||||
");
|
");
|
||||||
|
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@@ -64,8 +66,9 @@ $mapPoints = array_values($steps);
|
|||||||
// Affichage de la date
|
// Affichage de la date
|
||||||
$dateDisplay = htmlspecialchars($holiday['period_hint'] ?? '');
|
$dateDisplay = htmlspecialchars($holiday['period_hint'] ?? '');
|
||||||
if (empty($dateDisplay) && $holiday['start_date']) {
|
if (empty($dateDisplay) && $holiday['start_date']) {
|
||||||
$dateDisplay = date('d/m/Y', strtotime($holiday['start_date']));
|
// 💡 Format Jour/Mois uniquement
|
||||||
if ($holiday['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($holiday['end_date']));
|
$dateDisplay = date('d/m', strtotime($holiday['start_date']));
|
||||||
|
if ($holiday['end_date']) $dateDisplay .= ' → ' . date('d/m', strtotime($holiday['end_date']));
|
||||||
}
|
}
|
||||||
|
|
||||||
$cost = (float)$holiday['total_cost'];
|
$cost = (float)$holiday['total_cost'];
|
||||||
@@ -74,203 +77,152 @@ $saved = (float)$holiday['total_saved'];
|
|||||||
$leftToPay = max(0, $cost - $paid);
|
$leftToPay = max(0, $cost - $paid);
|
||||||
$pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0;
|
$pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0;
|
||||||
$pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
$pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||||
|
|
||||||
|
// Image de fond (API Pixabay ou fallback par défaut)
|
||||||
|
$bgImage = !empty($holiday['image_url']) ? htmlspecialchars($holiday['image_url']) : '/modules/home/assets/img/background.jpg';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.mobile-only { display: none !important; }
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.desktop-only { display: none !important; }
|
|
||||||
.mobile-only { display: flex !important; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="pf-holidays-detail">
|
<div class="pf-holidays-detail">
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;">
|
<!-- Scripts JSON invisibles -->
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
|
||||||
<a href="?tab=list" class="pf-btn btn-secondary pf-btn-small" style="width: fit-content; text-decoration: none;"><?= tr('btn_back') ?></a>
|
|
||||||
<div style="display: flex; align-items: center; gap: 15px;">
|
|
||||||
<h1 style="margin: 0; font-size: 1.8rem;"><?= htmlspecialchars($holiday['title']) ?></h1>
|
|
||||||
<span class="hol-badge-status"><?= tr('hdl_status_' . $holiday['status']) ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
|
||||||
<script id="holidayDataJson" type="application/json">
|
<script id="holidayDataJson" type="application/json">
|
||||||
<?= json_encode(['main' => $holiday, 'items' => $generalItems], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
|
<?= json_encode(['main' => $holiday, 'items' => $generalItems], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button type="button" class="pf-btn btn-primary pf-btn-small" onclick="generateTravelBook()" style="display: flex; align-items: center; gap: 6px;">
|
<!-- 1. HERO HEADER IMMERSIF -->
|
||||||
📖 Carnet de Voyage
|
<div class="hol-hero-banner">
|
||||||
</button>
|
<img src="<?= $bgImage ?>" class="hol-hero-bg-img" alt="">
|
||||||
|
<div class="hol-hero-overlay"></div>
|
||||||
|
<div class="hol-hero-content">
|
||||||
|
|
||||||
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="editHoliday(JSON.parse(document.getElementById('holidayDataJson').textContent))">
|
<div class="hol-hero-top">
|
||||||
<?= tr('btn_edit_bases') ?>
|
<a href="?tab=list" class="hol-hero-back-link">◀ <?= tr('btn_back') ?></a>
|
||||||
</button>
|
<span class="hol-hero-status-badge"><?= tr('hdl_status_' . $holiday['status']) ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h1 class="hol-hero-title"><?= htmlspecialchars($holiday['title']) ?></h1>
|
||||||
|
<div class="hol-hero-date-badge">
|
||||||
|
🗓️ <?= $dateDisplay ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hol-summary-card">
|
<!-- LES PILLS (Indicateurs Route & Quotidien) -->
|
||||||
<div class="hol-summary-grid">
|
<div class="hol-hero-pills">
|
||||||
|
|
||||||
<?php if (!empty($holiday['vehicle_name'])): ?>
|
<!-- PILL ROUTE (Masqué par défaut, géré par OSRM) -->
|
||||||
<div class="hol-summary-item">
|
<span id="block_total_distance" class="hol-pill-block hol-hidden">
|
||||||
<div class="hol-summary-label">Transport</div>
|
<span class="hol-pill">
|
||||||
<div class="hol-summary-value">🚗 <?= htmlspecialchars($holiday['vehicle_name']) ?></div>
|
🛣️ <span id="global_total_distance">0</span> km (⏱️ <span id="global_total_duration">0h00</span>)
|
||||||
</div>
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- PILL PÉAGES -->
|
||||||
|
<span class="hol-pill">
|
||||||
|
💳 <?= number_format($holiday['total_tolls'], 0) ?> €
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- PILL ESSENCE (Avec Edit + Oeil) -->
|
||||||
|
<span class="hol-pill">
|
||||||
|
⛽ <span id="global_fuel_cost"><?= number_format($holiday['total_fuel'], 0) ?></span> €
|
||||||
|
<span onclick="updateFuelPrice()" class="hol-pill-action" title="Modifier le prix de l'essence">(<span id="display_fuel_price">1.85</span>€/L) ✏️</span>
|
||||||
|
<?php if ($holiday['total_fuel'] > 0 || $holiday['total_tolls'] > 0): ?>
|
||||||
|
<span onclick="openTransitModal()" class="hol-pill-action" title="Voir le détail des trajets">👁️</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="hol-summary-item">
|
|
||||||
<div class="hol-summary-label">
|
|
||||||
Frais de route (Essence/Péages)
|
|
||||||
</div>
|
|
||||||
<div class="hol-summary-value" style="display:flex; align-items:center; gap:6px;">
|
|
||||||
<span style="font-size: 1.1rem;">⛽</span>
|
|
||||||
<strong><?= number_format($holiday['total_transit'], 0) ?> €</strong>
|
|
||||||
|
|
||||||
<span onclick="updateFuelPrice()" style="font-size: 0.75rem; color: var(--text-muted); cursor: pointer; transition: color 0.2s; display: inline-flex; align-items: center; gap: 3px;" onmouseover="this.style.color='var(--primary)';" onmouseout="this.style.color='var(--text-muted)';" title="Modifier le prix estimé du carburant">
|
|
||||||
(<span id="display_fuel_price">1.85</span> €/L) <span style="font-size:0.7rem; opacity:0.8;">✏️</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<?php if ($holiday['total_transit'] > 0): ?>
|
<!-- PILL BURGER (Food) -->
|
||||||
<span onclick="openTransitModal()" style="font-size: 1rem; cursor: pointer; opacity: 0.5; transition: opacity 0.2s; margin-left: 4px; display: inline-flex; align-items: center;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.5'" title="Voir le détail des trajets">
|
<span class="hol-pill">
|
||||||
👁️
|
🍔 <?= number_format($holiday['budget_food'], 0) ?> €
|
||||||
</span>
|
</span>
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-summary-item" id="block_total_distance" style="display:none;">
|
<!-- PILL CADEAU (Extras) -->
|
||||||
<div class="hol-summary-label">Route & Temps de conduite</div>
|
<span class="hol-pill">
|
||||||
<div class="hol-summary-value" style="display:flex; align-items:center; gap:6px;">
|
🎁 <?= number_format($holiday['budget_extra'], 0) ?> €
|
||||||
<span style="font-size: 1.1rem;">🛣️</span>
|
|
||||||
<strong><span id="global_total_distance">0</span> km</strong>
|
|
||||||
<span style="font-size: 0.85rem; color: var(--text-muted); margin-left: 5px;">
|
|
||||||
(⏱️ <span id="global_total_duration">0h00</span>)
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-summary-item">
|
<!-- FINANCES GLOBALES -->
|
||||||
<div class="hol-summary-label"><?= tr('hdl_label_budget_food_extras') ?></div>
|
<div class="hol-hero-finances">
|
||||||
<div class="hol-summary-value">🍔 <?= number_format($holiday['budget_food'], 0) ?> € | 🎁 <?= number_format($holiday['budget_extra'], 0) ?> €</div>
|
<div class="hol-finance-block">
|
||||||
|
<div class="hol-finance-label"><?= tr('hdl_total_budget') ?></div>
|
||||||
|
<div class="hol-finance-value"><?= number_format($cost, 0, ',', ' ') ?> €</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hol-summary-item">
|
<div class="hol-finance-block hol-finance-right">
|
||||||
<div class="hol-summary-label"><?= tr('hdl_total_budget') ?></div>
|
<div class="hol-finance-label"><?= tr('hdl_left_to_pay') ?></div>
|
||||||
<div class="hol-summary-value total"><?= number_format($cost, 0, ',', ' ') ?> €</div>
|
<div class="hol-finance-value hol-finance-value-due"><?= number_format($leftToPay, 0, ',', ' ') ?> €</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre de progression -->
|
||||||
<div class="hol-progress-bar">
|
<div class="hol-progress-bar">
|
||||||
<div class="hol-progress-paid" style="width:<?= $pctPaid ?>%;" title="<?= tr('hdl_paid') ?>"></div>
|
<div class="hol-progress-paid" style="width: <?= $pctPaid ?>%;"></div>
|
||||||
<div class="hol-progress-saved" style="width:<?= $pctSaved ?>%;" title="<?= tr('hdl_saved') ?>"></div>
|
<div class="hol-progress-saved" style="width: <?= $pctSaved ?>%;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="hol-hero-progress-labels">
|
||||||
|
<span class="hol-label-paid-text">✓ <?= tr('hdl_paid') ?> : <?= number_format($paid, 0, ',', ' ') ?> €</span>
|
||||||
|
<span class="hol-label-saved-text">💼 <?= tr('hdl_saved') ?> : <?= number_format($saved, 0, ',', ' ') ?> €</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hol-progress-labels">
|
|
||||||
<span class="hol-label-paid">✓ <?= tr('hdl_paid') ?> : <?= number_format($paid, 0, ',', ' ') ?> €</span>
|
|
||||||
<span class="hol-label-saved">💼 <?= tr('hdl_saved') ?> : <?= number_format($saved, 0, ',', ' ') ?> €</span>
|
|
||||||
<span class="hol-label-left">⏳ <?= tr('hdl_left_to_pay') ?> : <?= number_format($leftToPay, 0, ',', ' ') ?> €</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hol-layout-grid">
|
<!-- 2. BARRE D'ACTIONS RAPIDES -->
|
||||||
<div class="hol-panel">
|
<div class="hol-quick-actions-bar">
|
||||||
<div class="hol-panel-header" style="flex-wrap: wrap; gap: 10px;">
|
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="toggleTripMap()">
|
||||||
<div class="hol-panel-header-group">
|
🗺️ Carte
|
||||||
<h3 style="margin:0;"><?= tr('hdl_map_itinerary') ?></h3>
|
</button>
|
||||||
|
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="openGlobalPlanningModal()">
|
||||||
|
📅 Planning
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="generateTravelBook()">
|
||||||
|
📖 PDF
|
||||||
|
</button>
|
||||||
|
<button type="button" class="pf-btn btn-secondary pf-btn-small hol-quick-action-btn" onclick="editHoliday(JSON.parse(document.getElementById('holidayDataJson').textContent))">
|
||||||
|
⚙️ Modifier
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. LA CARTE (Masquée par défaut) -->
|
||||||
|
<div id="tripMapWrapper" class="hol-map-wrapper hol-hidden">
|
||||||
|
<div class="hol-panel-header hol-map-panel-header">
|
||||||
|
<h3 class="hol-map-title">Itinéraire</h3>
|
||||||
<div class="hol-map-legend">
|
<div class="hol-map-legend">
|
||||||
<span class="hol-legend-item" title="<?= tr('hdl_outbound') ?>"><span class="hol-legend-color aller"></span> <?= tr('hdl_outbound') ?></span>
|
<span class="hol-legend-item"><span class="hol-legend-color aller"></span> Aller</span>
|
||||||
<span class="hol-legend-item" title="<?= tr('hdl_return') ?>"><span class="hol-legend-color retour"></span> <?= tr('hdl_return') ?></span>
|
<span class="hol-legend-item"><span class="hol-legend-color retour"></span> Retour</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="pf-btn pf-btn-small" onclick="openCheckpointModal('add')"><?= tr('hdl_btn_add_step') ?></button>
|
<!-- IMPORTANT : On garde l'ID tripMap pour que Leaflet (OSRM) fonctionne ! -->
|
||||||
|
<div id="tripMap" class="hol-map-canvas"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tripMap" style="flex:1; width:100%; min-height: 400px; background:#f1f5f9; position: relative; z-index: 1;"></div> </div>
|
|
||||||
|
|
||||||
<div class="hol-panel">
|
<!-- 4. TIMELINE (Carnet de route Pleine Largeur) -->
|
||||||
<div class="hol-panel-header">
|
<div class="hol-panel hol-panel-timeline">
|
||||||
<h3 style="margin:0;"><?= tr('hdl_step_details') ?></h3>
|
<div class="hol-panel-header hol-timeline-header">
|
||||||
|
<h3><?= tr('hdl_step_details') ?></h3>
|
||||||
|
<button class="pf-btn pf-btn-small" onclick="openCheckpointModal('add')">+ <?= tr('hdl_btn_add_step') ?></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hol-panel-body">
|
<div class="hol-panel-body">
|
||||||
<?php if (empty($steps)): ?>
|
<?php if (empty($steps)): ?>
|
||||||
<p style="color:var(--text-muted); font-style:italic; text-align:center; margin-top:40px;"><?= tr('hdl_no_steps') ?></p>
|
<p class="hol-empty-steps-text"><?= tr('hdl_no_steps') ?></p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
|
||||||
<?php if (!empty($generalItems) || $holiday['budget_food'] > 0 || $holiday['budget_extra'] > 0): ?>
|
|
||||||
<div class="hol-checkpoint" style="border-left-color: #64748b; background: #f8fafc; margin-bottom: 20px;">
|
|
||||||
<div class="hol-cp-header">
|
|
||||||
<div style="display: flex; align-items: center; gap: 10px;">
|
|
||||||
<span style="font-size: 1.2rem;">🌍</span>
|
|
||||||
<strong style="color: #0f172a;"><?= tr('hdl_general_costs') ?></strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$generalTotal = $holiday['budget_food'] + $holiday['budget_extra'];
|
|
||||||
foreach ($generalItems as $gi) { $generalTotal += $gi['amount']; }
|
|
||||||
?>
|
|
||||||
<div style="display: flex; align-items: center; gap: 12px;">
|
|
||||||
<div style="font-size: 1.1rem; font-weight: 800; color: var(--primary); white-space: nowrap;"><?= number_format($generalTotal, 2, ',', ' ') ?> €</div>
|
|
||||||
<button onclick='editHoliday(JSON.parse(document.getElementById("holidayDataJson").textContent))' class="btn-icon-small" title="<?= tr('btn_edit') ?>">⚙️</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-cp-body">
|
|
||||||
<?php if ($holiday['budget_food'] > 0): ?>
|
|
||||||
<div class="hol-expense-wrapper"><div class="hol-expense-main">
|
|
||||||
<span class="hol-expense-name" style="color:#64748b;">🍔 <?= tr('hdl_food_bev') ?></span>
|
|
||||||
<span><strong class="hol-expense-amount"><?= number_format($holiday['budget_food'], 2, ',', ' ') ?> €</strong><span class="status-pending">⏳</span></span>
|
|
||||||
</div></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($holiday['budget_extra'] > 0): ?>
|
|
||||||
<div class="hol-expense-wrapper"><div class="hol-expense-main">
|
|
||||||
<span class="hol-expense-name" style="color:#64748b;">🎁 <?= tr('hdl_extras') ?></span>
|
|
||||||
<span><strong class="hol-expense-amount"><?= number_format($holiday['budget_extra'], 2, ',', ' ') ?> €</strong><span class="status-pending">⏳</span></span>
|
|
||||||
</div></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php foreach ($generalItems as $it):
|
|
||||||
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
|
||||||
?>
|
|
||||||
<div class="hol-expense-wrapper">
|
|
||||||
<div class="hol-expense-main">
|
|
||||||
<span class="hol-expense-name">
|
|
||||||
<?= $icon ?> <?= htmlspecialchars($it['name']) ?>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="hol-expense-price-group">
|
|
||||||
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
|
|
||||||
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>">
|
|
||||||
<?= $it['is_paid'] ? '✓' : '⏳' ?>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php foreach ($steps as $step): ?>
|
<?php foreach ($steps as $step): ?>
|
||||||
<div id="step-card-<?= $step['sort_order'] ?>" class="hol-checkpoint hol-checkpoint-draggable" draggable="true" data-location="<?= htmlspecialchars($step['location_name']) ?>">
|
<div id="step-card-<?= $step['sort_order'] ?>" class="hol-checkpoint hol-checkpoint-draggable" draggable="true" data-location="<?= htmlspecialchars($step['location_name']) ?>">
|
||||||
<div class="hol-step-header">
|
<div class="hol-step-header">
|
||||||
|
|
||||||
<div class="hol-step-row-top">
|
<div class="hol-step-row-top">
|
||||||
<?php
|
<?php
|
||||||
$stepIcon = '📍'; $iconBg = 'transparent'; $iconColor = '#64748b';
|
$stepIcon = '📍';
|
||||||
$isReturn = ($holiday['return_step_id'] !== null && $holiday['return_step_id'] == $step['sort_order']);
|
|
||||||
|
|
||||||
if ($step['step_type'] === 'origin') {
|
if ($step['step_type'] === 'origin') {
|
||||||
$stepIcon = '🛫'; $iconBg = '#ecfdf5'; $iconColor = '#059669';
|
$stepIcon = '🛫';
|
||||||
} elseif ($step['step_type'] === 'destination') {
|
} elseif ($step['step_type'] === 'destination') {
|
||||||
$stepIcon = '🛬'; $iconBg = '#fef2f2'; $iconColor = '#e11d48';
|
$stepIcon = '🛬';
|
||||||
}
|
}
|
||||||
|
$isReturn = ($holiday['return_step_id'] !== null && $holiday['return_step_id'] == $step['sort_order']);
|
||||||
?>
|
?>
|
||||||
<div class="hol-step-icon" style="background: <?= $iconBg ?>; color: <?= $iconColor ?>;">
|
<div class="hol-step-icon hol-step-icon-<?= htmlspecialchars($step['step_type']) ?>">
|
||||||
<?= $stepIcon ?>
|
<?= $stepIcon ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -297,7 +249,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<span class="hol-step-date">
|
<span class="hol-step-date">
|
||||||
<?= date('d/m', strtotime($step['step_start_date'])) ?> ➔ <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
<?= date('d/m', strtotime($step['step_start_date'])) ?> ➔ <?= date('d/m', strtotime($step['step_end_date'])) ?>
|
||||||
</span>
|
</span>
|
||||||
<div class="hol-weather-info" style="display:inline-flex;"></div>
|
<div class="hol-weather-info"></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -305,15 +257,13 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<span class="hol-step-price"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</span>
|
<span class="hol-step-price"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</span>
|
||||||
|
|
||||||
<button onclick="openGpsModal(<?= $step['lat'] ?>, <?= $step['lng'] ?>)"
|
<button onclick="openGpsModal(<?= $step['lat'] ?>, <?= $step['lng'] ?>)"
|
||||||
class="btn-icon-small"
|
class="btn-icon-small hol-btn-action-icon"
|
||||||
title="Y aller avec le GPS"
|
title="Y aller avec le GPS">
|
||||||
style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">
|
|
||||||
🧭
|
🧭
|
||||||
</button>
|
</button>
|
||||||
<button onclick="openDocsModal(<?= $step['sort_order'] ?>)" class="btn-icon-small" title="Documents & Billets" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">📎</button>
|
<button onclick="openDocsModal(<?= $step['sort_order'] ?>)" class="btn-icon-small hol-btn-action-icon" title="Documents & Billets">📎</button>
|
||||||
|
|
||||||
<button onclick='openPlanningModal(<?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('hdl_view_planning') ?>" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">📅</button>
|
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small hol-btn-action-icon" title="<?= tr('btn_edit') ?>">✏️</button>
|
||||||
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('btn_edit') ?>" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">✏️</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -326,15 +276,32 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
if ($it['name'] === 'PF_TECHNICAL_POINT') continue;
|
if ($it['name'] === 'PF_TECHNICAL_POINT') continue;
|
||||||
$visibleItemsCount++;
|
$visibleItemsCount++;
|
||||||
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||||
|
|
||||||
|
// On vérifie si l'activité possède ses propres coordonnées GPS
|
||||||
|
$hasCustomGps = (!empty($it['lat']) && !empty($it['lng']) && ((float)$it['lat'] !== (float)$step['lat'] || (float)$it['lng'] !== (float)$step['lng']));
|
||||||
?>
|
?>
|
||||||
<div class="hol-expense-wrapper">
|
<div class="hol-expense-wrapper">
|
||||||
<div class="hol-expense-main">
|
<div class="hol-expense-main">
|
||||||
<span class="hol-expense-name"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
|
<span class="hol-expense-name">
|
||||||
|
<?= $icon ?>
|
||||||
|
<?php if ($hasCustomGps): ?>
|
||||||
|
<span onclick="openGpsModal(<?= $it['lat'] ?>, <?= $it['lng'] ?>)" class="hol-custom-gps-link" title="Y aller (GPS)">
|
||||||
|
<?= htmlspecialchars($it['name']) ?>
|
||||||
|
</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= htmlspecialchars($it['name']) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
|
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
|
||||||
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>" title="<?= $it['is_paid'] ? tr('hdl_paid') : tr('hdl_to_pay') ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
|
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>" title="<?= $it['is_paid'] ? tr('hdl_paid') : tr('hdl_to_pay') ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (!empty($it['notes'])): ?>
|
||||||
|
<div class="hol-expense-note">
|
||||||
|
<?= htmlspecialchars($it['notes']) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
@@ -347,23 +314,22 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (!empty($holiday['notes'])): ?>
|
<div class="pf-card hol-notes-card">
|
||||||
<div class="hol-summary-card" style="margin-top: 24px; padding: 25px; border-left: 5px solid #f59e0b;">
|
<h2 class="pf-card-title">📝 <?= tr('hdl_label_notes') ?></h2>
|
||||||
<h3 style="margin: 0 0 15px 0; font-size: 1.2rem; color: #0f172a; display: flex; align-items: center; gap: 8px;">
|
<div class="pf-card-body">
|
||||||
📝 <?= tr('hdl_label_notes') ?>
|
<textarea id="holidayGlobalNotes" class="pf-input hol-notes-textarea" rows="6" placeholder="<?= tr('hdl_ph_notes') ?>"><?= htmlspecialchars($holiday['notes'] ?? '') ?></textarea>
|
||||||
</h3>
|
<div class="hol-notes-actions">
|
||||||
<div style="font-size: 0.95rem; color: #334155; white-space: pre-wrap; line-height: 1.6;">
|
<button id="btnSaveHolidayNote" class="pf-btn" onclick="saveHolidayGlobalNote(<?= (int)$_GET['id'] ?>)">
|
||||||
<?= htmlspecialchars($holiday['notes']) ?>
|
💾 <?= tr('btn_save') ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="checkpointModal" class="pf-modal">
|
<div id="checkpointModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 600px;">
|
<div class="pf-modal-content" style="max-width: 600px;">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||||
@@ -371,6 +337,12 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<button type="button" onclick="closeCheckpointModal()" class="pf-modal-close">×</button>
|
<button type="button" onclick="closeCheckpointModal()" class="pf-modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- LES ONGLETS SONT ICI -->
|
||||||
|
<div style="display: flex; gap: 15px; border-bottom: 1px solid var(--border-light); margin-bottom: 20px;">
|
||||||
|
<button type="button" id="tabBtnInfo" onclick="switchCpTab('info')" style="background: none; border: none; font-weight: bold; font-size: 1rem; color: var(--primary); border-bottom: 2px solid var(--primary); padding-bottom: 8px; cursor: pointer; flex: 1;">📍 Infos de l'étape</button>
|
||||||
|
<button type="button" id="tabBtnProg" onclick="switchCpTab('prog')" style="background: none; border: none; font-weight: bold; font-size: 1rem; color: var(--text-muted); border-bottom: 2px solid transparent; padding-bottom: 8px; cursor: pointer; flex: 1;">🎟️ Programme & Frais</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="cpSearchBlock" style="margin-bottom:20px;">
|
<div id="cpSearchBlock" style="margin-bottom:20px;">
|
||||||
<?php if (!empty($favorites)): ?>
|
<?php if (!empty($favorites)): ?>
|
||||||
<div style="margin-bottom:15px; display:flex; gap:8px; flex-wrap:wrap;">
|
<div style="margin-bottom:15px; display:flex; gap:8px; flex-wrap:wrap;">
|
||||||
@@ -390,6 +362,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<div id="searchResults" style="margin-top:10px; max-height:200px; overflow-y:auto;"></div>
|
<div id="searchResults" style="margin-top:10px; max-height:200px; overflow-y:auto;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- FORMULAIRE (Le contenu apparaîtra ici) -->
|
||||||
<form action="/modules/holidays/includes/api/save_checkpoint.php" method="POST" id="formCheckpoint" style="display:none; border-top:1px solid #e2e8f0; padding-top:20px;">
|
<form action="/modules/holidays/includes/api/save_checkpoint.php" method="POST" id="formCheckpoint" style="display:none; border-top:1px solid #e2e8f0; padding-top:20px;">
|
||||||
<input type="hidden" name="holiday_id" value="<?= $id ?>">
|
<input type="hidden" name="holiday_id" value="<?= $id ?>">
|
||||||
<input type="hidden" name="old_location_name" id="cp_old_name">
|
<input type="hidden" name="old_location_name" id="cp_old_name">
|
||||||
@@ -397,6 +370,8 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<input type="hidden" name="lng" id="cp_lng">
|
<input type="hidden" name="lng" id="cp_lng">
|
||||||
<input type="hidden" name="old_sort_order" id="cp_old_sort_order">
|
<input type="hidden" name="old_sort_order" id="cp_old_sort_order">
|
||||||
|
|
||||||
|
<!-- CONTENU DE L'ONGLET INFO -->
|
||||||
|
<div id="cpTabInfo">
|
||||||
<div class="form-group" style="margin-bottom:15px;">
|
<div class="form-group" style="margin-bottom:15px;">
|
||||||
<label class="pf-label"><?= tr('hdl_label_step_name') ?></label>
|
<label class="pf-label"><?= tr('hdl_label_step_name') ?></label>
|
||||||
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:var(--primary);" required>
|
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:var(--primary);" required>
|
||||||
@@ -429,13 +404,17 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
</label>
|
</label>
|
||||||
<p style="margin: 4px 0 0 24px; font-size: 0.75rem; color: #64748b;">La route sera tracée en orange à partir d'ici.</p>
|
<p style="margin: 4px 0 0 24px; font-size: 0.75rem; color: #64748b;">La route sera tracée en orange à partir d'ici.</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CONTENU DE L'ONGLET PROGRAMME & FRAIS -->
|
||||||
|
<div id="cpTabProg" class="hol-hidden" style="display: none;">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
|
||||||
<label class="pf-label" style="margin:0;"><?= tr('hdl_planned_expenses') ?></label>
|
<label class="pf-label" style="margin:0;"><?= tr('hdl_planned_expenses') ?></label>
|
||||||
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="addCpExpenseLine()"><?= tr('hdl_btn_add_expense') ?></button>
|
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="addCpExpenseLine()"><?= tr('hdl_btn_add_expense') ?></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="cpExpensesContainer" style="margin-bottom:15px;"></div>
|
<div id="cpExpensesContainer" style="margin-bottom:15px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 20px;">
|
<div style="margin-bottom: 20px;">
|
||||||
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.85rem; color:#475569;">
|
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.85rem; color:#475569;">
|
||||||
@@ -454,12 +433,15 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="planningModal" class="pf-modal">
|
<div id="planningModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 550px;">
|
<div class="pf-modal-content hol-modal-planning">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
<div class="hol-modal-header">
|
||||||
<h3 id="planningModalTitle" style="margin:0; color:var(--primary);">📅 <?= tr('hdl_planning_title') ?></h3>
|
<h3 id="planningModalTitle" class="hol-modal-title">📅 Planning Global</h3>
|
||||||
<button type="button" onclick="closePlanningModal()" class="pf-modal-close">×</button>
|
<button type="button" onclick="closePlanningModal()" class="pf-modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="planningContainer" style="width: 100%;"></div>
|
|
||||||
|
<!-- Conteneur Injecté en JS -->
|
||||||
|
<div id="planningContainer" class="hol-planning-container"></div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" onclick="closePlanningModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
<button type="button" onclick="closePlanningModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -467,41 +449,40 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="transitModal" class="pf-modal">
|
<div id="transitModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 500px;">
|
<div class="pf-modal-content hol-modal-sm">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-bottom: 1px solid var(--border-light); padding-bottom: 10px;">
|
<div class="hol-modal-header">
|
||||||
<h3 style="margin:0; color:var(--text-main);">🛣️ Détail des trajets</h3>
|
<h3 class="hol-modal-title">🛣️ Détail des trajets</h3>
|
||||||
<button type="button" onclick="closeTransitModal()" class="pf-modal-close">×</button>
|
<button type="button" onclick="closeTransitModal()" class="pf-modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="transitDetailsContainer" style="max-height: 60vh; overflow-y: auto; padding-right: 5px;">
|
<div id="transitDetailsContainer" class="hol-transit-container">
|
||||||
<p style="text-align:center; color:var(--text-muted); font-style:italic;">Calcul en cours...</p>
|
<p class="hol-empty-text">Calcul en cours...</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer" style="padding-top:15px; border-top:1px solid var(--border-light); margin-top:15px;">
|
<div class="modal-footer">
|
||||||
<button type="button" onclick="closeTransitModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
<button type="button" onclick="closeTransitModal()" class="pf-btn btn-secondary"><?= tr('btn_close') ?></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="gpsModal" class="pf-modal">
|
<div id="gpsModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 320px; height: auto !important; min-height: 0 !important; text-align: center; padding: 20px; margin: auto;">
|
<div class="pf-modal-content hol-modal-gps">
|
||||||
|
<div class="hol-modal-header">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
<h3 class="hol-modal-title">🧭 Y aller avec...</h3>
|
||||||
<h3 style="margin:0; font-size: 1.2rem; color: var(--text-main);">🧭 Y aller avec...</h3>
|
|
||||||
<button type="button" onclick="closeGpsModal()" class="pf-modal-close">×</button>
|
<button type="button" onclick="closeGpsModal()" class="pf-modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
<div class="hol-gps-buttons">
|
||||||
<button onclick="launchGpsApp('waze')" class="pf-btn btn-secondary" style="font-size: 1.1rem; padding: 12px; display:flex; align-items:center; justify-content:center; gap:10px;">
|
<button onclick="launchGpsApp('waze')" class="pf-btn btn-secondary hol-gps-btn">
|
||||||
<img src="https://cdn.simpleicons.org/waze/05C8F0" alt="Waze" style="width: 24px; height: 24px;">
|
<img src="https://cdn.simpleicons.org/waze/05C8F0" alt="Waze" class="hol-gps-icon">
|
||||||
Waze
|
Waze
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onclick="launchGpsApp('gmaps')" class="pf-btn btn-secondary" style="font-size: 1.1rem; padding: 12px; display:flex; align-items:center; justify-content:center; gap:10px;">
|
<button onclick="launchGpsApp('gmaps')" class="pf-btn btn-secondary hol-gps-btn">
|
||||||
<img src="https://cdn.simpleicons.org/googlemaps" alt="Google Maps" style="width: 24px; height: 24px;">
|
<img src="https://cdn.simpleicons.org/googlemaps" alt="Google Maps" class="hol-gps-icon">
|
||||||
Google Maps
|
Google Maps
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onclick="launchGpsApp('amaps')" class="pf-btn btn-secondary" style="font-size: 1.1rem; padding: 12px; display:flex; align-items:center; justify-content:center; gap:10px;">
|
<button onclick="launchGpsApp('amaps')" class="pf-btn btn-secondary hol-gps-btn">
|
||||||
<img src="https://cdn.simpleicons.org/apple/000000" alt="Apple Maps" style="width: 24px; height: 24px;">
|
<img src="https://cdn.simpleicons.org/apple/000000" alt="Apple Maps" class="hol-gps-icon">
|
||||||
Apple Maps
|
Apple Maps
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -509,31 +490,35 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="docsModal" class="pf-modal">
|
<div id="docsModal" class="pf-modal">
|
||||||
<div class="pf-modal-content" style="max-width: 450px; padding: 20px;">
|
<div class="pf-modal-content hol-modal-docs">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-bottom: 1px solid var(--border-light); padding-bottom: 10px;">
|
<div class="hol-modal-header">
|
||||||
<h3 style="margin:0; font-size: 1.2rem; color: var(--text-main);">📎 Porte-documents</h3>
|
<h3 class="hol-modal-title">📎 Porte-documents</h3>
|
||||||
<button type="button" onclick="closeDocsModal()" class="pf-modal-close">×</button>
|
<button type="button" onclick="closeDocsModal()" class="pf-modal-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; padding: 20px; background: var(--bg-page); border: 2px dashed var(--border-light); border-radius: 8px; margin-bottom: 15px;">
|
<div class="hol-docs-dropzone">
|
||||||
<p style="margin-top:0; color: var(--text-muted); font-size: 0.9rem;">Ajoutez vos billets, réservations ou PDFs pour cette étape.</p>
|
<p class="hol-docs-hint">Ajoutez vos billets, réservations ou PDFs pour cette étape.</p>
|
||||||
|
|
||||||
<input type="file" id="docFileInput" style="display: none;" accept=".pdf,.png,.jpg,.jpeg" onchange="handleFileUpload(this)">
|
<input type="file" id="docFileInput" class="hol-hidden" accept=".pdf,.png,.jpg,.jpeg" onchange="handleFileUpload(this)">
|
||||||
|
|
||||||
<button type="button" class="pf-btn btn-primary" onclick="document.getElementById('docFileInput').click()" style="margin: 10px 0;">
|
<button type="button" class="pf-btn btn-primary hol-docs-select-btn" onclick="document.getElementById('docFileInput').click()">
|
||||||
+ Sélectionner un fichier
|
+ Sélectionner un fichier
|
||||||
</button>
|
</button>
|
||||||
<div id="uploadStatus" style="font-size: 0.85rem; font-weight: bold; margin-top: 10px;"></div>
|
<div id="uploadStatus" class="hol-upload-status"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="docsListContainer" style="display: flex; flex-direction: column; gap: 8px;">
|
<div id="docsListContainer" class="hol-docs-list">
|
||||||
<p style="text-align: center; font-size: 0.85rem; color: var(--text-muted); font-style: italic;">Aucun document pour cette étape.</p>
|
<p class="hol-empty-text">Aucun document pour cette étape.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php include __DIR__ . '/modal.php'; ?>
|
<?php include __DIR__ . '/modal.php'; ?>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||||
|
<?php include __DIR__ . '/pdf_template.php'; ?>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// --- 1. SÉCURISATION TRADUCTIONS ET VARIABLES ---
|
// --- 1. SÉCURISATION TRADUCTIONS ET VARIABLES ---
|
||||||
window.MAP_POINTS = <?= json_encode($mapPoints ?? []) ?>;
|
window.MAP_POINTS = <?= json_encode($mapPoints ?? []) ?>;
|
||||||
@@ -541,62 +526,281 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
|
|
||||||
window.I18N = {
|
window.I18N = {
|
||||||
...(window.I18N || {}),
|
...(window.I18N || {}),
|
||||||
'hdl_js_search_loading': "<?= tr('hdl_js_search_loading') ?>",
|
'hdl_js_search_loading': <?= json_encode(tr('hdl_js_search_loading')) ?>,
|
||||||
'hdl_js_no_result': "<?= tr('hdl_js_no_result') ?>",
|
'hdl_js_no_result': <?= json_encode(tr('hdl_js_no_result')) ?>,
|
||||||
'hdl_js_confirm_del_trip': "<?= tr('hdl_js_confirm_del_trip') ?>",
|
'hdl_js_confirm_del_trip': <?= json_encode(tr('hdl_js_confirm_del_trip')) ?>,
|
||||||
'hdl_js_confirm_del_step': "<?= tr('hdl_js_confirm_del_step') ?>",
|
'hdl_js_confirm_del_step': <?= json_encode(tr('hdl_js_confirm_del_step')) ?>,
|
||||||
'hdl_js_step_label': "<?= tr('hdl_js_step_label') ?>",
|
'hdl_js_step_label': <?= json_encode(tr('hdl_js_step_label')) ?>,
|
||||||
'hdl_js_ph_expense_name': "<?= tr('hdl_js_ph_expense_name') ?>",
|
'hdl_js_ph_expense_name': <?= json_encode(tr('hdl_js_ph_expense_name')) ?>,
|
||||||
'hdl_js_delete_line': "<?= tr('btn_delete') ?>",
|
'hdl_js_delete_line': <?= json_encode(tr('btn_delete')) ?>,
|
||||||
'hdl_planning_title': "<?= tr('hdl_planning_title') ?>",
|
'hdl_planning_title': <?= json_encode(tr('hdl_planning_title')) ?>,
|
||||||
'hdl_to_place': "<?= tr('hdl_to_place') ?>",
|
'hdl_to_place': <?= json_encode(tr('hdl_to_place')) ?>,
|
||||||
'hdl_js_missing_dates_title': "<?= tr('hdl_js_missing_dates_title') ?>",
|
'hdl_js_missing_dates_title': <?= json_encode(tr('hdl_js_missing_dates_title')) ?>,
|
||||||
'hdl_js_missing_dates_msg': "<?= tr('hdl_js_missing_dates_msg') ?>",
|
'hdl_js_missing_dates_msg': <?= json_encode(tr('hdl_js_missing_dates_msg')) ?>,
|
||||||
'hdl_modal_title': "<?= tr('hdl_modal_title') ?>",
|
'hdl_modal_title': <?= json_encode(tr('hdl_modal_title')) ?>,
|
||||||
'hdl_js_edit_step': "<?= tr('hdl_js_edit_step') ?>",
|
'hdl_js_edit_step': <?= json_encode(tr('hdl_js_edit_step')) ?>,
|
||||||
'hdl_ph_notes': "<?= tr('hdl_ph_notes') ?>",
|
'hdl_ph_notes': <?= json_encode(tr('hdl_ph_notes')) ?>,
|
||||||
'hdl_btn_add_step': "<?= tr('hdl_btn_add_step') ?>",
|
'hdl_btn_add_step': <?= json_encode(tr('hdl_btn_add_step')) ?>,
|
||||||
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>",
|
'hdl_quick_edit_title': <?= json_encode(tr('hdl_quick_edit_title')) ?>,
|
||||||
'hdl_paid': "<?= tr('hdl_paid') ?>",
|
'hdl_paid': <?= json_encode(tr('hdl_paid')) ?>,
|
||||||
|
'weather_sunny': <?= json_encode(tr('weather_sunny')) ?>,
|
||||||
// --- NOUVELLES CLÉS MÉTÉO ICI ---
|
'weather_cloudy': <?= json_encode(tr('weather_cloudy')) ?>,
|
||||||
'weather_sunny': "<?= tr('weather_sunny') ?>",
|
'weather_rainy': <?= json_encode(tr('weather_rainy')) ?>,
|
||||||
'weather_cloudy': "<?= tr('weather_cloudy') ?>",
|
'weather_snowy': <?= json_encode(tr('weather_snowy')) ?>,
|
||||||
'weather_rainy': "<?= tr('weather_rainy') ?>",
|
'weather_forecast': <?= json_encode(tr('weather_forecast')) ?>,
|
||||||
'weather_snowy': "<?= tr('weather_snowy') ?>",
|
'weather_historical': <?= json_encode(tr('weather_historical')) ?>
|
||||||
'weather_forecast': "<?= tr('weather_forecast') ?>",
|
|
||||||
'weather_historical': "<?= tr('weather_historical') ?>"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fallback de sécurité pour s'assurer que les modales peuvent toujours se fermer
|
const savedFuelPrice = localStorage.getItem('holidays_fuel_price') || 1.85;
|
||||||
window.closeCheckpointModal = window.closeCheckpointModal || function() {
|
window.FUEL_PRICE = parseFloat(savedFuelPrice);
|
||||||
|
|
||||||
|
const displayFuelEl = document.getElementById('display_fuel_price');
|
||||||
|
if (displayFuelEl) displayFuelEl.innerText = window.FUEL_PRICE.toFixed(2);
|
||||||
|
|
||||||
|
window.VEHICLE_CONSUMPTION = <?= !empty($holiday['vehicle_consumption']) ? (float)$holiday['vehicle_consumption'] : 7 ?>;
|
||||||
|
window.GLOBAL_RETURN_STEP_ID = <?= $holiday['return_step_id'] !== null ? $holiday['return_step_id'] : 'null' ?>;
|
||||||
|
|
||||||
|
// --- 2. SURCHARGE SÉCURISÉE DES FONCTIONS JS (APRÈS CHARGEMENT DE HOLIDAYS.JS) ---
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
|
// 1. Correction du conflit d'onglet
|
||||||
|
window.switchCpTab = function(tabId) {
|
||||||
|
const btnInfo = document.getElementById('tabBtnInfo');
|
||||||
|
const btnProg = document.getElementById('tabBtnProg');
|
||||||
|
const tabInfo = document.getElementById('cpTabInfo');
|
||||||
|
const tabProg = document.getElementById('cpTabProg');
|
||||||
|
|
||||||
|
if (tabId === 'info') {
|
||||||
|
if (btnInfo) { btnInfo.style.borderBottomColor = "var(--primary)"; btnInfo.style.color = "var(--primary)"; }
|
||||||
|
if (btnProg) { btnProg.style.borderBottomColor = "transparent"; btnProg.style.color = "var(--text-muted)"; }
|
||||||
|
if (tabInfo) { tabInfo.style.display = "block"; tabInfo.classList.remove("hol-hidden"); }
|
||||||
|
if (tabProg) { tabProg.style.display = "none"; tabProg.classList.add("hol-hidden"); }
|
||||||
|
} else {
|
||||||
|
if (btnProg) { btnProg.style.borderBottomColor = "var(--primary)"; btnProg.style.color = "var(--primary)"; }
|
||||||
|
if (btnInfo) { btnInfo.style.borderBottomColor = "transparent"; btnInfo.style.color = "var(--text-muted)"; }
|
||||||
|
if (tabProg) { tabProg.style.display = "block"; tabProg.classList.remove("hol-hidden"); }
|
||||||
|
if (tabInfo) { tabInfo.style.display = "none"; tabInfo.classList.add("hol-hidden"); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Correction modale : ajout de l'injection lat/lng aux items
|
||||||
|
window.openCheckpointModal = function(mode, data = null) {
|
||||||
|
const searchBlock = document.getElementById("cpSearchBlock");
|
||||||
|
const formBlock = document.getElementById("formCheckpoint");
|
||||||
|
const container = document.getElementById("cpExpensesContainer");
|
||||||
|
const btnDel = document.getElementById("btnDeleteCp");
|
||||||
|
const modalTitle = document.getElementById("cpModalTitle");
|
||||||
|
|
||||||
|
if(container) container.innerHTML = "";
|
||||||
|
if (document.getElementById("cp_start_date")) document.getElementById("cp_start_date").value = "";
|
||||||
|
if (document.getElementById("cp_end_date")) document.getElementById("cp_end_date").value = "";
|
||||||
|
if (document.getElementById("searchPlaceInput")) document.getElementById("searchPlaceInput").value = "";
|
||||||
|
if (document.getElementById("searchResults")) document.getElementById("searchResults").innerHTML = "";
|
||||||
|
|
||||||
|
window.switchCpTab('info');
|
||||||
|
|
||||||
|
if (mode === "add") {
|
||||||
|
if (modalTitle) modalTitle.innerText = "📍 " + (window.I18N['hdl_btn_add_step'] || "Ajouter une étape");
|
||||||
|
if (searchBlock) searchBlock.style.display = "block";
|
||||||
|
if (formBlock) formBlock.style.display = "block";
|
||||||
|
if (btnDel) btnDel.style.display = "none";
|
||||||
|
|
||||||
|
if (document.getElementById("cp_old_sort_order")) document.getElementById("cp_old_sort_order").value = "";
|
||||||
|
if (document.getElementById("cp_name")) document.getElementById("cp_name").value = "";
|
||||||
|
|
||||||
|
if (document.getElementById("cp_step_type")) {
|
||||||
|
document.getElementById("cp_step_type").value = "stop";
|
||||||
|
if (typeof toggleStepDates === 'function') toggleStepDates("stop");
|
||||||
|
}
|
||||||
|
if (document.getElementById("cp_set_as_return")) document.getElementById("cp_set_as_return").checked = false;
|
||||||
|
|
||||||
|
if (typeof window.addCpExpenseLine === 'function') window.addCpExpenseLine();
|
||||||
|
|
||||||
|
} else if (mode === "edit" && data) {
|
||||||
|
if (modalTitle) modalTitle.innerText = "✏️ " + (window.I18N['hdl_js_edit_step'] || "Modifier l'étape");
|
||||||
|
if (searchBlock) searchBlock.style.display = "none";
|
||||||
|
if (formBlock) formBlock.style.display = "block";
|
||||||
|
if (btnDel) btnDel.style.display = "block";
|
||||||
|
|
||||||
|
if (document.getElementById("cp_lat")) document.getElementById("cp_lat").value = data.lat;
|
||||||
|
if (document.getElementById("cp_lng")) document.getElementById("cp_lng").value = data.lng;
|
||||||
|
if (document.getElementById("cp_old_sort_order")) document.getElementById("cp_old_sort_order").value = data.sort_order;
|
||||||
|
if (document.getElementById("cp_name")) document.getElementById("cp_name").value = data.location_name;
|
||||||
|
if (document.getElementById("cp_start_date")) document.getElementById("cp_start_date").value = data.step_start_date || "";
|
||||||
|
if (document.getElementById("cp_end_date")) document.getElementById("cp_end_date").value = data.step_end_date || "";
|
||||||
|
|
||||||
|
if (document.getElementById("cp_step_type")) {
|
||||||
|
const type = data.step_type || "stop";
|
||||||
|
document.getElementById("cp_step_type").value = type;
|
||||||
|
if (typeof toggleStepDates === 'function') toggleStepDates(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.getElementById("cp_set_as_return")) {
|
||||||
|
document.getElementById("cp_set_as_return").checked = (window.GLOBAL_RETURN_STEP_ID == data.sort_order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Génération avec transmission de la latitude et longitude de chaque dépense
|
||||||
|
if (data.items && data.items.length > 0) {
|
||||||
|
let visibleCount = 0;
|
||||||
|
data.items.forEach((it) => {
|
||||||
|
if (it.name !== "PF_TECHNICAL_POINT") {
|
||||||
|
if (typeof window.addCpExpenseLine === 'function') {
|
||||||
|
window.addCpExpenseLine(
|
||||||
|
it.category, it.name, it.amount, it.is_paid, it.notes || "",
|
||||||
|
it.id || "", it.item_date || "", it.item_time || "", it.duration || 1,
|
||||||
|
it.expense_context || "local", it.lat || "", it.lng || ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
visibleCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (visibleCount === 0 && typeof window.addCpExpenseLine === 'function') window.addCpExpenseLine();
|
||||||
|
} else {
|
||||||
|
if (typeof window.addCpExpenseLine === 'function') window.addCpExpenseLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof cpDateRangePicker !== 'undefined' && cpDateRangePicker && typeof cpDateRangePicker.destroy === 'function') {
|
||||||
|
cpDateRangePicker.destroy();
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const modal = document.getElementById("checkpointModal");
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = "flex";
|
||||||
|
document.body.classList.add("no-scroll");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. LA FONCTION AVEC LA CLASSE CSS PARFAITE ET LE BOUTON 📍
|
||||||
|
window.addCpExpenseLine = function(category = "accommodation", name = "", amount = "", isPaid = 0, notes = "", itemId = "", itemDate = "", itemTime = "", itemDur = 1, expenseContext = "local", itemLat = "", itemLng = "") {
|
||||||
|
const container = document.getElementById("cpExpensesContainer");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "hol-form-row"; // Force l'utilisation du CSS
|
||||||
|
const isChecked = isPaid == 1 ? "checked" : "";
|
||||||
|
|
||||||
|
// Indicateur visuel (bouton vert) si on a déjà des coordonnées pour cette activité
|
||||||
|
const pinBtnStyle = (itemLat && itemLng) ? "background:var(--success);color:white;border-color:var(--success);" : "";
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="hol-form-inner">
|
||||||
|
<select name="items[cat][]" class="pf-input hol-form-select">
|
||||||
|
<option value="accommodation" ${category === "accommodation" ? "selected" : ""}>🏨</option>
|
||||||
|
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
|
||||||
|
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
|
||||||
|
</select>
|
||||||
|
<input type="hidden" name="items[context][]" value="${expenseContext}">
|
||||||
|
|
||||||
|
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="${window.I18N['hdl_js_ph_expense_name'] || 'Nom de la dépense'}" value="${name}" required>
|
||||||
|
|
||||||
|
<button type="button" class="btn-icon-action" style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border-light);border-radius:6px; margin:0; flex-shrink:0; ${pinBtnStyle}" onclick="window.toggleItemSearch(this)" title="Adresse précise">📍</button>
|
||||||
|
|
||||||
|
<input type="number" step="0.01" name="items[amount][]" class="pf-input hol-form-number" placeholder="0.00" value="${amount}" required>
|
||||||
|
|
||||||
|
<label class="hol-form-paid-label" title="${window.I18N['hdl_paid'] || 'Payé'}">
|
||||||
|
<input type="checkbox" ${isChecked} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="width:16px;height:16px;margin:0;accent-color:var(--primary);">
|
||||||
|
<input type="hidden" name="items[paid][]" value="${isPaid}">
|
||||||
|
<span class="hol-form-paid-text">Payé</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="button" onclick="this.parentElement.parentElement.remove()" title="${window.I18N['hdl_js_delete_line'] || 'Supprimer'}" class="btn-icon-action delete btn-remove-expense" style="margin: 0;">🗑️</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-item-search-box" style="display:none; padding: 10px; background: var(--bg-subtle); border-radius: 8px; margin-top: 6px; width: 100%;">
|
||||||
|
<div style="display:flex; gap:10px;">
|
||||||
|
<input type="text" class="pf-input item-search-input" placeholder="Chercher une adresse..." onkeypress="if(event.key === 'Enter') { window.searchItemPlace(this); return false; }">
|
||||||
|
<button type="button" class="pf-btn btn-secondary" onclick="window.searchItemPlace(this)">🔍</button>
|
||||||
|
</div>
|
||||||
|
<div class="item-search-results" style="margin-top:10px; max-height:150px; overflow-y:auto;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hol-form-subrow" style="display: flex; padding-left: 58px; margin-top: 6px;">
|
||||||
|
<input type="text" name="items[notes][]" class="pf-input hol-form-notes-input" placeholder="${window.I18N['hdl_ph_notes'] || 'Notes...'}" value="${notes}" style="font-size: 0.8rem; padding: 6px 8px; border-style: dashed; color: var(--text-muted); width: 100%; background: transparent; margin: 0;">
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="items[id][]" value="${itemId}">
|
||||||
|
<input type="hidden" name="items[date][]" value="${itemDate}">
|
||||||
|
<input type="hidden" name="items[time][]" value="${itemTime}">
|
||||||
|
<input type="hidden" name="items[duration][]" value="${itemDur}">
|
||||||
|
<input type="hidden" name="items[lat][]" value="${itemLat}">
|
||||||
|
<input type="hidden" name="items[lng][]" value="${itemLng}">
|
||||||
|
`;
|
||||||
|
container.appendChild(div);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. RÉINTÉGRATION DES FONCTIONS DE GÉOCODAGE DANS LE MÊME SCOPE
|
||||||
|
window.toggleItemSearch = function(btn) {
|
||||||
|
const searchBox = btn.closest(".hol-form-row").querySelector(".hol-item-search-box");
|
||||||
|
searchBox.style.display = searchBox.style.display === "none" ? "block" : "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
window.searchItemPlace = async function(btn) {
|
||||||
|
const container = btn.closest(".hol-item-search-box");
|
||||||
|
const input = container.querySelector(".item-search-input");
|
||||||
|
const resultsDiv = container.querySelector(".item-search-results");
|
||||||
|
const q = input.value.trim();
|
||||||
|
if (q.length < 3) return;
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = `<span style="color:#64748b; font-size:0.85rem;">Recherche en cours...</span>`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await pachaFetch("/modules/holidays/includes/api/geocode.php?limit=5&q=" + encodeURIComponent(q), { method: 'GET' });
|
||||||
|
resultsDiv.innerHTML = "";
|
||||||
|
if (data.error || !data.results || data.results.length === 0) {
|
||||||
|
resultsDiv.innerHTML = `<span style="color:#ef4444; font-size:0.85rem;">Aucun résultat</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.results.forEach((place) => {
|
||||||
|
const b = document.createElement("button");
|
||||||
|
b.type = "button";
|
||||||
|
b.className = "pf-btn btn-secondary";
|
||||||
|
b.style.textAlign = "left";
|
||||||
|
b.style.padding = "8px";
|
||||||
|
b.style.marginBottom = "4px";
|
||||||
|
b.style.width = "100%";
|
||||||
|
b.innerText = "📍 " + place.display_name;
|
||||||
|
b.onclick = () => window.selectItemPlace(container, place.lat, place.lng, place.display_name);
|
||||||
|
resultsDiv.appendChild(b);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
resultsDiv.innerHTML = `<span style="color:#ef4444; font-size:0.85rem;">Erreur de connexion</span>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selectItemPlace = function(container, lat, lng, displayName) {
|
||||||
|
const row = container.closest(".hol-form-row");
|
||||||
|
row.querySelector('input[name="items[lat][]"]').value = lat;
|
||||||
|
row.querySelector('input[name="items[lng][]"]').value = lng;
|
||||||
|
|
||||||
|
// Si le champ note est vide, on l'utilise pour y placer le nom complet de l'adresse
|
||||||
|
const notesInput = row.querySelector('input[name="items[notes][]"]');
|
||||||
|
if (notesInput.value === "") {
|
||||||
|
notesInput.value = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.style.display = "none";
|
||||||
|
|
||||||
|
// Le bouton devient vert pour confirmer la présence de coordonnées
|
||||||
|
const pinBtn = row.querySelector('.btn-icon-action[title="Adresse précise"]');
|
||||||
|
if (pinBtn) {
|
||||||
|
pinBtn.style.background = "var(--success)";
|
||||||
|
pinBtn.style.color = "white";
|
||||||
|
pinBtn.style.borderColor = "var(--success)";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeCheckpointModal = function() {
|
||||||
const modal = document.getElementById('checkpointModal');
|
const modal = document.getElementById('checkpointModal');
|
||||||
if(modal) modal.style.display = 'none';
|
if(modal) modal.style.display = 'none';
|
||||||
document.body.classList.remove('no-scroll');
|
document.body.classList.remove('no-scroll');
|
||||||
};
|
};
|
||||||
|
|
||||||
window.closePlanningModal = window.closePlanningModal || function() {
|
window.closePlanningModal = function() {
|
||||||
const modal = document.getElementById('planningModal');
|
const modal = document.getElementById('planningModal');
|
||||||
if(modal) modal.style.display = 'none';
|
if(modal) modal.style.display = 'none';
|
||||||
document.body.classList.remove('no-scroll');
|
document.body.classList.remove('no-scroll');
|
||||||
};
|
};
|
||||||
|
});
|
||||||
// 1. On charge le prix depuis le navigateur (ou 1.85 par défaut)
|
|
||||||
const savedFuelPrice = localStorage.getItem('holidays_fuel_price') || 1.85;
|
|
||||||
window.FUEL_PRICE = parseFloat(savedFuelPrice);
|
|
||||||
|
|
||||||
// 2. On met à jour le texte du petit bouton "✏️" en haut de la page
|
|
||||||
const displayFuelEl = document.getElementById('display_fuel_price');
|
|
||||||
if (displayFuelEl) displayFuelEl.innerText = window.FUEL_PRICE.toFixed(2);
|
|
||||||
|
|
||||||
// 3. Variables voiture et retour
|
|
||||||
window.VEHICLE_CONSUMPTION = <?= !empty($holiday['vehicle_consumption']) ? (float)$holiday['vehicle_consumption'] : 7 ?>;
|
|
||||||
window.GLOBAL_RETURN_STEP_ID = <?= $holiday['return_step_id'] !== null ? $holiday['return_step_id'] : 'null' ?>;
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
|
||||||
|
|
||||||
<?php include __DIR__ . '/pdf_template.php'; ?>
|
|
||||||
|
|
||||||
<script src="/modules/holidays/holidays.js?v=<?= time() ?>"></script>
|
|
||||||
@@ -122,9 +122,10 @@ window.closeHolidayModal = window.closeHolidayModal || function() {
|
|||||||
if(modal) modal.style.display = 'none';
|
if(modal) modal.style.display = 'none';
|
||||||
document.body.classList.remove('no-scroll');
|
document.body.classList.remove('no-scroll');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/modules/holidays/holidays.js"></script>
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
function renderHolidayCard($h, $pdo) {
|
function renderHolidayCard($h, $pdo) {
|
||||||
@@ -136,8 +137,9 @@ function renderHolidayCard($h, $pdo) {
|
|||||||
$dateDisplay = htmlspecialchars($h['period_hint'] ?? '');
|
$dateDisplay = htmlspecialchars($h['period_hint'] ?? '');
|
||||||
|
|
||||||
if (empty($dateDisplay) && $h['start_date']) {
|
if (empty($dateDisplay) && $h['start_date']) {
|
||||||
$dateDisplay = date('d/m/Y', strtotime($h['start_date']));
|
// 💡 Format Jour/Mois uniquement
|
||||||
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($h['end_date']));
|
$dateDisplay = date('d/m', strtotime($h['start_date']));
|
||||||
|
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m', strtotime($h['end_date']));
|
||||||
}
|
}
|
||||||
|
|
||||||
$statusClass = match($h['status']) {
|
$statusClass = match($h['status']) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<div id="holidayModal" class="pf-modal">
|
<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">
|
<form action="/modules/holidays/includes/api/save_holiday.php" method="POST" id="holidayForm">
|
||||||
<input type="hidden" name="id" id="inp_id">
|
<input type="hidden" name="id" id="inp_id">
|
||||||
@@ -23,23 +24,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div>
|
<div class="hol-flex-1">
|
||||||
<label class="pf-label"><?= tr('hdl_label_period') ?></label>
|
<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') ?>">
|
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="<?= tr('hdl_ph_period') ?>">
|
||||||
</div>
|
</div>
|
||||||
<div class="hol-date-range-group">
|
<!-- DATES AVEC FLATPICKR -->
|
||||||
<div class="hol-flex-1">
|
<div class="hol-flex-2">
|
||||||
<label class="pf-label"><?= tr('hdl_label_from') ?></label>
|
<label class="pf-label">📅 Dates du voyage</label>
|
||||||
<input type="date" name="start_date" id="inp_start" class="pf-input">
|
<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;">
|
||||||
</div>
|
<!-- Vraies données pour save_holiday.php -->
|
||||||
<div class="hol-flex-1">
|
<input type="hidden" name="start_date" id="inp_start">
|
||||||
<label class="pf-label"><?= tr('hdl_label_to') ?></label>
|
<input type="hidden" name="end_date" id="inp_end">
|
||||||
<input type="date" name="end_date" id="inp_end" class="pf-input">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<label class="pf-label" style="margin-bottom: 5px;">🚗 Véhicule utilisé (Optionnel)</label>
|
||||||
<select name="vehicle_id" id="inp_vehicle_id" class="pf-input">
|
<select name="vehicle_id" id="inp_vehicle_id" class="pf-input">
|
||||||
<option value="">-- Aucun / Autre transport --</option>
|
<option value="">-- Aucun / Autre transport --</option>
|
||||||
@@ -52,21 +51,16 @@
|
|||||||
<hr class="hol-divider">
|
<hr class="hol-divider">
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div>
|
<div class="hol-flex-1">
|
||||||
<label class="pf-label">🍔 <?= tr('hdl_label_budget_food') ?></label>
|
<label class="pf-label">🍔 <?= tr('hdl_label_budget_food') ?></label>
|
||||||
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
|
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="hol-flex-1">
|
||||||
<label class="pf-label">🎁 <?= tr('hdl_label_budget_extras') ?></label>
|
<label class="pf-label">🎁 <?= tr('hdl_label_budget_extras') ?></label>
|
||||||
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
|
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<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="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>
|
<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>
|
<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;">
|
<h2 style="font-size: 1.5rem; color: #64748b; font-weight: normal; margin-top: 0;">
|
||||||
<?php if (!empty($holiday['start_date']) && !empty($holiday['end_date'])): ?>
|
<?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: ?>
|
<?php else: ?>
|
||||||
Dates à définir
|
Dates à définir
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ function renderAttachmentsView(atts,noteId){
|
|||||||
<div class="memo-att-title">🖼️ Images</div>
|
<div class="memo-att-title">🖼️ Images</div>
|
||||||
<div class="att-img-grid">${images.map(a=>`
|
<div class="att-img-grid">${images.map(a=>`
|
||||||
<div class="att-img-item" onclick="openLightbox('${API}?action=file&id=${a.id}')">
|
<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>
|
<button class="att-del" onclick="event.stopPropagation();deleteAttachment(${a.id},${noteId})" title="Supprimer">✕</button>
|
||||||
</div>`).join('')}</div></div>`;
|
</div>`).join('')}</div></div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-15
@@ -26,33 +26,23 @@ $stops = $params['stops'];
|
|||||||
$fuelL100 = (float)($params['fuel_l100'] ?? 7);
|
$fuelL100 = (float)($params['fuel_l100'] ?? 7);
|
||||||
$fuelPrice= (float)($params['fuel_price'] ?? 1.85);
|
$fuelPrice= (float)($params['fuel_price'] ?? 1.85);
|
||||||
|
|
||||||
// --- Load databases ---
|
// --- Load pre-indexed PHP database ---
|
||||||
$tollsPath = __DIR__ . '/data/tolls.json';
|
$tollsDataPath = __DIR__ . '/data/tolls.data.php';
|
||||||
$gpsPath = __DIR__ . '/data/toll_gps.json';
|
$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']);
|
echo json_encode(['ok' => false, 'error' => 'Base de péages introuvable']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tollsRaw = json_decode(file_get_contents($tollsPath), true);
|
// Grâce à OPcache, le fichier est exécuté et stocké en RAM partagée
|
||||||
$tollData = $tollsRaw['data'] ?? [];
|
$tollIndex = require $tollsDataPath;
|
||||||
|
|
||||||
$gpsData = [];
|
$gpsData = [];
|
||||||
if (file_exists($gpsPath)) {
|
if (file_exists($gpsPath)) {
|
||||||
$gpsData = json_decode(file_get_contents($gpsPath), true) ?? [];
|
$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) ---
|
// --- Helper: great-circle distance (km) ---
|
||||||
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
|
||||||
$R = 6371.0;
|
$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