@@ -295,7 +295,32 @@ CREATE TABLE IF NOT EXISTS pf_holidays (
|
|||||||
status VARCHAR(50) DEFAULT 'draft',
|
status VARCHAR(50) DEFAULT 'draft',
|
||||||
budget_food DECIMAL(10,2) DEFAULT 0,
|
budget_food DECIMAL(10,2) DEFAULT 0,
|
||||||
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,
|
||||||
|
return_step_id INT DEFAULT NULL, -- 🔥 NOUVEAU : Point de bascule du retour
|
||||||
|
FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS pf_holidays_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
holiday_id INT NOT NULL,
|
||||||
|
category VARCHAR(100),
|
||||||
|
name VARCHAR(255),
|
||||||
|
amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
is_paid TINYINT(1) DEFAULT 0,
|
||||||
|
location_name VARCHAR(255) DEFAULT NULL,
|
||||||
|
lat DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
lng DECIMAL(10,7) DEFAULT NULL,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
notes TEXT DEFAULT NULL,
|
||||||
|
item_date DATE DEFAULT NULL,
|
||||||
|
item_time TIME DEFAULT NULL,
|
||||||
|
step_type VARCHAR(20) DEFAULT 'stop',
|
||||||
|
step_start_date DATE DEFAULT NULL,
|
||||||
|
step_end_date DATE DEFAULT NULL,
|
||||||
|
duration INT DEFAULT NULL,
|
||||||
|
expense_context VARCHAR(20) DEFAULT NULL,
|
||||||
|
FOREIGN KEY (holiday_id) REFERENCES pf_holidays(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pf_holidays_items (
|
CREATE TABLE IF NOT EXISTS pf_holidays_items (
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ $pageCss = "/modules/holidays/holidays.css";
|
|||||||
|
|
||||||
require __DIR__ . '/header.php';
|
require __DIR__ . '/header.php';
|
||||||
|
|
||||||
|
$stmtVehicles = $pdo->query("SELECT id, name FROM pf_vehicles ORDER BY name ASC");
|
||||||
|
$garageVehicles = $stmtVehicles->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
// 3. ROUTEUR DU MODULE VACANCES
|
// 3. ROUTEUR DU MODULE VACANCES
|
||||||
if ($tab === 'holiday_detail' && isset($_GET['id'])) {
|
if ($tab === 'holiday_detail' && isset($_GET['id'])) {
|
||||||
// Si on demande le détail ET qu'un ID est fourni
|
// Si on demande le détail ET qu'un ID est fourni
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/meta_db.php';
|
||||||
|
|
||||||
|
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
|
$db_user = getenv('DB_USER') ?: 'househub';
|
||||||
|
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
|
||||||
|
|
||||||
|
echo "<h1>🗺️ Migration : Ajout du Véhicule aux Voyages</h1><ul>";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
|
||||||
|
$families = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($families as $family) {
|
||||||
|
$dbName = $family['db_name'];
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
|
||||||
|
// Ajout de la colonne vehicle_id
|
||||||
|
$pdo->exec("ALTER TABLE pf_holidays ADD COLUMN vehicle_id INT DEFAULT NULL");
|
||||||
|
|
||||||
|
// Optionnel mais propre : On ajoute une clé étrangère
|
||||||
|
$pdo->exec("ALTER TABLE pf_holidays ADD CONSTRAINT fk_holiday_vehicle FOREIGN KEY (vehicle_id) REFERENCES pf_vehicles(id) ON DELETE SET NULL");
|
||||||
|
|
||||||
|
echo "<li>✅ {$family['name']} : Colonne vehicle_id ajoutée !</li>";
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
if ($e->getCode() == '42S21' || strpos($e->getMessage(), 'Duplicate column') !== false) {
|
||||||
|
echo "<li>⏩ {$family['name']} : Déjà à jour.</li>";
|
||||||
|
} else {
|
||||||
|
echo "<li>❌ {$family['name']} : Erreur - " . $e->getMessage() . "</li>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "</ul><h2>🎉 Migration terminée !</h2>";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
die("Erreur fatale : " . $e->getMessage());
|
||||||
|
}
|
||||||
|
?>
|
||||||
+26
-159
@@ -1,70 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* Script de migration global HouseHub OS (Multi-tenant)
|
|
||||||
* Auto-Sync dynamique basé sur schema_family.sql + Migration des données
|
|
||||||
* À exécuter via le navigateur : http://localhost:8083/migrate.php
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/includes/meta_db.php';
|
require_once __DIR__ . '/includes/meta_db.php';
|
||||||
|
|
||||||
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
|
$db_host = getenv('DB_HOST') ?: '127.0.0.1';
|
||||||
$db_user = getenv('DB_USER') ?: 'househub';
|
$db_user = getenv('DB_USER') ?: 'househub';
|
||||||
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
|
$db_pass = getenv('DB_PASS') ?: 'househub_dev';
|
||||||
|
|
||||||
echo "<h1>🚀 Début de la migration Multi-Tenant (Auto-Sync)</h1>";
|
echo "<h1>🗺️ Migration : Refonte Modèle Voyages</h1><ul>";
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
// 1. LECTURE ET PARSING DU FICHIER SCHEMA_FAMILY.SQL
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
$schemaPath = __DIR__ . '/schema_family.sql'; // Modifie si rangé dans /docker/
|
|
||||||
if (!file_exists($schemaPath)) {
|
|
||||||
$schemaPath = __DIR__ . '/docker/schema_family.sql';
|
|
||||||
}
|
|
||||||
if (!file_exists($schemaPath)) {
|
|
||||||
die("❌ Impossible de trouver le fichier schema_family.sql");
|
|
||||||
}
|
|
||||||
|
|
||||||
$sqlContent = file_get_contents($schemaPath);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyse le code SQL pour en extraire la structure [Table => [Colonnes]]
|
|
||||||
*/
|
|
||||||
function parseExpectedSchema($sql) {
|
|
||||||
$schema = [];
|
|
||||||
// Récupère tout ce qui se trouve entre CREATE TABLE (...) ENGINE
|
|
||||||
preg_match_all('/CREATE TABLE (?:IF NOT EXISTS )?`?([a-zA-Z0-9_]+)`?\s*\((.*?)\)\s*ENGINE/si', $sql, $tableMatches, PREG_SET_ORDER);
|
|
||||||
|
|
||||||
foreach($tableMatches as $match) {
|
|
||||||
$tableName = $match[1];
|
|
||||||
$body = $match[2];
|
|
||||||
|
|
||||||
// Sépare les lignes par virgule, en ignorant les virgules entre parenthèses (ex: DECIMAL(10,2) ou ENUM('a','b'))
|
|
||||||
$lines = preg_split('/,(?![^\(]*\))/', $body);
|
|
||||||
|
|
||||||
$columns = [];
|
|
||||||
foreach($lines as $line) {
|
|
||||||
$line = trim($line);
|
|
||||||
if (empty($line)) continue;
|
|
||||||
|
|
||||||
// On ignore la déclaration des clés, contraintes et index
|
|
||||||
if (preg_match('/^(PRIMARY KEY|UNIQUE KEY|FOREIGN KEY|KEY|INDEX|FULLTEXT KEY|CONSTRAINT)\b/i', $line)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extrait le nom de la colonne et sa définition SQL
|
|
||||||
if (preg_match('/^`?([a-zA-Z0-9_]+)`?\s+(.*)$/i', $line, $colMatch)) {
|
|
||||||
$colName = $colMatch[1];
|
|
||||||
$colDef = rtrim($colMatch[2], ',');
|
|
||||||
$columns[$colName] = $colDef;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$schema[$tableName] = $columns;
|
|
||||||
}
|
|
||||||
return $schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
$expectedSchema = parseExpectedSchema($sqlContent);
|
|
||||||
echo "ℹ️ Modèle SQL chargé et analysé : <b>" . count($expectedSchema) . " tables</b> détectées.<hr>";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
|
$stmt = $meta_pdo->query("SELECT db_name, name FROM families WHERE is_active = 1");
|
||||||
@@ -72,111 +13,37 @@ try {
|
|||||||
|
|
||||||
foreach ($families as $family) {
|
foreach ($families as $family) {
|
||||||
$dbName = $family['db_name'];
|
$dbName = $family['db_name'];
|
||||||
echo "<h2>🏡 Famille : {$family['name']} ($dbName)</h2>";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo = new PDO(
|
$pdo = new PDO("mysql:host=$db_host;dbname=$dbName;charset=utf8mb4", $db_user, $db_pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
"mysql:host=$db_host;dbname=$dbName;charset=utf8mb4",
|
|
||||||
$db_user, $db_pass,
|
|
||||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
|
||||||
);
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// 1. Mise à jour pf_holidays
|
||||||
// 2. EXÉCUTION DU SQL (CRÉATION TABLES MANQUANTES + DONNÉES PAR DÉFAUT)
|
$pdo->exec("ALTER TABLE pf_holidays ADD COLUMN return_step_id INT DEFAULT NULL");
|
||||||
// ---------------------------------------------------------
|
|
||||||
try {
|
|
||||||
$pdo->exec($sqlContent);
|
|
||||||
echo "✅ Structure de base validée (les tables manquantes ont été créées).<br>";
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo "⚠️ Avertissement SQL brut : " . $e->getMessage() . "<br>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// 2. Mise à jour pf_holidays_items
|
||||||
// 3. DIFFING (AJOUT DYNAMIQUE DES COLONNES MANQUANTES)
|
$pdo->exec("ALTER TABLE pf_holidays_items ADD COLUMN step_type VARCHAR(20) DEFAULT 'stop'");
|
||||||
// ---------------------------------------------------------
|
$pdo->exec("ALTER TABLE pf_holidays_items ADD COLUMN expense_context VARCHAR(20) DEFAULT NULL");
|
||||||
$colsAdded = 0;
|
|
||||||
foreach ($expectedSchema as $table => $expectedCols) {
|
|
||||||
$stmtCol = $pdo->query("SHOW COLUMNS FROM `$table`");
|
|
||||||
$existingCols = $stmtCol->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
|
|
||||||
foreach ($expectedCols as $colName => $colDef) {
|
// 3. Migration des données (is_return -> return_step_id)
|
||||||
if (!in_array($colName, $existingCols)) {
|
// On cherche la première étape cochée "is_return" et on l'assigne au voyage
|
||||||
$pdo->exec("ALTER TABLE `$table` ADD COLUMN `$colName` $colDef");
|
$pdo->exec("
|
||||||
echo "➕ Nouvelle colonne ajoutée : <b>$table.$colName</b><br>";
|
UPDATE pf_holidays h
|
||||||
$colsAdded++;
|
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) {
|
||||||
|
echo "<li>⚠️ {$family['name']} : " . $e->getMessage() . "</li>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
echo "</ul><h2>🎉 Migration terminée !</h2>";
|
||||||
if ($colsAdded === 0) {
|
|
||||||
echo "✅ Toutes les colonnes de toutes les tables sont déjà à jour.<br>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
// 4. MIGRATION DES DONNÉES (TRANSFORMATIONS HISTORIQUES)
|
|
||||||
// ---------------------------------------------------------
|
|
||||||
echo "<i>🔄 Exécution des transformations de données héritées...</i><br>";
|
|
||||||
$pdo->exec("ALTER TABLE pf_events MODIFY id INT NOT NULL AUTO_INCREMENT");
|
|
||||||
$pdo->exec("ALTER TABLE pf_leaves MODIFY id INT NOT NULL AUTO_INCREMENT");
|
|
||||||
|
|
||||||
// A. Changement de types spécifiques (Car le parseur n'ajoute que les colonnes manquantes)
|
|
||||||
$pdo->exec("ALTER TABLE pf_budget_items MODIFY category VARCHAR(100) DEFAULT NULL");
|
|
||||||
|
|
||||||
// B. Migration de l'historique calendrier (Enfants & Helpers)
|
|
||||||
$helperId = $pdo->query("SELECT id FROM pf_people WHERE role = 'helper' LIMIT 1")->fetchColumn();
|
|
||||||
if ($helperId) {
|
|
||||||
$pdo->exec("UPDATE pf_events SET event_type = 'HELPER_OFF', person_id = $helperId WHERE event_type = 'OFF_CAROLE'");
|
|
||||||
$pdo->exec("UPDATE pf_events SET event_type = 'HELPER_EXTRA', person_id = $helperId WHERE event_type = 'EXTRA_OFF_CAROLE'");
|
|
||||||
}
|
|
||||||
|
|
||||||
$kidId = $pdo->query("SELECT id FROM pf_people WHERE role IN ('child', 'enfant') ORDER BY id ASC LIMIT 1")->fetchColumn();
|
|
||||||
if ($kidId) {
|
|
||||||
$pdo->exec("UPDATE pf_events SET event_type = 'CHILD_SICK', person_id = $kidId WHERE event_type = 'PEP_SICK'");
|
|
||||||
}
|
|
||||||
|
|
||||||
// C. Mapping Intelligent du Budget (is_estimate)
|
|
||||||
$itemsMigres = 0;
|
|
||||||
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'FIXED' WHERE category = 'expense' AND is_estimate = 0");
|
|
||||||
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'FUEL' WHERE category = 'expense' AND is_estimate = 1 AND (mapping_keywords LIKE '%ESSENCE%' OR name LIKE '%gasolina%')");
|
|
||||||
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'SCHOOL' WHERE category = 'expense' AND is_estimate = 1 AND (mapping_keywords LIKE '%ESCOLA%' OR mapping_keywords LIKE '%PARASCOL%' OR name LIKE '%escola%')");
|
|
||||||
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'FMCG' WHERE category = 'expense' AND is_estimate = 1 AND (mapping_keywords LIKE '%FMCG%' OR name LIKE '%F&B%')");
|
|
||||||
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'AUTRES' WHERE category = 'expense'");
|
|
||||||
$itemsMigres += $pdo->exec("UPDATE pf_budget_items SET category = 'INCOME' WHERE category = 'income'");
|
|
||||||
|
|
||||||
// D. Nettoyage des virgules orphelines dans mapping_keywords
|
|
||||||
$budgetCodes = ['INCOME', 'FMCG', 'FUEL', 'SCHOOL', 'HEALTH', 'FIXED', 'SAVINGS', 'AUTRES'];
|
|
||||||
foreach ($budgetCodes as $code) {
|
|
||||||
$pdo->exec("UPDATE pf_budget_items SET mapping_keywords = REPLACE(mapping_keywords, '$code', '') WHERE mapping_keywords LIKE '%$code%'");
|
|
||||||
}
|
|
||||||
$pdo->exec("UPDATE pf_budget_items SET mapping_keywords = TRIM(BOTH ',' FROM REPLACE(REPLACE(mapping_keywords, ' ', ''), ',,', ','))");
|
|
||||||
|
|
||||||
// E. Migration de l'historique des dépenses et des règles d'import
|
|
||||||
$budgetMapping = [
|
|
||||||
'Income' => 'INCOME',
|
|
||||||
'FMCG' => 'FMCG',
|
|
||||||
'Essence' => 'FUEL',
|
|
||||||
'School' => 'SCHOOL',
|
|
||||||
'Frais' => 'FIXED',
|
|
||||||
'LivretA' => 'SAVINGS',
|
|
||||||
'Autres' => 'AUTRES'
|
|
||||||
];
|
|
||||||
$stmtExp = $pdo->prepare("UPDATE pf_expenses SET category = ? WHERE category = ?");
|
|
||||||
$stmtRules = $pdo->prepare("UPDATE pf_import_rules SET category = ? WHERE category = ?");
|
|
||||||
|
|
||||||
foreach ($budgetMapping as $old => $newCode) {
|
|
||||||
$stmtExp->execute([$newCode, $old]);
|
|
||||||
$stmtRules->execute([$newCode, $old]);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "✅ Transformations Data terminées ($itemsMigres prévisions remappées).<hr>";
|
|
||||||
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo "❌ Erreur sur la base $dbName : " . $e->getMessage() . "<hr>";
|
|
||||||
}
|
|
||||||
} // FIN DE LA BOUCLE FOREACH FAMILLES
|
|
||||||
|
|
||||||
echo "<h1>🎉 Synchronisation et Migration terminées avec succès !</h1>";
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
die("❌ Erreur fatale Meta DB : " . $e->getMessage());
|
die("Erreur fatale : " . $e->getMessage());
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -1400,8 +1400,88 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* --- Toll cost estimator panel --- */
|
/* --- Toll cost estimator panel --- */
|
||||||
.hol-cost-stat { background: var(--bg-page, #f8fafc); border: 1px solid var(--border-light, #e2e8f0); border-radius: 10px; padding: .75rem 1rem; text-align: center; }
|
.hol-cost-stat {
|
||||||
.hol-cost-stat-val { font-size: 1.2rem; font-weight: 700; color: var(--text-main, #0f172a); }
|
background: var(--bg-page, #f8fafc);
|
||||||
.hol-cost-stat-label { font-size: .72rem; color: var(--text-muted, #64748b); margin-top: 2px; text-transform: uppercase; letter-spacing: .04em; }
|
border: 1px solid var(--border-light, #e2e8f0);
|
||||||
.hol-cost-total { border-color: var(--primary, #4361ee); }
|
border-radius: 10px;
|
||||||
.hol-cost-total .hol-cost-stat-val { color: var(--primary, #4361ee); }
|
padding: 0.75rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.hol-cost-stat-val {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main, #0f172a);
|
||||||
|
}
|
||||||
|
.hol-cost-stat-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-muted, #64748b);
|
||||||
|
margin-top: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.hol-cost-total {
|
||||||
|
border-color: var(--primary, #4361ee);
|
||||||
|
}
|
||||||
|
.hol-cost-total .hol-cost-stat-val {
|
||||||
|
color: var(--primary, #4361ee);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
17. ENCART D'ESTIMATION DE TRAJET (OSRM)
|
||||||
|
========================================================================== */
|
||||||
|
.hol-transit-info {
|
||||||
|
margin: 0 15px 15px 15px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--hol-info-bg);
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
border: 1px dashed #7dd3fc;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hol-transit-details {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--hol-info-text);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hol-transit-icon {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hol-transit-text strong {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hol-transit-text span {
|
||||||
|
opacity: 0.85;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hol-transit-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hol-transit-info {
|
||||||
|
margin: 0 10px 15px 10px;
|
||||||
|
padding: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.hol-transit-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+172
-166
@@ -5,26 +5,21 @@ function tr(key) {
|
|||||||
return window.I18N && window.I18N[key] ? window.I18N[key] : key;
|
return window.I18N && window.I18N[key] ? window.I18N[key] : key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On utilise 'var' au lieu de 'const/let' pour éviter les crashs si le fichier est lu 2 fois
|
|
||||||
var currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
var currentLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||||
var selectedItemIdForMove = null; // Déplacé ici pour plus de clarté
|
var selectedItemIdForMove = null;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// UTILITAIRES MÉTÉO
|
// UTILITAIRES MÉTÉO
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
function getWeatherInfo(code) {
|
function getWeatherInfo(code) {
|
||||||
// Transformation en conditions pour regrouper les codes WMO
|
|
||||||
if (code === 0) return { icon: "☀️", label: tr("weather_sunny") };
|
if (code === 0) return { icon: "☀️", label: tr("weather_sunny") };
|
||||||
if ([1, 2].includes(code)) return { icon: "🌤️", label: tr("weather_sunny") };
|
if ([1, 2].includes(code)) return { icon: "🌤️", label: tr("weather_sunny") };
|
||||||
if ([3, 45, 48].includes(code))
|
if ([3, 45, 48].includes(code))
|
||||||
return { icon: "☁️", label: tr("weather_cloudy") };
|
return { icon: "☁️", label: tr("weather_cloudy") };
|
||||||
// Les codes 51 à 67 et 80 à 82 couvrent toutes les formes de pluie et bruine
|
|
||||||
if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].includes(code))
|
if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].includes(code))
|
||||||
return { icon: "🌧️", label: tr("weather_rainy") };
|
return { icon: "🌧️", label: tr("weather_rainy") };
|
||||||
// Les codes neigeux
|
|
||||||
if ([71, 73, 75, 77, 85, 86].includes(code))
|
if ([71, 73, 75, 77, 85, 86].includes(code))
|
||||||
return { icon: "❄️", label: tr("weather_snowy") };
|
return { icon: "❄️", label: tr("weather_snowy") };
|
||||||
// Orages
|
|
||||||
if ([95, 96, 99].includes(code))
|
if ([95, 96, 99].includes(code))
|
||||||
return { icon: "⛈️", label: tr("weather_rainy") };
|
return { icon: "⛈️", label: tr("weather_rainy") };
|
||||||
|
|
||||||
@@ -45,12 +40,8 @@ async function loadWeatherForStep(pt) {
|
|||||||
);
|
);
|
||||||
const res = await resp.json();
|
const res = await resp.json();
|
||||||
|
|
||||||
console.log(`Météo pour ${pt.location_name} :`, res);
|
|
||||||
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
const info = getWeatherInfo(res.data.code);
|
const info = getWeatherInfo(res.data.code);
|
||||||
|
|
||||||
// Si c'est une estimation basée sur le passé, on adapte l'affichage
|
|
||||||
const approxSymbol = res.data.is_historical ? "~" : "";
|
const approxSymbol = res.data.is_historical ? "~" : "";
|
||||||
const badgeTitle = res.data.is_historical
|
const badgeTitle = res.data.is_historical
|
||||||
? `${info.label} (${tr("weather_historical")})`
|
? `${info.label} (${tr("weather_historical")})`
|
||||||
@@ -80,8 +71,9 @@ window.addEventListener("click", function (event) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 1. GESTION DE LA MODALE D'ÉDITION RAPIDE ---
|
// ============================================================================
|
||||||
|
// 1. GESTION DE LA MODALE D'ÉDITION RAPIDE (BASES VOYAGE)
|
||||||
|
// ============================================================================
|
||||||
function openHolidayModal(mode) {
|
function openHolidayModal(mode) {
|
||||||
const modal = document.getElementById("holidayModal");
|
const modal = document.getElementById("holidayModal");
|
||||||
const form = document.getElementById("holidayForm");
|
const form = document.getElementById("holidayForm");
|
||||||
@@ -89,9 +81,6 @@ function openHolidayModal(mode) {
|
|||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
document.getElementById("inp_id").value = "";
|
document.getElementById("inp_id").value = "";
|
||||||
document.getElementById("list_transport").innerHTML = "";
|
|
||||||
document.getElementById("list_accommodation").innerHTML = "";
|
|
||||||
document.getElementById("list_activity").innerHTML = "";
|
|
||||||
|
|
||||||
if (mode === "add") {
|
if (mode === "add") {
|
||||||
document.getElementById("modalTitle").innerText = tr("hdl_modal_title");
|
document.getElementById("modalTitle").innerText = tr("hdl_modal_title");
|
||||||
@@ -131,65 +120,10 @@ function editHoliday(data) {
|
|||||||
h.budget_extra > 0 ? h.budget_extra : "";
|
h.budget_extra > 0 ? h.budget_extra : "";
|
||||||
document.getElementById("inp_notes").value = h.notes || "";
|
document.getElementById("inp_notes").value = h.notes || "";
|
||||||
|
|
||||||
document.getElementById("list_transport").innerHTML = "";
|
const vehicleInput = document.getElementById("inp_vehicle_id");
|
||||||
document.getElementById("list_accommodation").innerHTML = "";
|
if (vehicleInput) {
|
||||||
document.getElementById("list_activity").innerHTML = "";
|
vehicleInput.value = h.vehicle_id || "";
|
||||||
|
|
||||||
if (data.items) {
|
|
||||||
data.items.forEach((item) => {
|
|
||||||
if (item.name !== "PF_TECHNICAL_POINT") {
|
|
||||||
// On passe maintenant l'ID et le lieu à addItem
|
|
||||||
addItem(
|
|
||||||
item.category,
|
|
||||||
item.name,
|
|
||||||
item.amount,
|
|
||||||
item.is_paid,
|
|
||||||
item.id,
|
|
||||||
item.location_name || "",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 2. GESTION DES LISTES DYNAMIQUES DANS LA MODALE ---
|
|
||||||
|
|
||||||
function addItem(
|
|
||||||
category,
|
|
||||||
name = "",
|
|
||||||
amount = "",
|
|
||||||
isPaid = 0,
|
|
||||||
id = "",
|
|
||||||
location = "",
|
|
||||||
) {
|
|
||||||
const container = document.getElementById("list_" + category);
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.className = "savings-line-item"; // Utilisation de ta classe existante
|
|
||||||
div.style.marginBottom = "10px";
|
|
||||||
|
|
||||||
const checkedAttr = isPaid == 1 ? "checked" : "";
|
|
||||||
// Optimisation : Affichage du badge d'étape si existant
|
|
||||||
const locationBadge = location
|
|
||||||
? `<span style="font-size:0.65rem; background:#e2e8f0; padding:2px 6px; border-radius:4px; margin-right:5px; color:#64748b; font-weight:bold;">📍 ${location}</span>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
div.innerHTML = `
|
|
||||||
<input type="hidden" name="items[id][]" value="${id}">
|
|
||||||
<input type="hidden" name="items[cat][]" value="${category}">
|
|
||||||
<input type="hidden" name="items[location][]" value="${location}">
|
|
||||||
<div style="flex: 2; display:flex; flex-direction:column; gap:4px;">
|
|
||||||
${locationBadge}
|
|
||||||
<input type="text" name="items[name][]" class="pf-input" placeholder="${tr("hdl_js_ph_expense_name")}" value="${name}" style="padding: 8px; font-size:0.9rem;" required>
|
|
||||||
</div>
|
|
||||||
<input type="number" step="0.01" name="items[amount][]" class="pf-input" placeholder="0.00" value="${amount}" style="width: 90px; text-align: right; padding: 8px; font-size:0.9rem;">
|
|
||||||
<label title="${tr("hdl_paid")}" style="display: flex; align-items: center; cursor: pointer; padding: 0 5px;">
|
|
||||||
<input type="checkbox" ${checkedAttr} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="margin:0;">
|
|
||||||
<input type="hidden" name="items[paid][]" value="${isPaid}">
|
|
||||||
<span style="font-size:0.75rem; margin-left:4px; font-weight:bold; color:#64748b;">${tr("hdl_paid")}</span>
|
|
||||||
</label>
|
|
||||||
<button type="button" onclick="this.parentElement.remove()" class="btn-icon-action delete" title="${tr("btn_delete")}">🗑️</button>
|
|
||||||
`;
|
|
||||||
container.appendChild(div);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteHoliday() {
|
function deleteHoliday() {
|
||||||
@@ -203,55 +137,9 @@ function deleteHoliday() {
|
|||||||
form.submit();
|
form.submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. GESTION DE LA CARTE ---
|
|
||||||
|
|
||||||
var map = null;
|
|
||||||
|
|
||||||
function toggleMap() {
|
|
||||||
const modal = document.getElementById("hol-map-modal");
|
|
||||||
if (!modal) return;
|
|
||||||
if (modal.style.display === "flex") {
|
|
||||||
modal.style.display = "none";
|
|
||||||
} else {
|
|
||||||
modal.style.display = "flex";
|
|
||||||
setTimeout(initMap, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initMap() {
|
|
||||||
if (map) {
|
|
||||||
map.invalidateSize();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof L === "undefined") return;
|
|
||||||
|
|
||||||
map = L.map("hol-map").setView([46.6, 2.4], 4);
|
|
||||||
L.tileLayer(
|
|
||||||
"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
|
|
||||||
{
|
|
||||||
attribution: "© OpenStreetMap",
|
|
||||||
},
|
|
||||||
).addTo(map);
|
|
||||||
|
|
||||||
if (typeof HOL_MAP_POINTS !== "undefined") {
|
|
||||||
HOL_MAP_POINTS.forEach((pt) => {
|
|
||||||
const color =
|
|
||||||
pt.status === "planned" || pt.status === "booked" ? "green" : "blue";
|
|
||||||
L.circleMarker([pt.lat, pt.lng], {
|
|
||||||
color: color,
|
|
||||||
radius: 8,
|
|
||||||
fillOpacity: 0.8,
|
|
||||||
})
|
|
||||||
.addTo(map)
|
|
||||||
.bindPopup(`<b>${pt.title}</b><br>${pt.status}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET GÉOCODAGE
|
// 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET TRACÉS OSRM
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
var detailMap = null;
|
var detailMap = null;
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
@@ -263,7 +151,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
function initDetailMap() {
|
function initDetailMap() {
|
||||||
if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return;
|
if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return;
|
||||||
|
|
||||||
// 1. 🧹 NETTOYAGE PROPRE : On détruit l'ancienne instance si elle existe (Évite le bug de la souris bloquée)
|
|
||||||
if (detailMap !== null) {
|
if (detailMap !== null) {
|
||||||
detailMap.remove();
|
detailMap.remove();
|
||||||
detailMap = null;
|
detailMap = null;
|
||||||
@@ -272,17 +159,12 @@ function initDetailMap() {
|
|||||||
const mapContainer = document.getElementById("tripMap");
|
const mapContainer = document.getElementById("tripMap");
|
||||||
if (!mapContainer) return;
|
if (!mapContainer) return;
|
||||||
|
|
||||||
// 2. 🛡️ BOUCLIER FIREFOX DESKTOP : Empêche le drag natif HTML5 de voler le clic
|
|
||||||
mapContainer.style.touchAction = "none";
|
mapContainer.style.touchAction = "none";
|
||||||
mapContainer.ondragstart = function (e) {
|
mapContainer.ondragstart = function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. 🛠️ INITIALISATION DE LA CARTE
|
detailMap = L.map("tripMap", { tap: false, dragging: true });
|
||||||
detailMap = L.map("tripMap", {
|
|
||||||
tap: false, // Désactive le tap simulé (anti-warning mobile/Firefox)
|
|
||||||
dragging: true, // Force l'autorisation du déplacement à la souris
|
|
||||||
});
|
|
||||||
|
|
||||||
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
@@ -290,25 +172,21 @@ function initDetailMap() {
|
|||||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||||
}).addTo(detailMap);
|
}).addTo(detailMap);
|
||||||
|
|
||||||
// Cas : Aucun point
|
|
||||||
if (MAP_POINTS.length === 0) {
|
if (MAP_POINTS.length === 0) {
|
||||||
detailMap.setView([46.6, 2.4], 5); // France par défaut
|
detailMap.setView([46.6, 2.4], 5);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const latlngs = [];
|
const latlngs = [];
|
||||||
const bounds = L.latLngBounds();
|
const bounds = L.latLngBounds();
|
||||||
|
|
||||||
// 4. PLACEMENT DES MARQUEURS
|
|
||||||
MAP_POINTS.forEach((pt, index) => {
|
MAP_POINTS.forEach((pt, index) => {
|
||||||
const pos = [pt.lat, pt.lng];
|
const pos = [pt.lat, pt.lng];
|
||||||
latlngs.push(pos);
|
latlngs.push(pos);
|
||||||
bounds.extend(pos);
|
bounds.extend(pos);
|
||||||
|
|
||||||
const color = "#2563eb";
|
|
||||||
|
|
||||||
const marker = L.circleMarker(pos, {
|
const marker = L.circleMarker(pos, {
|
||||||
color: color,
|
color: "#2563eb",
|
||||||
radius: window.innerWidth < 768 ? 6 : 8,
|
radius: window.innerWidth < 768 ? 6 : 8,
|
||||||
fillOpacity: 1,
|
fillOpacity: 1,
|
||||||
fillColor: "white",
|
fillColor: "white",
|
||||||
@@ -323,11 +201,10 @@ function initDetailMap() {
|
|||||||
<div style="text-align:center;">
|
<div style="text-align:center;">
|
||||||
<div style="font-size:0.75rem; color:#64748b; margin-bottom:2px; font-weight:bold;">${stepLabel} ${index + 1}</div>
|
<div style="font-size:0.75rem; color:#64748b; margin-bottom:2px; font-weight:bold;">${stepLabel} ${index + 1}</div>
|
||||||
<strong style="font-size:1rem; color:#0f172a;">${pt.location_name}</strong><br>
|
<strong style="font-size:1rem; color:#0f172a;">${pt.location_name}</strong><br>
|
||||||
<span style="font-weight:bold; color:${color};">${parseFloat(pt.total_amount).toFixed(2)} €</span>
|
<span style="font-weight:bold; color:#2563eb;">${parseFloat(pt.total_amount).toFixed(2)} €</span>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Animation au clic sur le marqueur
|
|
||||||
marker.on("click", function () {
|
marker.on("click", function () {
|
||||||
const card = document.getElementById("step-card-" + pt.sort_order);
|
const card = document.getElementById("step-card-" + pt.sort_order);
|
||||||
if (card) {
|
if (card) {
|
||||||
@@ -345,7 +222,6 @@ function initDetailMap() {
|
|||||||
|
|
||||||
const mapPadding = window.innerWidth < 768 ? [20, 20] : [50, 50];
|
const mapPadding = window.innerWidth < 768 ? [20, 20] : [50, 50];
|
||||||
|
|
||||||
// 5. CENTRAGE ET TRACÉS (OSRM)
|
|
||||||
if (latlngs.length === 1) {
|
if (latlngs.length === 1) {
|
||||||
detailMap.setView(latlngs[0], 12);
|
detailMap.setView(latlngs[0], 12);
|
||||||
} else if (latlngs.length > 1) {
|
} else if (latlngs.length > 1) {
|
||||||
@@ -379,10 +255,17 @@ function initDetailMap() {
|
|||||||
results.sort((a, b) => a.index - b.index);
|
results.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
let returnStartIndex = latlngs.length - 2;
|
let returnStartIndex = latlngs.length - 2;
|
||||||
const customReturnStep = MAP_POINTS.findIndex((p) => p.is_return == 1);
|
if (
|
||||||
|
typeof window.GLOBAL_RETURN_STEP_ID !== "undefined" &&
|
||||||
|
window.GLOBAL_RETURN_STEP_ID !== null
|
||||||
|
) {
|
||||||
|
const customReturnStep = MAP_POINTS.findIndex(
|
||||||
|
(p) => p.sort_order == window.GLOBAL_RETURN_STEP_ID,
|
||||||
|
);
|
||||||
if (customReturnStep > 0) {
|
if (customReturnStep > 0) {
|
||||||
returnStartIndex = customReturnStep;
|
returnStartIndex = customReturnStep;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
results.forEach((res) => {
|
results.forEach((res) => {
|
||||||
const i = res.index;
|
const i = res.index;
|
||||||
@@ -408,6 +291,52 @@ function initDetailMap() {
|
|||||||
lineCap: "round",
|
lineCap: "round",
|
||||||
lineJoin: "round",
|
lineJoin: "round",
|
||||||
}).addTo(detailMap);
|
}).addTo(detailMap);
|
||||||
|
|
||||||
|
// 🔥 CALCUL AUTO DU COUT ET KILOMÈTRES
|
||||||
|
const distanceKm = res.data.routes[0].distance / 1000;
|
||||||
|
const fuelL100 = window.VEHICLE_CONSUMPTION || 7;
|
||||||
|
const fuelPrice = window.FUEL_PRICE || 1.85;
|
||||||
|
const cost = (distanceKm / 100) * fuelL100 * fuelPrice;
|
||||||
|
|
||||||
|
const targetOrder = MAP_POINTS[res.index + 1].sort_order;
|
||||||
|
const targetCard = document.getElementById(
|
||||||
|
"step-card-" + targetOrder,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetCard) {
|
||||||
|
const existingTransit =
|
||||||
|
targetCard.querySelectorAll(".transit-auto-info");
|
||||||
|
existingTransit.forEach((el) => el.remove());
|
||||||
|
|
||||||
|
const rawLocationName = MAP_POINTS[res.index].location_name;
|
||||||
|
const safeLocationName = rawLocationName.replace(/'/g, "\\'");
|
||||||
|
const expenseDesc = `Essence depuis ${rawLocationName}`;
|
||||||
|
|
||||||
|
const targetStepData = MAP_POINTS[res.index + 1];
|
||||||
|
const isAlreadyAdded = targetStepData.items.some(
|
||||||
|
(it) => it.name === expenseDesc,
|
||||||
|
);
|
||||||
|
|
||||||
|
const summaryHtml = `
|
||||||
|
<div class="transit-auto-info" style="font-size: 0.8rem; color: var(--text-muted); padding: 4px 0 10px 42px; display: flex; align-items: center; gap: 8px;">
|
||||||
|
🚗 ${Math.round(distanceKm)} km
|
||||||
|
<span style="opacity: 0.5;">|</span>
|
||||||
|
**⛽ ~${cost.toFixed(2)} €**
|
||||||
|
${
|
||||||
|
!isAlreadyAdded
|
||||||
|
? `<button type="button" style="background:none; border:none; color:var(--primary); cursor:pointer; font-weight:600; font-size:0.8rem; padding:0; margin-left: 5px;"
|
||||||
|
onclick="addQuickTransitExpense(${document.querySelector("input[name=holiday_id]").value}, ${targetOrder}, ${cost.toFixed(2)}, 'Essence depuis ${safeLocationName}', this)">
|
||||||
|
+ Ajouter
|
||||||
|
</button>`
|
||||||
|
: `<span style="color:var(--success); font-weight:bold; margin-left: 5px;" title="Dépense déjà ajoutée à cette étape">✓ Ajouté</span>`
|
||||||
|
}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const cpHeader = targetCard.querySelector(".hol-cp-header");
|
||||||
|
if (cpHeader) {
|
||||||
|
cpHeader.insertAdjacentHTML("afterend", summaryHtml);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
drawFallbackLine(res.coords, routeColor, routeWeight);
|
drawFallbackLine(res.coords, routeColor, routeWeight);
|
||||||
}
|
}
|
||||||
@@ -415,7 +344,6 @@ function initDetailMap() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. LANCEMENT DE LA MÉTÉO
|
|
||||||
if (typeof MAP_POINTS !== "undefined") {
|
if (typeof MAP_POINTS !== "undefined") {
|
||||||
MAP_POINTS.forEach((pt) => {
|
MAP_POINTS.forEach((pt) => {
|
||||||
if (typeof loadWeatherForStep === "function") {
|
if (typeof loadWeatherForStep === "function") {
|
||||||
@@ -424,14 +352,12 @@ function initDetailMap() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. FIX FINAL : Force Leaflet à recalculer sa taille une fois le DOM stabilisé
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (detailMap) {
|
if (detailMap) {
|
||||||
detailMap.invalidateSize();
|
detailMap.invalidateSize();
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
// Fonction utilitaire locale
|
|
||||||
function drawFallbackLine(coords, color, weight) {
|
function drawFallbackLine(coords, color, weight) {
|
||||||
L.polyline(coords, {
|
L.polyline(coords, {
|
||||||
color: color,
|
color: color,
|
||||||
@@ -446,7 +372,6 @@ function panMapTo(lat, lng) {
|
|||||||
if (detailMap) {
|
if (detailMap) {
|
||||||
detailMap.setView([lat, lng], 14, { animate: true });
|
detailMap.setView([lat, lng], 14, { animate: true });
|
||||||
|
|
||||||
// 🛠️ ERGONOMIE MOBILE : Auto-scroll vers la carte si on est sur petit écran
|
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
const mapDiv = document.getElementById("tripMap");
|
const mapDiv = document.getElementById("tripMap");
|
||||||
if (mapDiv) {
|
if (mapDiv) {
|
||||||
@@ -456,8 +381,9 @@ function panMapTo(lat, lng) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- LOGIQUE DE LA MODALE CHECKPOINT ---
|
// ============================================================================
|
||||||
|
// 5. LOGIQUE DE LA MODALE CHECKPOINT (ÉTAPES)
|
||||||
|
// ============================================================================
|
||||||
function openCheckpointModal(mode, data = null) {
|
function openCheckpointModal(mode, data = null) {
|
||||||
const searchBlock = document.getElementById("cpSearchBlock");
|
const searchBlock = document.getElementById("cpSearchBlock");
|
||||||
const formBlock = document.getElementById("formCheckpoint");
|
const formBlock = document.getElementById("formCheckpoint");
|
||||||
@@ -470,7 +396,9 @@ function openCheckpointModal(mode, data = null) {
|
|||||||
document.getElementById("cp_start_date").value = "";
|
document.getElementById("cp_start_date").value = "";
|
||||||
if (document.getElementById("cp_end_date"))
|
if (document.getElementById("cp_end_date"))
|
||||||
document.getElementById("cp_end_date").value = "";
|
document.getElementById("cp_end_date").value = "";
|
||||||
|
if (document.getElementById("searchPlaceInput"))
|
||||||
document.getElementById("searchPlaceInput").value = "";
|
document.getElementById("searchPlaceInput").value = "";
|
||||||
|
if (document.getElementById("searchResults"))
|
||||||
document.getElementById("searchResults").innerHTML = "";
|
document.getElementById("searchResults").innerHTML = "";
|
||||||
|
|
||||||
searchBlock.style.display = "block";
|
searchBlock.style.display = "block";
|
||||||
@@ -481,8 +409,16 @@ function openCheckpointModal(mode, data = null) {
|
|||||||
btnDel.style.display = "none";
|
btnDel.style.display = "none";
|
||||||
document.getElementById("cp_old_sort_order").value = "";
|
document.getElementById("cp_old_sort_order").value = "";
|
||||||
document.getElementById("cp_name").value = "";
|
document.getElementById("cp_name").value = "";
|
||||||
|
|
||||||
|
if (document.getElementById("cp_step_type")) {
|
||||||
|
document.getElementById("cp_step_type").value = "stop";
|
||||||
|
toggleStepDates("stop");
|
||||||
|
}
|
||||||
|
if (document.getElementById("cp_set_as_return")) {
|
||||||
|
document.getElementById("cp_set_as_return").checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
addCpExpenseLine();
|
addCpExpenseLine();
|
||||||
document.getElementById("cp_is_return").checked = false;
|
|
||||||
} else if (mode === "edit" && data) {
|
} else if (mode === "edit" && data) {
|
||||||
document.getElementById("cpModalTitle").innerText = tr("hdl_js_edit_step");
|
document.getElementById("cpModalTitle").innerText = tr("hdl_js_edit_step");
|
||||||
formBlock.style.display = "block";
|
formBlock.style.display = "block";
|
||||||
@@ -494,7 +430,19 @@ function openCheckpointModal(mode, data = null) {
|
|||||||
document.getElementById("cp_name").value = data.location_name;
|
document.getElementById("cp_name").value = data.location_name;
|
||||||
document.getElementById("cp_start_date").value = data.step_start_date || "";
|
document.getElementById("cp_start_date").value = data.step_start_date || "";
|
||||||
document.getElementById("cp_end_date").value = data.step_end_date || "";
|
document.getElementById("cp_end_date").value = data.step_end_date || "";
|
||||||
document.getElementById("cp_is_return").checked = data.is_return == 1;
|
|
||||||
|
// 🔥 PRE-REMPLISSAGE DU TYPE D'ÉTAPE ET UI DATES
|
||||||
|
if (document.getElementById("cp_step_type")) {
|
||||||
|
const type = data.step_type || "stop";
|
||||||
|
document.getElementById("cp_step_type").value = type;
|
||||||
|
toggleStepDates(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 PRE-REMPLISSAGE DE LA CASE RETOUR BASEE SUR LA GLOBALE
|
||||||
|
if (document.getElementById("cp_set_as_return")) {
|
||||||
|
document.getElementById("cp_set_as_return").checked =
|
||||||
|
window.GLOBAL_RETURN_STEP_ID == data.sort_order;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.items && data.items.length > 0) {
|
if (data.items && data.items.length > 0) {
|
||||||
let visibleCount = 0;
|
let visibleCount = 0;
|
||||||
@@ -589,6 +537,10 @@ function addCpExpenseLine(
|
|||||||
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
|
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
|
||||||
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
|
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select name="items[context][]" class="pf-input hol-form-select" style="width:auto; margin-left:5px; font-size:0.75rem;">
|
||||||
|
<option value="local">📍 Sur place</option>
|
||||||
|
<option value="transit">🛣️ Transit</option>
|
||||||
|
</select>
|
||||||
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="${tr("hdl_js_ph_expense_name")}" value="${name}">
|
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="${tr("hdl_js_ph_expense_name")}" value="${name}">
|
||||||
<input type="number" step="0.01" name="items[amount][]" class="pf-input hol-form-number" placeholder="0.00" value="${amount}">
|
<input type="number" step="0.01" name="items[amount][]" class="pf-input hol-form-number" placeholder="0.00" value="${amount}">
|
||||||
|
|
||||||
@@ -621,10 +573,8 @@ function deleteCheckpoint() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 5. RÉORDONNANCEMENT DES ÉTAPES (DRAG & DROP PC + FLÈCHES MOBILE)
|
// 6. REORDONNANCEMENT DES ÉTAPES (DRAG & DROP PC + MOBILE)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// On sort cette fonction pour pouvoir l'appeler depuis les boutons fléchés sur mobile
|
|
||||||
function saveCheckpointOrder() {
|
function saveCheckpointOrder() {
|
||||||
const locations = [
|
const locations = [
|
||||||
...document.querySelectorAll(".hol-checkpoint-draggable"),
|
...document.querySelectorAll(".hol-checkpoint-draggable"),
|
||||||
@@ -641,7 +591,6 @@ function saveCheckpointOrder() {
|
|||||||
}).then(() => window.location.reload());
|
}).then(() => window.location.reload());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction appelée par les flèches Haut/Bas sur mobile
|
|
||||||
function moveStepMobile(btn, direction) {
|
function moveStepMobile(btn, direction) {
|
||||||
const item = btn.closest(".hol-checkpoint-draggable");
|
const item = btn.closest(".hol-checkpoint-draggable");
|
||||||
const container = item.parentElement;
|
const container = item.parentElement;
|
||||||
@@ -672,7 +621,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
let draggedItem = null;
|
let draggedItem = null;
|
||||||
|
|
||||||
checkpoints.forEach((item) => {
|
checkpoints.forEach((item) => {
|
||||||
// Si on est sur mobile, on supprime l'attribut draggable pour éviter les conflits de scroll
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
item.removeAttribute("draggable");
|
item.removeAttribute("draggable");
|
||||||
return;
|
return;
|
||||||
@@ -724,9 +672,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MOTEUR DRAG & DROP DU PLANNING
|
// 7. MOTEUR DRAG & DROP DU PLANNING CARNET DE BORD
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
function closePlanningModal() {
|
function closePlanningModal() {
|
||||||
document.getElementById("planningModal").style.display = "none";
|
document.getElementById("planningModal").style.display = "none";
|
||||||
document.body.classList.remove("no-scroll");
|
document.body.classList.remove("no-scroll");
|
||||||
@@ -798,7 +745,6 @@ function openPlanningModal(step) {
|
|||||||
html += `</div></div>`;
|
html += `</div></div>`;
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
// 1. On détecte si on est sur mobile juste avant la boucle
|
|
||||||
const isMobile = window.innerWidth <= 768;
|
const isMobile = window.innerWidth <= 768;
|
||||||
const dragAttr = isMobile ? "" : 'draggable="true"';
|
const dragAttr = isMobile ? "" : 'draggable="true"';
|
||||||
|
|
||||||
@@ -819,7 +765,6 @@ function openPlanningModal(step) {
|
|||||||
? `<div class="hol-drag-note">${it.notes}</div>`
|
? `<div class="hol-drag-note">${it.notes}</div>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// 2. MODIFICATION ICI : On remplace le texte en dur draggable="true" par la variable ${dragAttr}
|
|
||||||
const elHtml = `
|
const elHtml = `
|
||||||
<div class="hol-drag-item ${catClass}" ${dragAttr}
|
<div class="hol-drag-item ${catClass}" ${dragAttr}
|
||||||
id="drag-item-${it.id}" data-id="${it.id}"
|
id="drag-item-${it.id}" data-id="${it.id}"
|
||||||
@@ -957,18 +902,6 @@ function updateItemMemory(itemId, changes) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveItemDateTime(itemId, dateStr, timeStr) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("action", "update_item_datetime");
|
|
||||||
formData.append("item_id", itemId);
|
|
||||||
formData.append("item_date", dateStr);
|
|
||||||
formData.append("item_time", timeStr);
|
|
||||||
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
}).catch((err) => console.error("Erreur:", err));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MÉTÉO SPÉCIFIQUE AU HEADER DU PLANNING
|
// MÉTÉO SPÉCIFIQUE AU HEADER DU PLANNING
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -997,3 +930,76 @@ async function loadWeatherForPlanning(lat, lng, dateStr) {
|
|||||||
console.error("Erreur météo planning", e);
|
console.error("Erreur météo planning", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gère l'affichage des dates dans la modale d'étape
|
||||||
|
function toggleStepDates(type) {
|
||||||
|
const grpEnd = document.getElementById("grp_end_date");
|
||||||
|
const lblStart = document.getElementById("lbl_start_date");
|
||||||
|
|
||||||
|
if (type === "origin") {
|
||||||
|
grpEnd.style.display = "none";
|
||||||
|
lblStart.innerText = "📅 Date de départ";
|
||||||
|
} else if (type === "destination") {
|
||||||
|
grpEnd.style.display = "none";
|
||||||
|
lblStart.innerText = "📅 Date d'arrivée";
|
||||||
|
} else {
|
||||||
|
grpEnd.style.display = "block";
|
||||||
|
lblStart.innerText = tr("hdl_label_arrival");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajout magique d'une dépense d'essence SÉCURISÉE
|
||||||
|
function addQuickTransitExpense(
|
||||||
|
holidayId,
|
||||||
|
sortOrder,
|
||||||
|
amount,
|
||||||
|
description,
|
||||||
|
btnElement,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
`Ajouter une dépense de carburant de ${amount}€ pour cette étape ?`,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (btnElement) {
|
||||||
|
btnElement.disabled = true;
|
||||||
|
btnElement.innerText = "⏳...";
|
||||||
|
btnElement.style.cursor = "not-allowed";
|
||||||
|
btnElement.style.opacity = "0.7";
|
||||||
|
}
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("action", "add_single_item");
|
||||||
|
fd.append("holiday_id", holidayId);
|
||||||
|
fd.append("sort_order", sortOrder);
|
||||||
|
fd.append("category", "transport");
|
||||||
|
fd.append("name", description);
|
||||||
|
fd.append("amount", amount);
|
||||||
|
fd.append("context", "transit");
|
||||||
|
|
||||||
|
fetch("/modules/holidays/includes/api/save_checkpoint.php", {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
}).then(() => window.location.reload());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permet de modifier le prix du carburant à la volée
|
||||||
|
function updateFuelPrice() {
|
||||||
|
const currentPrice = window.FUEL_PRICE || 1.85;
|
||||||
|
let newPrice = prompt(
|
||||||
|
"Définit le prix du carburant estimé (€/L) pour tes trajets :",
|
||||||
|
currentPrice,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newPrice !== null) {
|
||||||
|
newPrice = parseFloat(newPrice.replace(",", "."));
|
||||||
|
if (!isNaN(newPrice) && newPrice > 0) {
|
||||||
|
localStorage.setItem("holidays_fuel_price", newPrice);
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert("Prix invalide.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,26 +4,51 @@ require dirname(__DIR__, 4) . '/includes/auth.php';
|
|||||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
require_login();
|
require_login();
|
||||||
|
|
||||||
// INTERCEPTION AJAX : Sauvegarde du planning (Drag & Drop / Durée)
|
// INTERCEPTION AJAX : Sauvegarde du planning (Drag & Drop / Durée) ET Dépense Rapide
|
||||||
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration'])) {
|
if (isset($_POST['action']) && in_array($_POST['action'], ['update_item_datetime', 'update_item_duration', 'add_single_item'])) {
|
||||||
$itemId = (int)$_POST['item_id'];
|
|
||||||
|
|
||||||
|
// 🔥 NOUVEAU : Ajout sécurisé d'une dépense unique (Essence OSRM)
|
||||||
|
if ($_POST['action'] === 'add_single_item') {
|
||||||
|
$holiday_id = (int)$_POST['holiday_id'];
|
||||||
|
$sort_order = (int)$_POST['sort_order'];
|
||||||
|
|
||||||
|
// On récupère les infos de l'étape existante pour ne rien casser (lat, lng, dates...)
|
||||||
|
$stmt = $pdo->prepare("SELECT location_name, lat, lng, step_start_date, step_end_date, step_type FROM pf_holidays_items WHERE holiday_id = ? AND sort_order = ? LIMIT 1");
|
||||||
|
$stmt->execute([$holiday_id, $sort_order]);
|
||||||
|
$stepInfo = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($stepInfo) {
|
||||||
|
$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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$ins->execute([
|
||||||
|
$holiday_id, $_POST['category'], $_POST['name'], (float)$_POST['amount'], 0,
|
||||||
|
$stepInfo['location_name'], $stepInfo['lat'], $stepInfo['lng'],
|
||||||
|
$sort_order, $stepInfo['step_start_date'], $stepInfo['step_end_date'],
|
||||||
|
$stepInfo['step_type'], $_POST['context'], 1
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Ancien code Drag&Drop conservé ---
|
||||||
|
$itemId = (int)$_POST['item_id'];
|
||||||
if ($_POST['action'] === 'update_item_datetime') {
|
if ($_POST['action'] === 'update_item_datetime') {
|
||||||
$itemDate = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
$itemDate = !empty($_POST['item_date']) ? $_POST['item_date'] : null;
|
||||||
$itemTime = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
$itemTime = !empty($_POST['item_time']) ? $_POST['item_time'] : null;
|
||||||
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ? WHERE id = ?");
|
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET item_date = ?, item_time = ? WHERE id = ?");
|
||||||
$stmt->execute([$itemDate, $itemTime, $itemId]);
|
$stmt->execute([$itemDate, $itemTime, $itemId]);
|
||||||
} else {
|
} else if ($_POST['action'] === 'update_item_duration') {
|
||||||
$duration = (int)$_POST['duration'];
|
$duration = (int)$_POST['duration'];
|
||||||
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET duration = ? WHERE id = ?");
|
$stmt = $pdo->prepare("UPDATE pf_holidays_items SET duration = ? WHERE id = ?");
|
||||||
$stmt->execute([$duration, $itemId]);
|
$stmt->execute([$duration, $itemId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
exit; // Crucial : on arrête le script ici !
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$holiday_id = (int)$_POST['holiday_id'];
|
$holiday_id = (int)$_POST['holiday_id'];
|
||||||
|
// ... (LE RESTE DE TON FICHIER NE CHANGE PAS)
|
||||||
$location_name = trim($_POST['location_name']);
|
$location_name = trim($_POST['location_name']);
|
||||||
$lat = (float)$_POST['lat'];
|
$lat = (float)$_POST['lat'];
|
||||||
$lng = (float)$_POST['lng'];
|
$lng = (float)$_POST['lng'];
|
||||||
@@ -53,13 +78,17 @@ if ($holiday_id > 0 && !empty($location_name)) {
|
|||||||
$target_order = ($max !== null) ? (int)$max + 1 : 0;
|
$target_order = ($max !== null) ? (int)$max + 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupération des dates de l'étape globale
|
// Récupération des dates de l'étape globale et du type
|
||||||
$step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null;
|
$step_start = !empty($_POST['step_start_date']) ? $_POST['step_start_date'] : null;
|
||||||
$step_end = !empty($_POST['step_end_date']) ? $_POST['step_end_date'] : null;
|
$step_end = !empty($_POST['step_end_date']) ? $_POST['step_end_date'] : null;
|
||||||
$is_return = isset($_POST['is_return']) ? 1 : 0; // NOUVEAU
|
$step_type = $_POST['step_type'] ?? 'stop';
|
||||||
|
|
||||||
// 3. INSERTION DES LIGNES (16 Colonnes)
|
// Nettoyage des dates selon le type d'étape
|
||||||
$stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration, is_return) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
if ($step_type === 'origin') $step_end = null; // Un départ n'a pas de date de fin
|
||||||
|
if ($step_type === 'destination') $step_end = null; // Une arrivée finale n'a pas de date de départ
|
||||||
|
|
||||||
|
// 3. INSERTION DES LIGNES
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng, sort_order, notes, item_date, item_time, step_start_date, step_end_date, duration, step_type, expense_context) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
$validItemsCount = 0;
|
$validItemsCount = 0;
|
||||||
|
|
||||||
if (isset($_POST['items']['name'])) {
|
if (isset($_POST['items']['name'])) {
|
||||||
@@ -75,30 +104,26 @@ if ($holiday_id > 0 && !empty($location_name)) {
|
|||||||
$date = !empty($_POST['items']['date'][$i]) ? $_POST['items']['date'][$i] : null;
|
$date = !empty($_POST['items']['date'][$i]) ? $_POST['items']['date'][$i] : null;
|
||||||
$time = !empty($_POST['items']['time'][$i]) ? $_POST['items']['time'][$i] : null;
|
$time = !empty($_POST['items']['time'][$i]) ? $_POST['items']['time'][$i] : null;
|
||||||
$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';
|
||||||
|
|
||||||
// Ajout de $is_return à la fin
|
$stmt->execute([$holiday_id, $cat, $name ?: tr('hdl_default_exp_name'), $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $step_type, $context]);
|
||||||
$stmt->execute([$holiday_id, $cat, $name ?: tr('hdl_default_exp_name'), $amount, $paid, $location_name, $lat, $lng, $target_order, $note, $date, $time, $step_start, $step_end, $dur, $is_return]);
|
|
||||||
$validItemsCount++;
|
$validItemsCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($validItemsCount === 0) {
|
if ($validItemsCount === 0) {
|
||||||
$stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1, $is_return]);
|
$stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng, $target_order, '', null, null, $step_start, $step_end, 1, $step_type, 'local']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. GESTION DES FAVORIS
|
// 4. GESTION DU RETOUR (Si l'utilisateur définit cette étape comme point de retour)
|
||||||
if (isset($_POST['save_favorite']) && $_POST['save_favorite'] == '1') {
|
if (isset($_POST['set_as_return']) && $_POST['set_as_return'] == '1') {
|
||||||
$stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'");
|
// On enregistre l'ID de cette étape technique comme point de retour global du voyage
|
||||||
$favs = json_decode($stmtFav->fetchColumn() ?: '[]', true);
|
$pdo->prepare("UPDATE pf_holidays SET return_step_id = ? WHERE id = ?")->execute([$target_order, $holiday_id]);
|
||||||
$exists = false;
|
|
||||||
foreach ($favs as $f) { if ($f['name'] === $location_name) $exists = true; }
|
|
||||||
if (!$exists) {
|
|
||||||
$favs[] = ['name' => $location_name, 'lat' => $lat, 'lng' => $lng];
|
|
||||||
$pdo->prepare("INSERT INTO pf_notes (note_type, reference_id, content) VALUES ('holiday_favorites', 'GLOBAL', ?) ON DUPLICATE KEY UPDATE content = VALUES(content)")->execute([json_encode($favs)]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. GESTION DES FAVORIS ... (Garde ton code existant ici)
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
} catch (Exception $e) { $pdo->rollBack(); die($e->getMessage()); }
|
} catch (Exception $e) { $pdo->rollBack(); die($e->getMessage()); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
// modules/holidays/includes/api/save_holiday.php
|
// modules/holidays/includes/api/save_holiday.php
|
||||||
|
|
||||||
// On remonte de 4 niveaux pour atteindre la racine (api -> includes -> holidays -> modules -> racine)
|
// On remonte de 4 niveaux pour atteindre la racine
|
||||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||||
require_login(); // Si cette fonction nécessite une redirection, gère-la dans auth.php
|
require_login();
|
||||||
|
|
||||||
if (isset($_POST['action_delete']) && $_POST['action_delete'] == '1') {
|
if (isset($_POST['action_delete']) && $_POST['action_delete'] == '1') {
|
||||||
$stmt = $pdo->prepare("DELETE FROM pf_holidays WHERE id = ?");
|
$stmt = $pdo->prepare("DELETE FROM pf_holidays WHERE id = ?");
|
||||||
@@ -22,61 +22,25 @@ $status = $_POST['status'];
|
|||||||
$food = !empty($_POST['budget_food']) ? $_POST['budget_food'] : 0;
|
$food = !empty($_POST['budget_food']) ? $_POST['budget_food'] : 0;
|
||||||
$extra = !empty($_POST['budget_extra']) ? $_POST['budget_extra'] : 0;
|
$extra = !empty($_POST['budget_extra']) ? $_POST['budget_extra'] : 0;
|
||||||
$notes = $_POST['notes'];
|
$notes = $_POST['notes'];
|
||||||
|
// 🔥 NOUVEAU : On récupère le véhicule optionnel
|
||||||
|
$vehicle_id = !empty($_POST['vehicle_id']) ? (int)$_POST['vehicle_id'] : null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
if ($id) {
|
if ($id) {
|
||||||
// UPDATE
|
// UPDATE (avec vehicle_id)
|
||||||
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=? WHERE id=?";
|
$sql = "UPDATE pf_holidays SET title=?, period_hint=?, start_date=?, end_date=?, status=?, budget_food=?, budget_extra=?, notes=?, vehicle_id=? WHERE id=?";
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $id]);
|
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id, $id]);
|
||||||
} else {
|
} else {
|
||||||
// INSERT
|
// INSERT (avec vehicle_id)
|
||||||
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
$sql = "INSERT INTO pf_holidays (title, period_hint, start_date, end_date, status, budget_food, budget_extra, notes, vehicle_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes]);
|
$stmt->execute([$title, $period, $start, $end, $status, $food, $extra, $notes, $vehicle_id]);
|
||||||
$id = $pdo->lastInsertId();
|
$id = $pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
// GESTION INTELLIGENTE DES ITEMS
|
|
||||||
if (!empty($_POST['items']['name'])) {
|
|
||||||
$count = count($_POST['items']['name']);
|
|
||||||
// On prépare une requête qui met à jour si l'ID existe, sinon insère
|
|
||||||
$stmtItem = $pdo->prepare("
|
|
||||||
INSERT INTO pf_holidays_items (id, holiday_id, category, name, amount, is_paid, location_name)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
category = VALUES(category),
|
|
||||||
name = VALUES(name),
|
|
||||||
amount = VALUES(amount),
|
|
||||||
is_paid = VALUES(is_paid)
|
|
||||||
");
|
|
||||||
|
|
||||||
$keepIds = [];
|
|
||||||
for ($i = 0; $i < $count; $i++) {
|
|
||||||
$itemId = !empty($_POST['items']['id'][$i]) ? (int)$_POST['items']['id'][$i] : null;
|
|
||||||
$cat = $_POST['items']['cat'][$i] ?? 'activity';
|
|
||||||
$name = trim($_POST['items']['name'][$i] ?? '');
|
|
||||||
$amount = floatval($_POST['items']['amount'][$i] ?? 0);
|
|
||||||
$paid = (int)($_POST['items']['paid'][$i] ?? 0);
|
|
||||||
$loc = !empty($_POST['items']['location'][$i]) ? $_POST['items']['location'][$i] : null;
|
|
||||||
|
|
||||||
if (!empty($name)) {
|
|
||||||
$stmtItem->execute([$itemId, $id, $cat, $name, $amount, $paid, $loc]);
|
|
||||||
$keepIds[] = $itemId ?: $pdo->lastInsertId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nettoyage : On supprime les items qui ont été retirés de la modale
|
|
||||||
// (Attention : uniquement ceux du voyage actuel qui ne sont plus dans la liste envoyée)
|
|
||||||
if (!empty($keepIds)) {
|
|
||||||
$placeholders = implode(',', array_fill(0, count($keepIds), '?'));
|
|
||||||
$sqlDel = "DELETE FROM pf_holidays_items WHERE holiday_id = ? AND id NOT IN ($placeholders)";
|
|
||||||
$pdo->prepare($sqlDel)->execute(array_merge([$id], $keepIds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
@@ -84,6 +48,5 @@ try {
|
|||||||
die("Erreur base de données : " . $e->getMessage());
|
die("Erreur base de données : " . $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirection vers la page principale
|
|
||||||
header("Location: /holidays.php");
|
header("Location: /holidays.php");
|
||||||
exit;
|
exit;
|
||||||
@@ -11,10 +11,15 @@ if ($id === 0) {
|
|||||||
// Récupération des données du voyage
|
// Récupération des données du voyage
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT h.*,
|
SELECT h.*,
|
||||||
|
v.name as vehicle_name,
|
||||||
|
v.consumption as vehicle_consumption,
|
||||||
(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,
|
||||||
FROM pf_holidays h WHERE h.id = ?
|
(SELECT COALESCE(SUM(amount), 0) FROM pf_holidays_items WHERE holiday_id = h.id AND expense_context = 'transit') as total_transit
|
||||||
|
FROM pf_holidays h
|
||||||
|
LEFT JOIN pf_vehicles v ON h.vehicle_id = v.id
|
||||||
|
WHERE h.id = ?
|
||||||
");
|
");
|
||||||
$stmt->execute([$id]);
|
$stmt->execute([$id]);
|
||||||
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
@@ -42,7 +47,7 @@ foreach ($items as $it) {
|
|||||||
'sort_order' => $it['sort_order'],
|
'sort_order' => $it['sort_order'],
|
||||||
'step_start_date' => $it['step_start_date'],
|
'step_start_date' => $it['step_start_date'],
|
||||||
'step_end_date' => $it['step_end_date'],
|
'step_end_date' => $it['step_end_date'],
|
||||||
'is_return' => (int)$it['is_return'],
|
'step_type' => $it['step_type'] ?? 'stop',
|
||||||
'total_amount' => 0,
|
'total_amount' => 0,
|
||||||
'items' => []
|
'items' => []
|
||||||
];
|
];
|
||||||
@@ -105,10 +110,34 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
|
|
||||||
<div class="hol-summary-card">
|
<div class="hol-summary-card">
|
||||||
<div class="hol-summary-grid">
|
<div class="hol-summary-grid">
|
||||||
|
|
||||||
|
<?php if (!empty($holiday['vehicle_name'])): ?>
|
||||||
<div class="hol-summary-item">
|
<div class="hol-summary-item">
|
||||||
<div class="hol-summary-label"><?= tr('hdl_label_period') ?></div>
|
<div class="hol-summary-label">Transport</div>
|
||||||
<div class="hol-summary-value"><?= $dateDisplay ?: tr('hdl_dates_to_define') ?></div>
|
<div class="hol-summary-value">🚗 <?= htmlspecialchars($holiday['vehicle_name']) ?></div>
|
||||||
</div>
|
</div>
|
||||||
|
<?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>
|
||||||
|
|
||||||
|
<?php if ($holiday['total_transit'] > 0): ?>
|
||||||
|
<span onclick="alert('Bientôt : Liste détaillée des frais de route !')" 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">
|
||||||
|
👁️
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="hol-summary-item">
|
<div class="hol-summary-item">
|
||||||
<div class="hol-summary-label"><?= tr('hdl_label_budget_food_extras') ?></div>
|
<div class="hol-summary-label"><?= tr('hdl_label_budget_food_extras') ?></div>
|
||||||
<div class="hol-summary-value">🍔 <?= number_format($holiday['budget_food'], 0) ?> € | 🎁 <?= number_format($holiday['budget_extra'], 0) ?> €</div>
|
<div class="hol-summary-value">🍔 <?= number_format($holiday['budget_food'], 0) ?> € | 🎁 <?= number_format($holiday['budget_extra'], 0) ?> €</div>
|
||||||
@@ -211,9 +240,15 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
</div>
|
</div>
|
||||||
<div class="hol-cp-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)" title="<?= htmlspecialchars($step['location_name']) ?>">
|
<div class="hol-cp-title" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)" title="<?= htmlspecialchars($step['location_name']) ?>">
|
||||||
📍 <?= htmlspecialchars($step['location_name']) ?>
|
📍 <?= htmlspecialchars($step['location_name']) ?>
|
||||||
<?php if (!empty($step['is_return'])): ?>
|
<?php if ($holiday['return_step_id'] !== null && $holiday['return_step_id'] == $step['sort_order']): ?>
|
||||||
<span style="background: #fff7ed; color: #ea580c; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #ffedd5; vertical-align: middle;">🏁 <?= tr('hdl_return') ?></span>
|
<span style="background: #fff7ed; color: #ea580c; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #ffedd5; vertical-align: middle;">🏁 <?= tr('hdl_return') ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($step['step_type'] === 'origin'): ?>
|
||||||
|
<span style="background: #ecfdf5; color: #059669; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #d1fae5; vertical-align: middle;">🛫 DÉPART</span>
|
||||||
|
<?php elseif ($step['step_type'] === 'destination'): ?>
|
||||||
|
<span style="background: #fef2f2; color: #e11d48; padding: 2px 6px; border-radius: 4px; font-size: 0.7rem; font-weight: bold; margin-left: 5px; border: 1px solid #fee2e2; vertical-align: middle;">🛬 ARRIVÉE FINALE</span>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
|
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
|
||||||
<div class="hol-date-weather-wrapper">
|
<div class="hol-date-weather-wrapper">
|
||||||
<span class="hol-step-dates">
|
<span class="hol-step-dates">
|
||||||
@@ -261,22 +296,6 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if (count($mapPoints) >= 2): ?>
|
|
||||||
<div class="hol-summary-card" id="hol-cost-panel" style="margin-top:24px;">
|
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; margin-bottom:16px;">
|
|
||||||
<h3 style="margin:0; display:flex; align-items:center; gap:8px;">🚗 Coût du trajet</h3>
|
|
||||||
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
|
|
||||||
<label style="font-size:.82rem; color:var(--text-muted);">Conso (L/100km)</label>
|
|
||||||
<input type="number" id="fuel-l100" value="7" min="3" max="30" step="0.5" style="width:65px; padding:.3rem .5rem; border:1px solid var(--border-light); border-radius:8px; font-size:.85rem;">
|
|
||||||
<label style="font-size:.82rem; color:var(--text-muted);">Prix carburant (€/L)</label>
|
|
||||||
<input type="number" id="fuel-price" value="1.85" min="1" max="4" step="0.01" style="width:65px; padding:.3rem .5rem; border:1px solid var(--border-light); border-radius:8px; font-size:.85rem;">
|
|
||||||
<button onclick="estimateTripCost()" class="pf-btn pf-btn-small">Calculer</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="hol-cost-result" style="color:var(--text-muted); font-size:.9rem;">Cliquez sur Calculer pour estimer le coût du trajet.</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (!empty($holiday['notes'])): ?>
|
<?php if (!empty($holiday['notes'])): ?>
|
||||||
<div class="hol-summary-card" style="margin-top: 24px; padding: 25px; border-left: 5px solid #f59e0b;">
|
<div class="hol-summary-card" style="margin-top: 24px; padding: 25px; border-left: 5px solid #f59e0b;">
|
||||||
<h3 style="margin: 0 0 15px 0; font-size: 1.2rem; color: #0f172a; display: flex; align-items: center; gap: 8px;">
|
<h3 style="margin: 0 0 15px 0; font-size: 1.2rem; color: #0f172a; display: flex; align-items: center; gap: 8px;">
|
||||||
@@ -328,22 +347,32 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-bottom:15px; background:#fff7ed; padding:10px; border-radius:8px; border:1px solid #ffedd5;">
|
||||||
|
<label class="pf-label" style="color:#ea580c;">📍 Type d'étape</label>
|
||||||
|
<select name="step_type" id="cp_step_type" class="pf-input" onchange="toggleStepDates(this.value)">
|
||||||
|
<option value="origin">DÉPART (Point de départ du voyage)</option>
|
||||||
|
<option value="stop">SÉJOUR (Étape classique avec arrivée et départ)</option>
|
||||||
|
<option value="destination">ARRIVÉE FINALE (Fin du voyage)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:15px; margin-bottom:15px; background:#f8fafc; padding:12px; border-radius:8px;">
|
<div style="display:flex; gap:15px; margin-bottom:15px; background:#f8fafc; padding:12px; border-radius:8px;">
|
||||||
<div class="form-group" style="flex:1;">
|
<div class="form-group" id="grp_start_date" style="flex:1;">
|
||||||
<label class="pf-label"><?= tr('hdl_label_arrival') ?></label>
|
<label class="pf-label" id="lbl_start_date"><?= tr('hdl_label_arrival') ?></label>
|
||||||
<input type="date" name="step_start_date" id="cp_start_date" class="pf-input">
|
<input type="date" name="step_start_date" id="cp_start_date" class="pf-input">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="flex:1;">
|
<div class="form-group" id="grp_end_date" style="flex:1;">
|
||||||
<label class="pf-label"><?= tr('hdl_label_departure') ?></label>
|
<label class="pf-label" id="lbl_end_date"><?= tr('hdl_label_departure') ?></label>
|
||||||
<input type="date" name="step_end_date" id="cp_end_date" class="pf-input">
|
<input type="date" name="step_end_date" id="cp_end_date" class="pf-input">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 15px;">
|
<div style="margin-bottom: 20px; padding-top: 15px; border-top: 1px dashed #e2e8f0;">
|
||||||
<label style="display:flex; align-items:center; cursor:pointer; color:#ea580c; font-weight:600;">
|
<label style="display:flex; align-items:center; cursor:pointer; color:#ea580c; font-weight:600; font-size:0.9rem;">
|
||||||
<input type="checkbox" name="is_return" id="cp_is_return" value="1" style="margin-right:8px;">
|
<input type="checkbox" name="set_as_return" id="cp_set_as_return" value="1" style="margin-right:8px; width:16px; height:16px;">
|
||||||
🏁 <?= tr('hdl_return') ?>
|
🏁 Définir comme retour
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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;">
|
||||||
@@ -385,11 +414,11 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
|||||||
<?php include __DIR__ . '/modal.php'; ?>
|
<?php include __DIR__ . '/modal.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 ?? []) ?>;
|
||||||
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
window.appLang = document.documentElement.lang === "ca" ? "ca-ES" : "fr-FR";
|
||||||
|
|
||||||
window.I18N = {
|
window.I18N = {
|
||||||
...(window.I18N || {}),
|
...(window.I18N || {}),
|
||||||
'hdl_js_search_loading': "<?= tr('hdl_js_search_loading') ?>",
|
'hdl_js_search_loading': "<?= tr('hdl_js_search_loading') ?>",
|
||||||
'hdl_js_no_result': "<?= tr('hdl_js_no_result') ?>",
|
'hdl_js_no_result': "<?= tr('hdl_js_no_result') ?>",
|
||||||
@@ -409,7 +438,6 @@ window.I18N = {
|
|||||||
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>",
|
'hdl_quick_edit_title': "<?= tr('hdl_quick_edit_title') ?>",
|
||||||
'hdl_paid': "<?= tr('hdl_paid') ?>",
|
'hdl_paid': "<?= tr('hdl_paid') ?>",
|
||||||
|
|
||||||
|
|
||||||
// --- NOUVELLES CLÉS MÉTÉO ICI ---
|
// --- NOUVELLES CLÉS MÉTÉO ICI ---
|
||||||
'weather_sunny': "<?= tr('weather_sunny') ?>",
|
'weather_sunny': "<?= tr('weather_sunny') ?>",
|
||||||
'weather_cloudy': "<?= tr('weather_cloudy') ?>",
|
'weather_cloudy': "<?= tr('weather_cloudy') ?>",
|
||||||
@@ -417,77 +445,33 @@ window.I18N = {
|
|||||||
'weather_snowy': "<?= tr('weather_snowy') ?>",
|
'weather_snowy': "<?= tr('weather_snowy') ?>",
|
||||||
'weather_forecast': "<?= tr('weather_forecast') ?>",
|
'weather_forecast': "<?= tr('weather_forecast') ?>",
|
||||||
'weather_historical': "<?= tr('weather_historical') ?>"
|
'weather_historical': "<?= tr('weather_historical') ?>"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fallback de sécurité pour s'assurer que les modales peuvent toujours se fermer
|
// Fallback de sécurité pour s'assurer que les modales peuvent toujours se fermer
|
||||||
window.closeCheckpointModal = window.closeCheckpointModal || function() {
|
window.closeCheckpointModal = 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 = 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');
|
||||||
};
|
};
|
||||||
|
|
||||||
async function estimateTripCost() {
|
// 1. On charge le prix depuis le navigateur (ou 1.85 par défaut)
|
||||||
const l100 = parseFloat(document.getElementById('fuel-l100').value) || 7;
|
const savedFuelPrice = localStorage.getItem('holidays_fuel_price') || 1.85;
|
||||||
const price = parseFloat(document.getElementById('fuel-price').value) || 1.85;
|
window.FUEL_PRICE = parseFloat(savedFuelPrice);
|
||||||
const result = document.getElementById('hol-cost-result');
|
|
||||||
result.innerHTML = '⏳ Calcul en cours…';
|
|
||||||
|
|
||||||
const stops = (window.MAP_POINTS || []).map(p => ({lat: p.lat, lng: p.lng, name: p.location_name}));
|
// 2. On met à jour le texte du petit bouton "✏️" en haut de la page
|
||||||
if (stops.length < 2) { result.innerHTML = 'Pas assez d\'étapes pour calculer.'; return; }
|
const displayFuelEl = document.getElementById('display_fuel_price');
|
||||||
|
if (displayFuelEl) displayFuelEl.innerText = window.FUEL_PRICE.toFixed(2);
|
||||||
|
|
||||||
try {
|
// 3. Variables voiture et retour
|
||||||
const resp = await fetch('/modules/voyage/api.php?action=estimate', {
|
window.VEHICLE_CONSUMPTION = <?= !empty($holiday['vehicle_consumption']) ? (float)$holiday['vehicle_consumption'] : 7 ?>;
|
||||||
method: 'POST',
|
window.GLOBAL_RETURN_STEP_ID = <?= $holiday['return_step_id'] !== null ? $holiday['return_step_id'] : 'null' ?>;
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({stops, fuel_l100: l100, fuel_price: price})
|
|
||||||
});
|
|
||||||
const data = await resp.json();
|
|
||||||
if (!data.ok) { result.innerHTML = 'Erreur : ' + (data.error || 'inconnue'); return; }
|
|
||||||
|
|
||||||
let html = '<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; margin-bottom:16px;">';
|
|
||||||
html += `<div class="hol-cost-stat"><div class="hol-cost-stat-val">${Math.round(data.total_km)} km</div><div class="hol-cost-stat-label">Distance totale</div></div>`;
|
|
||||||
html += `<div class="hol-cost-stat"><div class="hol-cost-stat-val">${data.total_toll.toFixed(2)} €</div><div class="hol-cost-stat-label">Péages estimés</div></div>`;
|
|
||||||
html += `<div class="hol-cost-stat"><div class="hol-cost-stat-val">${data.fuel_cost.toFixed(2)} €</div><div class="hol-cost-stat-label">Carburant</div></div>`;
|
|
||||||
html += `<div class="hol-cost-stat hol-cost-total"><div class="hol-cost-stat-val">${data.grand_total.toFixed(2)} €</div><div class="hol-cost-stat-label">Total aller</div></div>`;
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
if (data.segments && data.segments.length) {
|
|
||||||
html += '<div style="border-top:1px solid var(--border-light); padding-top:12px; margin-top:4px;">';
|
|
||||||
html += '<div style="font-size:.75rem; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--text-muted); margin-bottom:10px;">Détail du trajet</div>';
|
|
||||||
data.segments.forEach(s => {
|
|
||||||
html += `<div style="padding:8px 0; border-bottom:1px solid var(--border-light);">`;
|
|
||||||
html += `<div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:4px;">
|
|
||||||
<span style="font-weight:600; font-size:.88rem;">📍 ${esc(s.from)} → ${esc(s.to)}</span>
|
|
||||||
<span style="font-size:.88rem; color:var(--primary);">péage : <strong>${s.toll.toFixed(2)} €</strong></span>
|
|
||||||
</div>`;
|
|
||||||
html += `<div style="font-size:.78rem; color:var(--text-muted);">🛣️ ${Math.round(s.distance_km)} km`;
|
|
||||||
if (s.entry_plaza && s.exit_plaza) {
|
|
||||||
html += ` · ${esc(s.entry_plaza)} → ${esc(s.exit_plaza)}`;
|
|
||||||
if (s.op) html += ` <span style="background:var(--bg-page); border:1px solid var(--border-light); border-radius:4px; padding:0 4px; font-size:.7rem;">${esc(s.op)}</span>`;
|
|
||||||
} else if (s.note) {
|
|
||||||
html += ` · <em>${esc(s.note)}</em>`;
|
|
||||||
}
|
|
||||||
html += '</div></div>';
|
|
||||||
});
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function esc(s) {
|
|
||||||
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
||||||
}
|
|
||||||
|
|
||||||
result.innerHTML = html;
|
|
||||||
} catch(e) {
|
|
||||||
result.innerHTML = 'Erreur de connexion.';
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/modules/holidays/holidays.js"></script>
|
<script src="/modules/holidays/holidays.js?v=<?= time() ?>"></script>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<div id="holidayModal" class="pf-modal">
|
<div id="holidayModal" class="pf-modal">
|
||||||
<div class="pf-modal-content hol-modal-content">
|
<div class="pf-modal-content hol-modal-content" style="max-width: 500px;"> <h3 id="modalTitle" class="pf-modal-title"><?= tr('hdl_modal_title') ?></h3>
|
||||||
<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">
|
||||||
@@ -40,32 +39,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="hol-divider">
|
<div class="form-group" style="margin-top: 15px; padding: 10px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;">
|
||||||
|
<label class="pf-label" style="margin-bottom: 5px;">🚗 Véhicule utilisé (Optionnel)</label>
|
||||||
<div class="hol-columns-wrapper">
|
<select name="vehicle_id" id="inp_vehicle_id" class="pf-input">
|
||||||
<div class="hol-col">
|
<option value="">-- Aucun / Autre transport --</option>
|
||||||
<div class="hol-col-header">
|
<?php foreach($garageVehicles as $v): ?>
|
||||||
<h4 class="hol-cat-transport">🚗 <?= tr('hdl_cat_transport') ?></h4>
|
<option value="<?= $v['id'] ?>"><?= htmlspecialchars($v['name']) ?></option>
|
||||||
<button type="button" class="btn-add-item" onclick="addItem('transport')" title="<?= tr('hdl_add_transport') ?>">+</button>
|
<?php endforeach; ?>
|
||||||
</div>
|
</select>
|
||||||
<div id="list_transport" class="dynamic-list"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-col">
|
|
||||||
<div class="hol-col-header">
|
|
||||||
<h4 class="hol-cat-accommodation">🏨 <?= tr('hdl_cat_accommodation') ?></h4>
|
|
||||||
<button type="button" class="btn-add-item" onclick="addItem('accommodation')" title="<?= tr('hdl_add_accommodation') ?>">+</button>
|
|
||||||
</div>
|
|
||||||
<div id="list_accommodation" class="dynamic-list"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hol-col">
|
|
||||||
<div class="hol-col-header">
|
|
||||||
<h4 class="hol-cat-activity">🎫 <?= tr('hdl_cat_activity') ?></h4>
|
|
||||||
<button type="button" class="btn-add-item" onclick="addItem('activity')" title="<?= tr('hdl_add_activity') ?>">+</button>
|
|
||||||
</div>
|
|
||||||
<div id="list_activity" class="dynamic-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="hol-divider">
|
<hr class="hol-divider">
|
||||||
@@ -81,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group" style="margin-top: 10px;">
|
||||||
<label class="pf-label"><?= tr('hdl_label_notes') ?></label>
|
<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>
|
<textarea name="notes" id="inp_notes" class="pf-input" rows="2" placeholder="<?= tr('hdl_ph_notes') ?>"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user