holidays
This commit is contained in:
+279
-38
@@ -1,15 +1,21 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Initialisation des écouteurs globaux si besoin
|
||||
// Fermer les modales si on clique en dehors du contenu
|
||||
window.onclick = function (event) {
|
||||
const modal = document.getElementById("holidayModal");
|
||||
if (event.target == modal) {
|
||||
closeHolidayModal();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// --- 1. GESTION DE LA MODALE D'ÉDITION ---
|
||||
// --- 1. GESTION DE LA MODALE D'ÉDITION RAPIDE ---
|
||||
|
||||
function openHolidayModal(mode) {
|
||||
const modal = document.getElementById("holidayModal");
|
||||
const form = document.getElementById("holidayForm");
|
||||
const btnDelete = document.getElementById("btn_delete");
|
||||
|
||||
// Reset complet
|
||||
// Reset complet pour éviter les résidus d'une carte précédente
|
||||
form.reset();
|
||||
document.getElementById("inp_id").value = "";
|
||||
document.getElementById("list_transport").innerHTML = "";
|
||||
@@ -17,51 +23,46 @@ function openHolidayModal(mode) {
|
||||
document.getElementById("list_activity").innerHTML = "";
|
||||
|
||||
if (mode === "add") {
|
||||
document.getElementById("modalTitle").innerText = "Nouveau Voyage";
|
||||
document.getElementById("modalTitle").innerText = "Planifier le voyage";
|
||||
btnDelete.style.display = "none";
|
||||
} else {
|
||||
document.getElementById("modalTitle").innerText = "Modifier le voyage";
|
||||
document.getElementById("modalTitle").innerText = "Modification rapide";
|
||||
btnDelete.style.display = "block";
|
||||
}
|
||||
|
||||
modal.classList.add("open");
|
||||
modal.style.display = "flex";
|
||||
|
||||
// --- AJOUT : FORCER LE SCROLL EN HAUT ---
|
||||
const content = modal.querySelector(".pf-modal-content");
|
||||
if (content) {
|
||||
content.scrollTop = 0;
|
||||
}
|
||||
// Optionnel : Mettre le focus sur le premier champ (Titre)
|
||||
// setTimeout(() => document.getElementById('inp_title').focus(), 50);
|
||||
// Focus sur le champ titre pour une saisie rapide (petit délai pour l'animation d'ouverture)
|
||||
setTimeout(() => document.getElementById("inp_title").focus(), 100);
|
||||
}
|
||||
|
||||
function closeHolidayModal() {
|
||||
const modal = document.getElementById("holidayModal");
|
||||
modal.classList.remove("open");
|
||||
modal.style.display = "none";
|
||||
document.getElementById("holidayModal").style.display = "none";
|
||||
}
|
||||
|
||||
// Fonction appelée au clic sur une carte (injectée par PHP)
|
||||
// Fonction appelée au clic sur le bouton ✏️ de la carte (injectée par PHP)
|
||||
function editHoliday(data) {
|
||||
openHolidayModal("edit");
|
||||
|
||||
const h = data.main;
|
||||
|
||||
// On ouvre la modale en mode édition (ça reset les listes)
|
||||
openHolidayModal("edit");
|
||||
|
||||
// Remplissage des champs principaux
|
||||
document.getElementById("inp_id").value = h.id;
|
||||
document.getElementById("inp_title").value = h.title;
|
||||
document.getElementById("inp_status").value = h.status;
|
||||
document.getElementById("inp_period").value = h.period_hint;
|
||||
document.getElementById("inp_start").value = h.start_date;
|
||||
document.getElementById("inp_end").value = h.end_date;
|
||||
document.getElementById("inp_period").value = h.period_hint || "";
|
||||
document.getElementById("inp_start").value = h.start_date || "";
|
||||
document.getElementById("inp_end").value = h.end_date || "";
|
||||
|
||||
// Pour éviter d'afficher "0" dans un champ vide, on vérifie si la valeur est > 0
|
||||
document.getElementById("inp_food").value =
|
||||
h.budget_food > 0 ? h.budget_food : "";
|
||||
document.getElementById("inp_extra").value =
|
||||
h.budget_extra > 0 ? h.budget_extra : "";
|
||||
document.getElementById("inp_notes").value = h.notes;
|
||||
document.getElementById("inp_notes").value = h.notes || "";
|
||||
|
||||
// Remplissage des listes dynamiques
|
||||
// Remplissage des listes dynamiques (Transport, Hébergement, Activité)
|
||||
if (data.items && data.items.length > 0) {
|
||||
data.items.forEach((item) => {
|
||||
addItem(item.category, item.name, item.amount, item.is_paid);
|
||||
@@ -69,36 +70,40 @@ function editHoliday(data) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. GESTION DES LISTES DYNAMIQUES (Transport, etc.) ---
|
||||
// --- 2. GESTION DES LISTES DYNAMIQUES DANS LA MODALE ---
|
||||
|
||||
function addItem(category, name = "", amount = "", isPaid = 0) {
|
||||
const container = document.getElementById("list_" + category);
|
||||
const div = document.createElement("div");
|
||||
|
||||
// Utilisation des classes CSS définies précédemment
|
||||
div.className = "savings-line-item";
|
||||
// Style en ligne pour s'assurer que ça reste propre sans dépendre de classes externes complexes
|
||||
div.style.display = "flex";
|
||||
div.style.gap = "8px";
|
||||
div.style.alignItems = "center";
|
||||
div.style.marginBottom = "10px";
|
||||
|
||||
// Checkbox logique pour "Payé"
|
||||
// Astuce pour lier la checkbox visuelle à l'input caché (valeur 0 ou 1 pour MySQL)
|
||||
const checkedAttr = isPaid == 1 ? "checked" : "";
|
||||
|
||||
div.innerHTML = `
|
||||
<input type="hidden" name="items[cat][]" value="${category}">
|
||||
|
||||
<input type="text" name="items[name][]" class="pf-input"
|
||||
placeholder="Nom (ex: Vol)" value="${name}"
|
||||
style="flex: 2; min-width: 0;">
|
||||
placeholder="Intitulé" value="${name}"
|
||||
style="flex: 2; padding: 8px; font-size:0.9rem;" required>
|
||||
|
||||
<input type="number" step="0.01" name="items[amount][]" class="pf-input"
|
||||
placeholder="€" value="${amount}"
|
||||
style="width: 80px; text-align: right;">
|
||||
placeholder="Prix (€)" value="${amount}"
|
||||
style="width: 80px; text-align: right; padding: 8px; font-size:0.9rem;">
|
||||
|
||||
<label title="Déjà payé ?" style="display: flex; align-items: center; cursor: pointer; padding: 0 5px;">
|
||||
<input type="checkbox" ${checkedAttr} onchange="this.nextElementSibling.value = this.checked ? 1 : 0">
|
||||
<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.8rem; margin-left:4px;">Payé</span>
|
||||
<span style="font-size:0.75rem; margin-left:4px; font-weight:bold; color:#64748b;">Payé</span>
|
||||
</label>
|
||||
|
||||
<button type="button" class="btn-remove" onclick="this.parentElement.remove()" title="Supprimer">
|
||||
<button type="button" onclick="this.parentElement.remove()" title="Retirer cette ligne"
|
||||
style="width: 28px; height: 28px; border: none; background: #fee2e2; color: #ef4444; border-radius: 6px; cursor: pointer; font-weight: bold; display:flex; align-items:center; justify-content:center;">
|
||||
×
|
||||
</button>
|
||||
`;
|
||||
@@ -107,11 +112,16 @@ function addItem(category, name = "", amount = "", isPaid = 0) {
|
||||
}
|
||||
|
||||
function deleteHoliday() {
|
||||
if (!confirm("Supprimer définitivement ce voyage ?")) return;
|
||||
if (
|
||||
!confirm(
|
||||
"Voulez-vous vraiment supprimer définitivement ce voyage ? Cette action est irréversible.",
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
const form = document.getElementById("holidayForm");
|
||||
|
||||
// On ajoute un input caché pour signaler la suppression au PHP
|
||||
// On injecte un input caché pour signaler au PHP que c'est une demande de suppression
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = "action_delete";
|
||||
@@ -121,7 +131,7 @@ function deleteHoliday() {
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// --- 3. GESTION DE LA CARTE (Leaflet) ---
|
||||
// --- 3. GESTION DE LA CARTE (Leaflet - Optionnel selon si tu l'utilises ou non) ---
|
||||
|
||||
let map = null;
|
||||
|
||||
@@ -172,3 +182,234 @@ function initMap() {
|
||||
});
|
||||
}
|
||||
}
|
||||
// ============================================================================
|
||||
// 4. GESTION DE LA CARTE DÉTAILLÉE (ROADTRIP) ET GÉOCODAGE
|
||||
// ============================================================================
|
||||
|
||||
let detailMap = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Si on est sur la page détail et que la div "tripMap" existe, on initie la carte
|
||||
if (document.getElementById("tripMap")) {
|
||||
initDetailMap();
|
||||
}
|
||||
});
|
||||
|
||||
function initDetailMap() {
|
||||
if (typeof L === "undefined" || typeof MAP_POINTS === "undefined") return;
|
||||
|
||||
detailMap = L.map("tripMap");
|
||||
|
||||
// Fond de carte propre (CartoDB Voyager)
|
||||
L.tileLayer(
|
||||
"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
|
||||
{
|
||||
attribution: "© OpenStreetMap",
|
||||
},
|
||||
).addTo(detailMap);
|
||||
|
||||
if (MAP_POINTS.length === 0) {
|
||||
detailMap.setView([46.6, 2.4], 5); // Centré sur la France par défaut si vide
|
||||
return;
|
||||
}
|
||||
|
||||
const latlngs = [];
|
||||
const bounds = L.latLngBounds();
|
||||
|
||||
MAP_POINTS.forEach((pt, index) => {
|
||||
const pos = [pt.lat, pt.lng];
|
||||
latlngs.push(pos);
|
||||
bounds.extend(pos);
|
||||
|
||||
// Couleur selon le paiement
|
||||
const color = pt.paid == 1 ? "#10b981" : "#f59e0b";
|
||||
|
||||
// On place un petit cercle pour chaque point
|
||||
const marker = L.circleMarker(pos, {
|
||||
color: color,
|
||||
radius: 7,
|
||||
fillOpacity: 1,
|
||||
fillColor: "white",
|
||||
weight: 3,
|
||||
}).addTo(detailMap);
|
||||
|
||||
// Numérotation et Popup
|
||||
marker.bindPopup(`
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:0.7rem; color:#64748b; margin-bottom:2px;">Étape ${index + 1}</div>
|
||||
<strong>${pt.title}</strong><br>
|
||||
<span style="font-weight:bold; color:${color};">${parseFloat(pt.amount).toFixed(2)} €</span>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
// On dessine le trait en pointillés pour relier le roadtrip !
|
||||
if (latlngs.length > 1) {
|
||||
L.polyline(latlngs, {
|
||||
color: "#3b82f6",
|
||||
weight: 3,
|
||||
dashArray: "8, 8",
|
||||
opacity: 0.7,
|
||||
}).addTo(detailMap);
|
||||
}
|
||||
|
||||
// On zoome automatiquement pour voir tous les points
|
||||
detailMap.fitBounds(bounds, { padding: [50, 50] });
|
||||
}
|
||||
|
||||
// Fonction appelée quand on clique sur "👁️ Voir sur la carte" dans la liste
|
||||
function panMapTo(lat, lng) {
|
||||
if (detailMap) {
|
||||
detailMap.setView([lat, lng], 14, { animate: true });
|
||||
}
|
||||
}
|
||||
|
||||
// --- LOGIQUE DE LA MODALE CHECKPOINT (Lignes multiples) ---
|
||||
|
||||
function openCheckpointModal(mode, data = null) {
|
||||
const searchBlock = document.getElementById("cpSearchBlock");
|
||||
const formBlock = document.getElementById("formCheckpoint");
|
||||
const container = document.getElementById("cpExpensesContainer");
|
||||
const btnDel = document.getElementById("btnDeleteCp");
|
||||
|
||||
container.innerHTML = ""; // Reset des lignes
|
||||
|
||||
if (mode === "add") {
|
||||
document.getElementById("cpModalTitle").innerText =
|
||||
"📍 Placer une nouvelle étape";
|
||||
searchBlock.style.display = "block";
|
||||
formBlock.style.display = "none";
|
||||
btnDel.style.display = "none";
|
||||
|
||||
document.getElementById("searchPlaceInput").value = "";
|
||||
document.getElementById("searchResults").innerHTML = "";
|
||||
|
||||
document.getElementById("cp_old_name").value = "";
|
||||
document.getElementById("cp_name").value = "";
|
||||
|
||||
// Ajout d'une ligne vide par défaut
|
||||
addCpExpenseLine();
|
||||
} else if (mode === "edit" && data) {
|
||||
document.getElementById("cpModalTitle").innerText = "✏️ Modifier l'étape";
|
||||
searchBlock.style.display = "none"; // On cache la recherche en mode édition
|
||||
formBlock.style.display = "block";
|
||||
btnDel.style.display = "block";
|
||||
|
||||
document.getElementById("cp_lat").value = data.lat;
|
||||
document.getElementById("cp_lng").value = data.lng;
|
||||
document.getElementById("cp_old_name").value = data.location_name;
|
||||
document.getElementById("cp_name").value = data.location_name;
|
||||
|
||||
// Remplissage des lignes existantes (en filtrant le point technique)
|
||||
if (data.items && data.items.length > 0) {
|
||||
let visibleCount = 0;
|
||||
data.items.forEach((it) => {
|
||||
if (it.name !== "PF_TECHNICAL_POINT") {
|
||||
addCpExpenseLine(it.category, it.name, it.amount, it.is_paid);
|
||||
visibleCount++;
|
||||
}
|
||||
});
|
||||
// Si l'étape ne contenait QUE le point technique, on affiche une ligne vide pour inviter à la saisie
|
||||
if (visibleCount === 0) {
|
||||
addCpExpenseLine();
|
||||
}
|
||||
} else {
|
||||
addCpExpenseLine(); // Sécurité
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("checkpointModal").style.display = "flex";
|
||||
}
|
||||
|
||||
function searchPlace() {
|
||||
const q = document.getElementById("searchPlaceInput").value.trim();
|
||||
if (q.length < 3) return;
|
||||
|
||||
const resultsDiv = document.getElementById("searchResults");
|
||||
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 trouvé.</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.results.forEach((place) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "pf-btn btn-secondary";
|
||||
btn.style.textAlign = "left";
|
||||
btn.style.padding = "8px";
|
||||
btn.style.height = "auto";
|
||||
btn.innerText = "📍 " + place.display_name;
|
||||
btn.onclick = () =>
|
||||
selectPlace(place.lat, place.lng, place.display_name);
|
||||
resultsDiv.appendChild(btn);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
resultsDiv.innerHTML =
|
||||
'<span style="color:#ef4444; font-size:0.85rem;">Erreur réseau.</span>';
|
||||
});
|
||||
}
|
||||
|
||||
function selectPlace(lat, lng, fullName) {
|
||||
document.getElementById("cp_lat").value = lat;
|
||||
document.getElementById("cp_lng").value = lng;
|
||||
// On nettoie le nom pour que ce soit joli (ex: "Paris, France" -> "Paris")
|
||||
document.getElementById("cp_name").value = fullName.split(",")[0].trim();
|
||||
document.getElementById("cpSearchBlock").style.display = "none";
|
||||
document.getElementById("formCheckpoint").style.display = "block";
|
||||
}
|
||||
|
||||
function addCpExpenseLine(
|
||||
category = "accommodation",
|
||||
name = "",
|
||||
amount = "",
|
||||
isPaid = 0,
|
||||
) {
|
||||
const container = document.getElementById("cpExpensesContainer");
|
||||
const div = document.createElement("div");
|
||||
div.style.display = "flex";
|
||||
div.style.gap = "8px";
|
||||
div.style.alignItems = "center";
|
||||
|
||||
const isChecked = isPaid == 1 ? "checked" : "";
|
||||
|
||||
div.innerHTML = `
|
||||
<select name="items[cat][]" class="pf-input" style="width:50px; padding:8px 4px; font-size:1.2rem; cursor:pointer;" title="Catégorie">
|
||||
<option value="accommodation" ${category === "accommodation" ? "selected" : ""}>🏨</option>
|
||||
<option value="transport" ${category === "transport" ? "selected" : ""}>🚗</option>
|
||||
<option value="activity" ${category === "activity" ? "selected" : ""}>🎫</option>
|
||||
</select>
|
||||
<input type="text" name="items[name][]" class="pf-input" placeholder="Libellé (Optionnel)" value="${name}" style="flex:2; padding:8px; font-size:0.9rem;">
|
||||
<input type="number" step="0.01" name="items[amount][]" class="pf-input" placeholder="0.00" value="${amount}" style="width:80px; text-align:right; padding:8px; font-size:0.9rem;">
|
||||
<label title="Payé ?" style="display:flex; align-items:center; cursor:pointer;">
|
||||
<input type="checkbox" ${isChecked} 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:2px; font-weight:bold; color:#64748b;">Payé</span>
|
||||
</label>
|
||||
<button type="button" onclick="this.parentElement.remove()" style="width:28px; height:28px; border:none; background:#fee2e2; color:#ef4444; border-radius:6px; cursor:pointer; font-weight:bold; display:flex; justify-content:center; align-items:center;">×</button>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function deleteCheckpoint() {
|
||||
if (!confirm("Supprimer cette étape et toutes les dépenses associées ?"))
|
||||
return;
|
||||
const form = document.getElementById("formCheckpoint");
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = "action_delete";
|
||||
input.value = "1";
|
||||
form.appendChild(input);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
// modules/holidays/geocode.php
|
||||
require __DIR__ . '/../../includes/auth.php';
|
||||
require_login('/login.php');
|
||||
require __DIR__ . '/../../includes/db.php';
|
||||
// modules/holidays/includes/api/geocode.php
|
||||
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
@@ -17,13 +18,13 @@ if ($q === '') {
|
||||
// Borner la limite
|
||||
$limit = (int)($_GET['limit'] ?? 1);
|
||||
if ($limit < 1) $limit = 1;
|
||||
if ($limit > 10) $limit = 10; // Nominatim bloque souvent au-dessus de 10-50
|
||||
if ($limit > 10) $limit = 10;
|
||||
|
||||
// 2. Normalisation pour le cache
|
||||
$qNorm = mb_strtolower($q);
|
||||
$qHash = hash('sha256', $qNorm);
|
||||
|
||||
// 3. Création automatique de la table cache si elle n'existe pas (Sécurité)
|
||||
// 3. Création automatique de la table cache
|
||||
try {
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS pf_geocode_cache (
|
||||
@@ -35,13 +36,9 @@ try {
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
");
|
||||
} catch (Throwable $e) {
|
||||
// On continue même si ça échoue (l'admin devra créer la table manuellement)
|
||||
}
|
||||
} catch (Throwable $e) {}
|
||||
|
||||
// 4. Vérification du cache (Même si limit > 1)
|
||||
// Stratégie : Si on a déjà cherché exactement "Paris", on renvoie le résultat stocké
|
||||
// pour économiser l'API et aller plus vite. Le JS gérera le résultat unique.
|
||||
// 4. Vérification du cache
|
||||
try {
|
||||
$st = $pdo->prepare("SELECT lat, lng, display_name FROM pf_geocode_cache WHERE q_hash = ?");
|
||||
$st->execute([$qHash]);
|
||||
@@ -54,11 +51,9 @@ try {
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// Erreur SQL silencieuse sur le cache
|
||||
}
|
||||
} catch (Throwable $e) {}
|
||||
|
||||
// 5. Appel Nominatim (Si pas en cache)
|
||||
// 5. Appel Nominatim
|
||||
$endpoint = 'https://nominatim.openstreetmap.org/search';
|
||||
$params = http_build_query([
|
||||
'format' => 'jsonv2',
|
||||
@@ -75,8 +70,6 @@ curl_setopt_array($ch, [
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
// Ton User-Agent est correct.
|
||||
// Important : Nominatim demande une identification claire.
|
||||
'User-Agent: PachaFamily-Holidays/1.0 (+contact: ferlan.alexandre@gmail.com)'
|
||||
],
|
||||
]);
|
||||
@@ -85,9 +78,8 @@ $http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
// Gestion erreur cURL / HTTP
|
||||
if ($body === false || $http !== 200) {
|
||||
http_response_code(502); // Bad Gateway
|
||||
http_response_code(502);
|
||||
echo json_encode([
|
||||
'error' => 'geocode_failed',
|
||||
'details' => $err ?: ('HTTP ' . $http)
|
||||
@@ -97,17 +89,13 @@ if ($body === false || $http !== 200) {
|
||||
|
||||
$data = json_decode($body, true);
|
||||
|
||||
// Gestion erreur JSON ou vide
|
||||
if (!is_array($data) || empty($data)) {
|
||||
// On renvoie 200 avec une liste vide ou 404, le JS gère les deux.
|
||||
// 404 est plus sémantique "Not Found".
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'not_found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 6. Mise en cache du PREMIER résultat (Le "meilleur")
|
||||
// On ne cache que le top result pour simplifier la structure de la DB.
|
||||
// 6. Mise en cache
|
||||
if (isset($data[0])) {
|
||||
$r = $data[0];
|
||||
$lat = round((float)$r['lat'], 6);
|
||||
@@ -124,9 +112,7 @@ if (isset($data[0])) {
|
||||
} catch (Throwable $e) {}
|
||||
}
|
||||
|
||||
// 7. Retour des résultats
|
||||
// Si limit=1, on renvoie format plat (pour compatibilité stricte)
|
||||
// Si limit>1, on renvoie format liste
|
||||
// 7. Retour
|
||||
if ($limit === 1) {
|
||||
$r = $data[0];
|
||||
echo json_encode([
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
// modules/holidays/view.php
|
||||
// modules/holidays/includes/api/get_holiday_data.php
|
||||
|
||||
require __DIR__ . '/../../includes/auth.php';
|
||||
require_login('/login.php');
|
||||
require __DIR__ . '/../../includes/db.php';
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
@@ -16,6 +16,7 @@ if ($id <= 0) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Si tu utilises pf_holidays au lieu de pf_holidays_ideas, change le nom de la table ici !
|
||||
$st = $pdo->prepare("SELECT * FROM pf_holidays_ideas WHERE id = ?");
|
||||
$st->execute([$id]);
|
||||
$it = $st->fetch(PDO::FETCH_ASSOC);
|
||||
@@ -26,17 +27,12 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Amélioration : Typage explicite pour le JSON
|
||||
// Cela évite que JS reçoive "4" (string) au lieu de 4 (int) pour les calculs
|
||||
$it['id'] = (int)$it['id'];
|
||||
|
||||
if (isset($it['lat'])) $it['lat'] = (float)$it['lat'];
|
||||
if (isset($it['lng'])) $it['lng'] = (float)$it['lng'];
|
||||
if (isset($it['ideal_days'])) $it['ideal_days'] = (int)$it['ideal_days'];
|
||||
|
||||
// On s'assure que les null restent null et pas des chaines vides si la DB est stricte
|
||||
// (Optionnel mais propre)
|
||||
|
||||
echo json_encode($it, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
// modules/holidays/includes/api/save_checkpoint.php
|
||||
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login();
|
||||
|
||||
$holiday_id = (int)$_POST['holiday_id'];
|
||||
|
||||
// Suppression complète d'une étape
|
||||
if (isset($_POST['action_delete']) && $_POST['action_delete'] === '1') {
|
||||
$loc = $_POST['old_location_name'];
|
||||
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND location_name = ?")->execute([$holiday_id, $loc]);
|
||||
header("Location: /holidays.php?tab=holiday_detail&id=" . $holiday_id);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ajout / Modification d'une étape
|
||||
$location_name = trim($_POST['location_name']);
|
||||
$old_location = trim($_POST['old_location_name'] ?? '');
|
||||
$lat = (float)$_POST['lat'];
|
||||
$lng = (float)$_POST['lng'];
|
||||
|
||||
if ($holiday_id > 0 && !empty($location_name)) {
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// 1. GESTION DES FAVORIS
|
||||
if (isset($_POST['save_favorite']) && $_POST['save_favorite'] == '1') {
|
||||
$stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'");
|
||||
$favs = json_decode($stmtFav->fetchColumn() ?: '[]', true);
|
||||
$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)]);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. GESTION DES DÉPENSES
|
||||
if (!empty($old_location)) {
|
||||
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND location_name = ?")->execute([$holiday_id, $old_location]);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid, location_name, lat, lng) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$validItemsCount = 0;
|
||||
|
||||
if (isset($_POST['items']['name'])) {
|
||||
$count = count($_POST['items']['name']);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$name = trim($_POST['items']['name'][$i]);
|
||||
$amount_raw = $_POST['items']['amount'][$i];
|
||||
|
||||
// Si la ligne n'est pas totalement vide
|
||||
if ($name !== '' || $amount_raw !== '') {
|
||||
$cat = $_POST['items']['cat'][$i] ?? 'activity';
|
||||
$amount = (float)$amount_raw;
|
||||
if ($name === '') $name = 'Dépense liée';
|
||||
$paid = isset($_POST['items']['paid'][$i]) ? 1 : 0;
|
||||
|
||||
$stmt->execute([$holiday_id, $cat, $name, $amount, $paid, $location_name, $lat, $lng]);
|
||||
$validItemsCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. ÉTAPE SANS DÉPENSE (Point de passage)
|
||||
// Si l'utilisateur n'a saisi aucune dépense, on crée une ligne technique invisible pour forcer l'affichage du point GPS
|
||||
if ($validItemsCount === 0) {
|
||||
$stmt->execute([$holiday_id, 'activity', 'PF_TECHNICAL_POINT', 0, 1, $location_name, $lat, $lng]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
die("Erreur de sauvegarde : " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
header("Location: /holidays.php?tab=holiday_detail&id=" . $holiday_id);
|
||||
exit;
|
||||
@@ -1,7 +1,10 @@
|
||||
<?php
|
||||
require __DIR__ . '/../../includes/auth.php';
|
||||
require __DIR__ . '/../../includes/db.php';
|
||||
require_login();
|
||||
// modules/holidays/includes/api/save_holiday.php
|
||||
|
||||
// On remonte de 4 niveaux pour atteindre la racine (api -> includes -> holidays -> modules -> racine)
|
||||
require dirname(__DIR__, 4) . '/includes/auth.php';
|
||||
require dirname(__DIR__, 4) . '/includes/db.php';
|
||||
require_login(); // Si cette fonction nécessite une redirection, gère-la dans auth.php
|
||||
|
||||
if (isset($_POST['action_delete']) && $_POST['action_delete'] == '1') {
|
||||
$stmt = $pdo->prepare("DELETE FROM pf_holidays WHERE id = ?");
|
||||
@@ -36,18 +39,19 @@ try {
|
||||
$id = $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
// GESTION DES ITEMS (On supprime tout et on recrée)
|
||||
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ?")->execute([$id]);
|
||||
// GESTION DES ITEMS GLOBAUX (On ne supprime QUE les items qui n'ont pas de lieu défini !)
|
||||
$pdo->prepare("DELETE FROM pf_holidays_items WHERE holiday_id = ? AND location_name IS NULL")->execute([$id]);
|
||||
|
||||
if (!empty($_POST['items']['name'])) {
|
||||
$stmtItem = $pdo->prepare("INSERT INTO pf_holidays_items (holiday_id, category, name, amount, is_paid) VALUES (?, ?, ?, ?, ?)");
|
||||
|
||||
$count = count($_POST['items']['name']);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$cat = $_POST['items']['cat'][$i];
|
||||
$name = trim($_POST['items']['name'][$i]);
|
||||
$amount = floatval($_POST['items']['amount'][$i]);
|
||||
$paid = $_POST['items']['paid'][$i];
|
||||
// Utilisation de "?? ''" pour sécuriser si la donnée n'est pas envoyée
|
||||
$cat = $_POST['items']['cat'][$i] ?? '';
|
||||
$name = trim($_POST['items']['name'][$i] ?? '');
|
||||
$amount = floatval($_POST['items']['amount'][$i] ?? 0);
|
||||
$paid = isset($_POST['items']['paid'][$i]) ? $_POST['items']['paid'][$i] : 0;
|
||||
|
||||
if (!empty($name)) {
|
||||
$stmtItem->execute([$id, $cat, $name, $amount, $paid]);
|
||||
@@ -59,8 +63,9 @@ try {
|
||||
|
||||
} catch (Exception $e) {
|
||||
$pdo->rollBack();
|
||||
die("Erreur : " . $e->getMessage());
|
||||
die("Erreur base de données : " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Redirection vers la page principale
|
||||
header("Location: /holidays.php");
|
||||
exit;
|
||||
@@ -1,229 +0,0 @@
|
||||
<?php
|
||||
// modules/holidays/index.php
|
||||
|
||||
// 1. Récupération des voyages + Calculs (Coût Total ET Montant déjà financé)
|
||||
// 1. Récupération des voyages + Calculs (Coût Total ET Montant déjà financé)
|
||||
$sql = "
|
||||
SELECT h.*,
|
||||
(
|
||||
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((SELECT SUM(ABS(amount)) FROM pf_expenses WHERE holiday_id = h.id), 0) +
|
||||
COALESCE((SELECT SUM(amount) FROM pf_savings WHERE holiday_id = h.id), 0)
|
||||
) as total_funded
|
||||
FROM pf_holidays h
|
||||
ORDER BY
|
||||
-- COALESCE permet de rejeter les voyages sans date tout à la fin de la liste (ex: l'an 2999)
|
||||
COALESCE(start_date, '2999-12-31') ASC,
|
||||
|
||||
FIELD(status, 'booked', 'planned', 'draft', 'passed', 'archived')
|
||||
";
|
||||
$holidays = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Tri par statut
|
||||
$active = array_filter($holidays, fn($h) => in_array($h['status'], ['draft', 'planned', 'booked']));
|
||||
$history = array_filter($holidays, fn($h) => in_array($h['status'], ['passed', 'archived']));
|
||||
?>
|
||||
|
||||
<div class="pf-holidays">
|
||||
|
||||
<div class="pf-holidays__titlebar">
|
||||
<h1>Mes Vacances ✈️</h1>
|
||||
<div class="hol-title-actions">
|
||||
<button class="hol-add-btn" onclick="openHolidayModal('add')">+ Créer un voyage</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="pf-section">
|
||||
<div class="hol-ideas-grid">
|
||||
<?php if (empty($active)): ?>
|
||||
<p style="color:var(--text-muted); font-style:italic;">Aucun voyage en cours. Planifions quelque chose !</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($active as $h): ?>
|
||||
<?php renderHolidayCard($h, $pdo); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (!empty($history)): ?>
|
||||
<section class="pf-section" style="margin-top: 40px; border-top: 1px solid #e2e8f0; padding-top: 20px;">
|
||||
<h3 style="color:var(--text-muted);">Historique</h3>
|
||||
<div class="hol-ideas-grid" style="opacity: 0.7;">
|
||||
<?php foreach ($history as $h): ?>
|
||||
<?php renderHolidayCard($h, $pdo); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="holidayModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 800px; width: 95%;">
|
||||
<h3 id="modalTitle" class="pf-modal-title">Planifier le voyage</h3>
|
||||
|
||||
<form action="/modules/holidays/save_new.php" method="POST" id="holidayForm">
|
||||
<input type="hidden" name="id" id="inp_id">
|
||||
<input type="hidden" name="action" value="save">
|
||||
|
||||
<div class="form-row">
|
||||
<div style="flex: 2;">
|
||||
<label class="pf-label">Nom du voyage</label>
|
||||
<input type="text" name="title" id="inp_title" class="pf-input" placeholder="Ex: Octobre - Portugal" required>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label class="pf-label">Statut</label>
|
||||
<select name="status" id="inp_status" class="pf-input">
|
||||
<option value="draft">Brouillon ✏️</option>
|
||||
<option value="planned">Planifié 📅</option>
|
||||
<option value="booked">Réservé ✅</option>
|
||||
<option value="passed">Passé 👋</option>
|
||||
<option value="archived">Archivé 🗄️</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label class="pf-label">Période (Texte libre)</label>
|
||||
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="Ex: Octobre 2026">
|
||||
</div>
|
||||
<div style="display:flex; gap:10px;">
|
||||
<div style="flex:1">
|
||||
<label class="pf-label">Du</label>
|
||||
<input type="date" name="start_date" id="inp_start" class="pf-input" lang="fr">
|
||||
</div>
|
||||
<div style="flex:1">
|
||||
<label class="pf-label">Au</label>
|
||||
<input type="date" name="end_date" id="inp_end" class="pf-input" lang="fr">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;">
|
||||
|
||||
<div class="hol-columns-wrapper">
|
||||
|
||||
<div class="hol-col">
|
||||
<div class="hol-col-header">
|
||||
<h4 style="color:#2563eb;">🚗 Transport</h4>
|
||||
<button type="button" class="btn-add-item" onclick="addItem('transport')" title="Ajouter un transport">+</button>
|
||||
</div>
|
||||
<div id="list_transport" class="dynamic-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="hol-col">
|
||||
<div class="hol-col-header">
|
||||
<h4 style="color:#059669;">🏨 Hébergement</h4>
|
||||
<button type="button" class="btn-add-item" onclick="addItem('accommodation')" title="Ajouter un hébergement">+</button>
|
||||
</div>
|
||||
<div id="list_accommodation" class="dynamic-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="hol-col">
|
||||
<div class="hol-col-header">
|
||||
<h4 style="color:#d97706;">🎫 Activité</h4>
|
||||
<button type="button" class="btn-add-item" onclick="addItem('activity')" title="Ajouter une activité">+</button>
|
||||
</div>
|
||||
<div id="list_activity" class="dynamic-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;">
|
||||
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label class="pf-label">🍔 Budget Food & Bev (€)</label>
|
||||
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
|
||||
</div>
|
||||
<div>
|
||||
<label class="pf-label">🎁 Budget Extras (€)</label>
|
||||
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="pf-label">Notes</label>
|
||||
<textarea name="notes" id="inp_notes" class="pf-input" rows="2" placeholder="Idées en vrac..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary" style="color:#ef4444; border-color:#fca5a5; margin-right:auto; display:none;">Supprimer</button>
|
||||
<button type="button" onclick="document.getElementById('holidayModal').style.display='none'" class="pf-btn btn-secondary">Annuler</button>
|
||||
<button type="submit" class="pf-btn">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
function renderHolidayCard($h, $pdo) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ?");
|
||||
$stmt->execute([$h['id']]);
|
||||
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$json = htmlspecialchars(json_encode(['main' => $h, 'items' => $items]), ENT_QUOTES, 'UTF-8');
|
||||
$dateDisplay = htmlspecialchars($h['period_hint'] ?? '');
|
||||
|
||||
if (empty($dateDisplay) && $h['start_date']) {
|
||||
$dateDisplay = date('d/m/Y', strtotime($h['start_date']));
|
||||
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($h['end_date']));
|
||||
}
|
||||
|
||||
$statusClass = match($h['status']) {
|
||||
'booked' => 'bg-green-100 text-green-800',
|
||||
'planned' => 'bg-blue-100 text-blue-800',
|
||||
'passed' => 'bg-gray-100 text-gray-600',
|
||||
default => 'bg-yellow-50 text-yellow-800'
|
||||
};
|
||||
|
||||
// --- NOUVEAU : Calcul de la progression ---
|
||||
$cost = (float)$h['total_cost'];
|
||||
$funded = (float)$h['total_funded'];
|
||||
$leftToPay = max(0, $cost - $funded);
|
||||
|
||||
// Pourcentage pour la barre de progression (max 100%)
|
||||
$percent = $cost > 0 ? min(100, round(($funded / $cost) * 100)) : 0;
|
||||
|
||||
// Couleur de la barre : Rouge si < 50%, Jaune si < 100%, Vert si tout est payé
|
||||
$barColor = $percent === 100 ? '#10b981' : ($percent > 50 ? '#f59e0b' : '#ef4444');
|
||||
|
||||
echo "
|
||||
<div class='hol-idea-card' onclick='editHoliday($json)'>
|
||||
<div class='hol-idea-card__head'>
|
||||
<h3>".htmlspecialchars($h['title'])."</h3>
|
||||
<span style='font-size:0.75rem; padding:2px 8px; border-radius:12px; font-weight:bold;' class='$statusClass'>
|
||||
".strtoupper($h['status'])."
|
||||
</span>
|
||||
</div>
|
||||
<div class='hol-idea-meta'>
|
||||
<span>🗓️ ".($dateDisplay ?: 'Dates à définir')."</span>
|
||||
</div>
|
||||
|
||||
<div style='margin-top:auto; padding-top:10px; border-top:1px solid #f1f5f9;'>
|
||||
|
||||
<div style='display:flex; justify-content:space-between; align-items:center; margin-bottom:5px;'>
|
||||
<span style='font-size:0.85rem; color:#64748b;'>Budget Total</span>
|
||||
<span style='font-size:1rem; font-weight:bold; color:#1e293b;'>".number_format($cost, 0, ',', ' ')." €</span>
|
||||
</div>
|
||||
|
||||
<div style='width:100%; height:6px; background:#e2e8f0; border-radius:3px; margin-bottom:8px; overflow:hidden;'>
|
||||
<div style='width:{$percent}%; height:100%; background:{$barColor}; transition:width 0.3s ease;'></div>
|
||||
</div>
|
||||
|
||||
<div style='display:flex; justify-content:space-between; align-items:center; font-size:0.8rem;'>
|
||||
<span style='color:#10b981; font-weight:600;'>✓ Financé : ".number_format($funded, 0, ',', ' ')." €</span>
|
||||
<span style='color:#ef4444; font-weight:600;'>Reste : ".number_format($leftToPay, 0, ',', ' ')." €</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
";
|
||||
}
|
||||
?>
|
||||
|
||||
<script src="/modules/holidays/holidays.js"></script>
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
// modules/holidays/views/detail.php
|
||||
|
||||
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||
|
||||
if ($id === 0) { echo "<div class='pf-section'><p>Erreur.</p></div>"; exit; }
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT h.*,
|
||||
(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(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) as total_paid,
|
||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
|
||||
FROM pf_holidays h WHERE h.id = ?
|
||||
");
|
||||
$stmt->execute([$id]);
|
||||
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$stmtItems = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ? ORDER BY id ASC");
|
||||
$stmtItems->execute([$id]);
|
||||
$items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Récupération des favoris géographiques
|
||||
$stmtFav = $pdo->query("SELECT content FROM pf_notes WHERE note_type = 'holiday_favorites'");
|
||||
$favorites = json_decode($stmtFav->fetchColumn() ?: '[]', true);
|
||||
|
||||
// GROUPEMENT PAR LIEU (Pour la carte et l'affichage)
|
||||
$steps = [];
|
||||
$generalItems = [];
|
||||
|
||||
foreach ($items as $it) {
|
||||
if (!empty($it['location_name'])) {
|
||||
$loc = $it['location_name'];
|
||||
if (!isset($steps[$loc])) {
|
||||
$steps[$loc] = [
|
||||
'location_name' => $loc,
|
||||
'lat' => (float)$it['lat'],
|
||||
'lng' => (float)$it['lng'],
|
||||
'total_amount' => 0,
|
||||
'items' => []
|
||||
];
|
||||
}
|
||||
$steps[$loc]['items'][] = $it;
|
||||
$steps[$loc]['total_amount'] += (float)$it['amount'];
|
||||
} else {
|
||||
$generalItems[] = $it;
|
||||
}
|
||||
}
|
||||
$mapPoints = array_values($steps);
|
||||
|
||||
// Calculs d'affichage (Dates & Finances)
|
||||
$dateDisplay = htmlspecialchars($holiday['period_hint'] ?? '');
|
||||
if (empty($dateDisplay) && $holiday['start_date']) {
|
||||
$dateDisplay = date('d/m/Y', strtotime($holiday['start_date']));
|
||||
if ($holiday['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($holiday['end_date']));
|
||||
}
|
||||
|
||||
$cost = (float)$holiday['total_cost'];
|
||||
$paid = (float)$holiday['total_paid'];
|
||||
$saved = (float)$holiday['total_saved'];
|
||||
$leftToPay = max(0, $cost - $paid);
|
||||
$pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0;
|
||||
$pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
?>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<div class="pf-holidays-detail">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<div style="display:flex; align-items:center; gap:15px;">
|
||||
<a href="?tab=list" class="pf-btn btn-secondary" style="padding:6px 12px; height:auto; width:auto; text-decoration:none;">◀ Retour</a>
|
||||
<h1 style="margin:0; font-size:1.5rem;"><?= htmlspecialchars($holiday['title']) ?></h1>
|
||||
<span style="font-size:0.75rem; padding:4px 10px; border-radius:12px; font-weight:bold; background:#e0f2fe; color:#0369a1;">
|
||||
<?= strtoupper($holiday['status']) ?>
|
||||
</span>
|
||||
</div>
|
||||
<button onclick='editHoliday(<?= htmlspecialchars(json_encode(['main' => $holiday, 'items' => $generalItems]), ENT_QUOTES, 'UTF-8') ?>)' class="pf-btn btn-secondary" style="width:auto;">⚙️ Modifier les bases</button>
|
||||
</div>
|
||||
|
||||
<div style="background:white; padding:20px; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0; margin-bottom:24px;">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:15px; flex-wrap:wrap; gap:15px;">
|
||||
<div>
|
||||
<div style="font-size:0.85rem; color:#64748b;">Période</div>
|
||||
<div style="font-weight:600; color:#1e293b;"><?= $dateDisplay ?: 'À définir' ?></div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:0.85rem; color:#64748b;">Budget Food & Extras</div>
|
||||
<div style="font-weight:600; color:#1e293b;">🍔 <?= number_format($holiday['budget_food'], 0) ?> € | 🎁 <?= number_format($holiday['budget_extra'], 0) ?> €</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div style="font-size:0.85rem; color:#64748b;">Coût Total Estimé</div>
|
||||
<div style="font-size:1.4rem; font-weight:bold; color:#0f172a;"><?= number_format($cost, 0, ',', ' ') ?> €</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="width:100%; height:12px; background:#e2e8f0; border-radius:6px; margin-bottom:10px; display:flex; overflow:hidden;">
|
||||
<div style="width:<?= $pctPaid ?>%; background:#10b981;" title="Payé"></div>
|
||||
<div style="width:<?= $pctSaved ?>%; background:#3b82f6;" title="Financé (Provision)"></div>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; font-size:0.85rem;">
|
||||
<span style="color:#10b981; font-weight:600;">✓ Payé : <?= number_format($paid, 0, ',', ' ') ?> €</span>
|
||||
<span style="color:#3b82f6; font-weight:600;">💼 Provisionné : <?= number_format($saved, 0, ',', ' ') ?> €</span>
|
||||
<span style="color:#ef4444; font-weight:700;">⏳ Reste à payer : <?= number_format($leftToPay, 0, ',', ' ') ?> €</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns: 2fr 1fr; gap:20px; align-items: stretch;">
|
||||
<div style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0; overflow:hidden; display:flex; flex-direction:column; height: 600px;">
|
||||
<div style="padding:15px 20px; border-bottom:1px solid #e2e8f0; display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 style="margin:0; font-size:1.1rem;">🗺️ Itinéraire & Checkpoints</h3>
|
||||
<button class="pf-btn" onclick="openCheckpointModal('add')" style="padding:6px 12px; height:auto; width:auto; font-size:0.85rem;">📍 Placer une étape</button>
|
||||
</div>
|
||||
<div id="tripMap" style="flex:1; width:100%; background:#f1f5f9;"></div>
|
||||
</div>
|
||||
|
||||
<div style="background:white; border-radius:16px; box-shadow:var(--shadow-sm); border:1px solid #e2e8f0; display:flex; flex-direction:column; height: 600px;">
|
||||
<div style="padding:15px 20px; border-bottom:1px solid #e2e8f0; background:#f8fafc; border-radius:16px 16px 0 0;">
|
||||
<h3 style="margin:0; font-size:1.1rem;">📝 Détail des étapes</h3>
|
||||
</div>
|
||||
|
||||
<div style="padding:15px; overflow-y:auto; flex:1;">
|
||||
<?php if (empty($steps)): ?>
|
||||
<p style="color:#94a3b8; font-style:italic; text-align:center; margin-top:40px;">Aucune étape planifiée.</p>
|
||||
<?php else: ?>
|
||||
<?php foreach ($steps as $step): ?>
|
||||
<div style="border:1px solid #e2e8f0; border-radius:8px; margin-bottom:15px; overflow:hidden;">
|
||||
<div style="background:#f8fafc; padding:10px 15px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #e2e8f0;">
|
||||
<div>
|
||||
<strong style="color:#0f172a; font-size:1rem; cursor:pointer;" onclick="panMapTo(<?= $step['lat'] ?>, <?= $step['lng'] ?>)">📍 <?= htmlspecialchars($step['location_name']) ?></strong>
|
||||
<div style="font-size:0.8rem; color:#64748b;">Total Étape : <?= number_format($step['total_amount'], 2) ?> €</div>
|
||||
</div>
|
||||
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="Modifier cette étape" style="background:white; border:1px solid #cbd5e1; border-radius:4px; padding:4px 8px; cursor:pointer;">✏️</button>
|
||||
</div>
|
||||
<div style="padding:10px 15px; background:white;">
|
||||
|
||||
<?php
|
||||
$visibleItemsCount = 0;
|
||||
foreach ($step['items'] as $it):
|
||||
if ($it['name'] === 'PF_TECHNICAL_POINT') continue; // On cache la ligne technique !
|
||||
$visibleItemsCount++;
|
||||
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
|
||||
?>
|
||||
<div style="display:flex; justify-content:space-between; font-size:0.85rem; margin-bottom:5px; border-bottom:1px dashed #f1f5f9; padding-bottom:5px;">
|
||||
<span style="color:#475569;"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
|
||||
<span>
|
||||
<strong style="color:#1e293b;"><?= number_format($it['amount'], 2) ?> €</strong>
|
||||
<span style="margin-left:5px; color:<?= $it['is_paid'] ? '#10b981' : '#f59e0b' ?>;" title="<?= $it['is_paid'] ? 'Payé' : 'À payer' ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
|
||||
</span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($visibleItemsCount === 0): ?>
|
||||
<div style="font-size:0.8rem; color:#94a3b8; font-style:italic;">Point de passage (Aucune dépense)</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($holiday['notes'])): ?>
|
||||
<div style="padding:15px; border-top:1px solid #e2e8f0; background:#fffbeb;">
|
||||
<h4 style="margin:0 0 5px 0; font-size:0.85rem; color:#d97706;">Notes du voyage :</h4>
|
||||
<p style="margin:0; font-size:0.85rem; color:#92400e; white-space:pre-wrap;"><?= htmlspecialchars($holiday['notes']) ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="checkpointModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 600px; width:95%;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||||
<h3 id="cpModalTitle" style="margin:0;">📍 Placer une étape</h3>
|
||||
<button type="button" onclick="document.getElementById('checkpointModal').style.display='none'" style="border:none; background:none; font-size:1.8rem; cursor:pointer;">×</button>
|
||||
</div>
|
||||
|
||||
<div id="cpSearchBlock" style="margin-bottom:20px;">
|
||||
<?php if (!empty($favorites)): ?>
|
||||
<div style="margin-bottom:15px; display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<?php foreach($favorites as $fav): ?>
|
||||
<button type="button" class="pf-btn btn-secondary" style="padding:4px 10px; font-size:0.8rem; height:auto; width:auto; border-radius:20px; background:#f0f9ff; color:#0369a1; border-color:#bae6fd;" onclick="selectPlace(<?= $fav['lat'] ?>, <?= $fav['lng'] ?>, '<?= htmlspecialchars(addslashes($fav['name'])) ?>')">
|
||||
⭐ <?= htmlspecialchars($fav['name']) ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<label class="pf-label">Rechercher un lieu géographique</label>
|
||||
<div style="display:flex; gap:10px;">
|
||||
<input type="text" id="searchPlaceInput" class="pf-input" placeholder="Ex: Paris, Ibis Barcelone..." onkeypress="if(event.key === 'Enter') { searchPlace(); return false; }">
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="searchPlace()" style="width:auto;">🔍</button>
|
||||
</div>
|
||||
<div id="searchResults" style="margin-top:10px; max-height:200px; overflow-y:auto; display:flex; flex-direction:column; gap:5px;"></div>
|
||||
</div>
|
||||
|
||||
<form action="/modules/holidays/includes/api/save_checkpoint.php" method="POST" id="formCheckpoint" style="display:none; border-top:1px solid #e2e8f0; padding-top:20px;">
|
||||
<input type="hidden" name="holiday_id" value="<?= $id ?>">
|
||||
<input type="hidden" name="old_location_name" id="cp_old_name">
|
||||
<input type="hidden" name="lat" id="cp_lat">
|
||||
<input type="hidden" name="lng" id="cp_lng">
|
||||
|
||||
<div class="form-group" style="margin-bottom:15px;">
|
||||
<label class="pf-label">Nom de l'étape (Ce qui s'affichera sur la carte)</label>
|
||||
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:#2563eb;" required>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
|
||||
<label class="pf-label" style="margin:0;">Dépenses prévues à cette étape</label>
|
||||
<button type="button" class="pf-btn btn-secondary" onclick="addCpExpenseLine()" style="padding:4px 8px; font-size:0.8rem; height:auto; width:auto;">+ Ajouter une dépense</button>
|
||||
</div>
|
||||
|
||||
<div id="cpExpensesContainer" style="margin-bottom:15px; display:flex; flex-direction:column; gap:10px; max-height:300px; overflow-y:auto;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px; padding-left: 5px;">
|
||||
<label style="display:flex; align-items:center; cursor:pointer; font-size:0.85rem; color:#475569;">
|
||||
<input type="checkbox" name="save_favorite" value="1" style="margin-right:8px; cursor:pointer; width:16px; height:16px;">
|
||||
⭐ Sauvegarder cette adresse dans mes favoris rapides
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; border-top:1px solid #e2e8f0; padding-top:15px;">
|
||||
<div>
|
||||
<button type="button" onclick="deleteCheckpoint()" id="btnDeleteCp" class="pf-btn btn-secondary" style="color:#ef4444; border-color:#fca5a5; display:none; width:auto; margin:0;">🗑️ Supprimer l'étape</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:10px;">
|
||||
<button type="button" onclick="document.getElementById('checkpointModal').style.display='none'" class="pf-btn btn-secondary" style="width:auto; margin:0;">Annuler</button>
|
||||
<button type="submit" class="pf-btn" style="width:auto; margin:0;">Enregistrer l'étape</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/modal.php'; ?>
|
||||
|
||||
<script>
|
||||
const MAP_POINTS = <?= json_encode($mapPoints) ?>;
|
||||
</script>
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
// modules/holidays/views/list.php
|
||||
|
||||
// 1. Récupération des voyages + Calculs (Coût Total, Montant Payé, Montant Épargné/Financé)
|
||||
$sql = "
|
||||
SELECT h.*,
|
||||
(
|
||||
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(ABS(amount)), 0) FROM pf_expenses WHERE holiday_id = h.id) as total_paid,
|
||||
(SELECT COALESCE(SUM(amount), 0) FROM pf_savings WHERE holiday_id = h.id) as total_saved
|
||||
FROM pf_holidays h
|
||||
ORDER BY
|
||||
-- COALESCE permet de rejeter les voyages sans date tout à la fin de la liste (ex: l'an 2999)
|
||||
COALESCE(start_date, '2999-12-31') ASC,
|
||||
|
||||
FIELD(status, 'booked', 'planned', 'draft', 'passed', 'archived')
|
||||
";
|
||||
$holidays = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Tri par statut
|
||||
$active = array_filter($holidays, fn($h) => in_array($h['status'], ['draft', 'planned', 'booked']));
|
||||
$history = array_filter($holidays, fn($h) => in_array($h['status'], ['passed', 'archived']));
|
||||
?>
|
||||
|
||||
<div class="pf-holidays">
|
||||
|
||||
<div class="pf-holidays__titlebar">
|
||||
<h1>Mes Vacances ✈️</h1>
|
||||
<div class="hol-title-actions">
|
||||
<button class="hol-add-btn" onclick="openHolidayModal('add')">+ Créer un voyage</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="pf-section">
|
||||
<div class="hol-ideas-grid">
|
||||
<?php if (empty($active)): ?>
|
||||
<p style="color:var(--text-muted); font-style:italic;">Aucun voyage en cours. Planifions quelque chose !</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($active as $h): ?>
|
||||
<?php renderHolidayCard($h, $pdo); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if (!empty($history)): ?>
|
||||
<section class="pf-section" style="margin-top: 40px; border-top: 1px solid #e2e8f0; padding-top: 20px;">
|
||||
<h3 style="color:var(--text-muted);">Historique</h3>
|
||||
<div class="hol-ideas-grid" style="opacity: 0.7;">
|
||||
<?php foreach ($history as $h): ?>
|
||||
<?php renderHolidayCard($h, $pdo); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/modal.php'; ?>
|
||||
|
||||
<?php
|
||||
function renderHolidayCard($h, $pdo) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ?");
|
||||
$stmt->execute([$h['id']]);
|
||||
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$json = htmlspecialchars(json_encode(['main' => $h, 'items' => $items]), ENT_QUOTES, 'UTF-8');
|
||||
$dateDisplay = htmlspecialchars($h['period_hint'] ?? '');
|
||||
|
||||
if (empty($dateDisplay) && $h['start_date']) {
|
||||
$dateDisplay = date('d/m/Y', strtotime($h['start_date']));
|
||||
if ($h['end_date']) $dateDisplay .= ' → ' . date('d/m/Y', strtotime($h['end_date']));
|
||||
}
|
||||
|
||||
$statusClass = match($h['status']) {
|
||||
'booked' => 'bg-green-100 text-green-800',
|
||||
'planned' => 'bg-blue-100 text-blue-800',
|
||||
'passed' => 'bg-gray-100 text-gray-600',
|
||||
default => 'bg-yellow-50 text-yellow-800'
|
||||
};
|
||||
|
||||
// --- Calcul des métriques financières ---
|
||||
$cost = (float)$h['total_cost'];
|
||||
$paid = (float)$h['total_paid'];
|
||||
$saved = (float)$h['total_saved'];
|
||||
|
||||
$leftToPay = max(0, $cost - $paid);
|
||||
|
||||
// Calculs pour la barre de progression bicolore
|
||||
$pctPaid = $cost > 0 ? min(100, ($paid / $cost) * 100) : 0;
|
||||
$pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
|
||||
|
||||
echo "
|
||||
<div class='hol-idea-card' style='display: flex; flex-direction: column;'>
|
||||
<div class='hol-idea-card__head' style='display:flex; justify-content:space-between; align-items:flex-start; margin-bottom: 10px;'>
|
||||
|
||||
<div style='flex:1;'>
|
||||
<h3 style='margin:0; font-size:1.15rem; color:#0f172a;'>
|
||||
<a href='?tab=holiday_detail&id={$h['id']}' style='text-decoration:none; color:inherit;' title='Ouvrir la page du voyage'>
|
||||
".htmlspecialchars($h['title'])."
|
||||
</a>
|
||||
</h3>
|
||||
<span style='font-size:0.7rem; padding:3px 8px; border-radius:12px; font-weight:bold; display:inline-block; margin-top:6px;' class='$statusClass'>
|
||||
".strtoupper($h['status'])."
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style='display:flex; gap:5px;'>
|
||||
<button onclick='editHoliday($json)' class='pf-btn btn-secondary' style='padding:6px; height:auto; width:auto; line-height:1;' title='Modification rapide'>✏️</button>
|
||||
<a href='?tab=holiday_detail&id={$h['id']}' class='pf-btn' style='padding:6px; height:auto; width:auto; line-height:1; text-decoration:none;' title='Gérer le voyage'>👁️</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='hol-idea-meta' style='margin-bottom:15px;'>
|
||||
<span>🗓️ ".($dateDisplay ?: 'Dates à définir')."</span>
|
||||
</div>
|
||||
|
||||
<div style='margin-top:auto; padding-top:15px; border-top:1px solid #f1f5f9;'>
|
||||
|
||||
<div style='display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;'>
|
||||
<span style='font-size:0.85rem; color:#64748b;'>Budget Total</span>
|
||||
<span style='font-size:1.1rem; font-weight:bold; color:#1e293b;'>".number_format($cost, 0, ',', ' ')." €</span>
|
||||
</div>
|
||||
|
||||
<div style='width:100%; height:8px; background:#e2e8f0; border-radius:4px; margin-bottom:12px; display:flex; overflow:hidden;'>
|
||||
<div style='width:{$pctPaid}%; background:#10b981; transition:width 0.3s ease;' title='Payé'></div>
|
||||
<div style='width:{$pctSaved}%; background:#3b82f6; transition:width 0.3s ease;' title='Financé (Provision)'></div>
|
||||
</div>
|
||||
|
||||
<div style='display:grid; grid-template-columns: 1fr 1fr; gap:8px; font-size:0.8rem;'>
|
||||
<div style='color:#10b981; font-weight:600;' title='Montant déjà dépensé'>✓ Payé : ".number_format($paid, 0, ',', ' ')." €</div>
|
||||
<div style='color:#3b82f6; font-weight:600; text-align:right;' title='Montant épargné non dépensé'>💼 Financé : ".number_format($saved, 0, ',', ' ')." €</div>
|
||||
<div style='color:#ef4444; font-weight:700; font-size:0.85rem; grid-column: span 2; padding-top: 4px; border-top: 1px dashed #fca5a5;'>
|
||||
⏳ Reste à payer : ".number_format($leftToPay, 0, ',', ' ')." €
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
";
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,96 @@
|
||||
<div id="holidayModal" class="pf-modal">
|
||||
<div class="pf-modal-content" style="max-width: 800px; width: 95%;">
|
||||
<h3 id="modalTitle" class="pf-modal-title">Planifier le voyage</h3>
|
||||
|
||||
<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="action" value="save">
|
||||
|
||||
<div class="form-row">
|
||||
<div style="flex: 2;">
|
||||
<label class="pf-label">Nom du voyage</label>
|
||||
<input type="text" name="title" id="inp_title" class="pf-input" placeholder="Ex: Octobre - Portugal" required>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label class="pf-label">Statut</label>
|
||||
<select name="status" id="inp_status" class="pf-input">
|
||||
<option value="draft">Brouillon ✏️</option>
|
||||
<option value="planned">Planifié 📅</option>
|
||||
<option value="booked">Réservé ✅</option>
|
||||
<option value="passed">Passé 👋</option>
|
||||
<option value="archived">Archivé 🗄️</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label class="pf-label">Période (Texte libre)</label>
|
||||
<input type="text" name="period_hint" id="inp_period" class="pf-input" placeholder="Ex: Octobre 2026">
|
||||
</div>
|
||||
<div style="display:flex; gap:10px;">
|
||||
<div style="flex:1">
|
||||
<label class="pf-label">Du</label>
|
||||
<input type="date" name="start_date" id="inp_start" class="pf-input" lang="fr">
|
||||
</div>
|
||||
<div style="flex:1">
|
||||
<label class="pf-label">Au</label>
|
||||
<input type="date" name="end_date" id="inp_end" class="pf-input" lang="fr">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;">
|
||||
|
||||
<div class="hol-columns-wrapper">
|
||||
<div class="hol-col">
|
||||
<div class="hol-col-header">
|
||||
<h4 style="color:#2563eb;">🚗 Transport</h4>
|
||||
<button type="button" class="btn-add-item" onclick="addItem('transport')" title="Ajouter un transport">+</button>
|
||||
</div>
|
||||
<div id="list_transport" class="dynamic-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="hol-col">
|
||||
<div class="hol-col-header">
|
||||
<h4 style="color:#059669;">🏨 Hébergement</h4>
|
||||
<button type="button" class="btn-add-item" onclick="addItem('accommodation')" title="Ajouter un hébergement">+</button>
|
||||
</div>
|
||||
<div id="list_accommodation" class="dynamic-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="hol-col">
|
||||
<div class="hol-col-header">
|
||||
<h4 style="color:#d97706;">🎫 Activité</h4>
|
||||
<button type="button" class="btn-add-item" onclick="addItem('activity')" title="Ajouter une activité">+</button>
|
||||
</div>
|
||||
<div id="list_activity" class="dynamic-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;">
|
||||
|
||||
<div class="form-row">
|
||||
<div>
|
||||
<label class="pf-label">🍔 Budget Food & Bev (€)</label>
|
||||
<input type="number" step="0.01" name="budget_food" id="inp_food" class="pf-input" placeholder="0.00">
|
||||
</div>
|
||||
<div>
|
||||
<label class="pf-label">🎁 Budget Extras (€)</label>
|
||||
<input type="number" step="0.01" name="budget_extra" id="inp_extra" class="pf-input" placeholder="0.00">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="pf-label">Notes</label>
|
||||
<textarea name="notes" id="inp_notes" class="pf-input" rows="2" placeholder="Idées en vrac..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" onclick="deleteHoliday()" id="btn_delete" class="pf-btn btn-secondary" style="color:#ef4444; border-color:#fca5a5; margin-right:auto; display:none;">Supprimer</button>
|
||||
<button type="button" onclick="closeHolidayModal()" class="pf-btn btn-secondary">Annuler</button>
|
||||
<button type="submit" class="pf-btn">Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user