gps expense
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-07-29 14:58:50 +02:00
parent ebdbee71db
commit f748330a1b
6 changed files with 26456 additions and 34 deletions
+95 -14
View File
@@ -598,6 +598,8 @@ function openCheckpointModal(mode, data = null) {
it.item_time || "", it.item_time || "",
it.duration || 1, it.duration || 1,
it.expense_context || "local", it.expense_context || "local",
it.lat || "",
it.lng || "",
); );
visibleCount++; visibleCount++;
} }
@@ -792,49 +794,128 @@ function addCpExpenseLine(
itemTime = "", itemTime = "",
itemDur = 1, itemDur = 1,
expenseContext = "local", expenseContext = "local",
itemLat = "",
itemLng = "",
) { ) {
const container = document.getElementById("cpExpensesContainer"); const container = document.getElementById("cpExpensesContainer");
const div = document.createElement("div"); const div = document.createElement("div");
div.className = "hol-form-row"; div.className = "hol-form-row";
const isChecked = isPaid == 1 ? "checked" : ""; const isChecked = isPaid == 1 ? "checked" : "";
// 💡 Nouveaux types (Les valeurs à 0 par défaut pour les visites gratuites) const pinBtnStyle =
let defaultAmount = amount; itemLat && itemLng
if (category === "visit_free" && amount === "") defaultAmount = "0.00"; ? "background:var(--success);color:white;border-color:var(--success);"
: "";
div.innerHTML = ` div.innerHTML = `
<div class="hol-form-inner"> <div class="hol-form-inner">
<select name="items[cat][]" class="pf-input hol-form-select" style="width:55px; padding:6px; font-size:1.1rem;" title="Catégorie"> <select name="items[cat][]" class="pf-input hol-form-select">
<option value="accommodation" ${category === "accommodation" ? "selected" : ""}>🏨</option> <option value="accommodation" ${category === "accommodation" ? "selected" : ""}>🏨</option>
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option> <option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
<option value="food" ${category === "food" ? "selected" : ""}>🍽️</option>
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option> <option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
<option value="visit_free" ${category === "visit_free" ? "selected" : ""}>🏞️</option>
<option value="other" ${category === "other" ? "selected" : ""}>🛍️</option>
</select> </select>
<input type="hidden" name="items[context][]" value="${expenseContext}"> <input type="hidden" name="items[context][]" value="${expenseContext}">
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="Description (Ex: Musée, Restaurant...)" 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="${defaultAmount}">
<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;${pinBtnStyle}" onclick="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}">
<label class="hol-form-paid-label" title="${tr("hdl_paid")}"> <label class="hol-form-paid-label" title="${tr("hdl_paid")}">
<input type="checkbox" ${isChecked} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="accent-color:var(--success); width:16px; height:16px;"> <input type="checkbox" ${isChecked} onchange="this.nextElementSibling.value = this.checked ? 1 : 0">
<input type="hidden" name="items[paid][]" value="${isPaid}"> <input type="hidden" name="items[paid][]" value="${isPaid}">
<span class="hol-form-paid-text" style="display:none;">${tr("hdl_paid")}</span> <span class="hol-form-paid-text">${tr("hdl_paid")}</span>
</label> </label>
<button type="button" class="btn-icon-action delete btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="${tr("btn_delete")}">🗑️</button> <button type="button" class="btn-icon-action delete btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="${tr("btn_delete")}">🗑️</button>
</div> </div>
<div class="hol-form-subrow" style="padding-left:0; margin-top:4px;">
<input type="text" name="items[notes][]" class="pf-input hol-form-notes-input hol-form-notes-full" placeholder="🔗 Réservation ou notes..." value="${notes}" style="margin-left:0; width:100%; border-radius:6px;"> <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') { searchItemPlace(this); return false; }">
<button type="button" class="pf-btn btn-secondary" onclick="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">
<input type="text" name="items[notes][]" class="pf-input hol-form-notes-input hol-form-notes-full" placeholder="${tr("hdl_ph_notes")}" value="${notes}">
</div> </div>
<input type="hidden" name="items[id][]" value="${itemId}"> <input type="hidden" name="items[id][]" value="${itemId}">
<input type="hidden" name="items[date][]" value="${itemDate}"> <input type="hidden" name="items[date][]" value="${itemDate}">
<input type="hidden" name="items[time][]" value="${itemTime}"> <input type="hidden" name="items[time][]" value="${itemTime}">
<input type="hidden" name="items[duration][]" value="${itemDur}"> <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); container.appendChild(div);
} }
function toggleItemSearch(btn) {
const searchBox = btn
.closest(".hol-form-row")
.querySelector(".hol-item-search-box");
searchBox.style.display =
searchBox.style.display === "none" ? "block" : "none";
}
function searchItemPlace(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>`;
fetch(
"/modules/holidays/includes/api/geocode.php?limit=5&q=" +
encodeURIComponent(q),
)
.then((res) => res.json())
.then((data) => {
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 = () =>
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>`;
});
}
function selectItemPlace(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;
const notesInput = row.querySelector('input[name="items[notes][]"]');
if (notesInput.value === "") {
notesInput.value = displayName;
}
container.style.display = "none";
const pinBtn = row.querySelector('button[title="Adresse précise"]');
if (pinBtn) {
pinBtn.style.background = "var(--success)";
pinBtn.style.color = "white";
pinBtn.style.borderColor = "var(--success)";
}
}
function deleteCheckpoint() { function deleteCheckpoint() {
if (!confirm(tr("hdl_js_confirm_del_step"))) return; if (!confirm(tr("hdl_js_confirm_del_step"))) return;
const form = document.getElementById("formCheckpoint"); const form = document.getElementById("formCheckpoint");
@@ -170,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++;
} }
} }
+18 -1
View File
@@ -292,15 +292,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'] ?>)" style="cursor:pointer; color:var(--primary); text-decoration:underline; font-weight:600;" 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" style="font-size: 0.75rem; color: var(--text-muted); margin-top: 2px;">
<?= htmlspecialchars($it['notes']) ?>
</div>
<?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
+5 -15
View File
@@ -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