carnet de voyage
Deploy HouseHub / deploy (push) Successful in 2s

This commit is contained in:
2026-06-21 09:17:15 +02:00
parent 9d22dc29b4
commit 5618448602
10 changed files with 565 additions and 129 deletions
+216
View File
@@ -1161,3 +1161,219 @@ function launchGpsApp(app) {
closeGpsModal();
}
}
// ============================================================================
// GESTION DU PORTE-DOCUMENTS (UPLOAD)
// ============================================================================
window.currentDocsStepId = null;
// 🔥 On attache le verrou à window pour éviter les erreurs de redéclaration
window.isUploadingDocs = window.isUploadingDocs || false;
function openDocsModal(sortOrder) {
window.currentDocsStepId = sortOrder;
document.getElementById("docsModal").style.display = "flex";
document.body.classList.add("no-scroll");
document.getElementById("uploadStatus").innerHTML = "";
const listContainer = document.getElementById("docsListContainer");
listContainer.innerHTML =
'<p style="text-align: center; font-size: 0.85rem; color: var(--text-muted);">⏳ Chargement des documents...</p>';
const holidayId = document.querySelector('input[name="holiday_id"]').value;
// 🔥 On va chercher les documents existants !
fetch(
`/modules/holidays/includes/api/get_attachments.php?holiday_id=${holidayId}&item_id=${sortOrder}`,
)
.then((response) => response.json())
.then((data) => {
listContainer.innerHTML = ""; // On vide le message de chargement
if (data.success && data.files.length > 0) {
data.files.forEach((f) => {
// On rend le nom du fichier cliquable pour ouvrir le document dans un nouvel onglet
const docHtml = `
<div style="display: flex; align-items: center; justify-content: space-between; padding: 10px; background: var(--bg-page); border: 1px solid var(--border-light); border-radius: 6px; margin-bottom: 6px;">
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden; cursor: pointer;" onclick="window.open('/${f.file_path}', '_blank')">
<span style="font-size: 1.2rem;">📄</span>
<span style="font-size: 0.9rem; color: var(--primary); font-weight: bold; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" title="Ouvrir ${f.file_name}">${f.file_name}</span>
</div>
<button type="button" onclick="deleteAttachment(${f.id}, this)" style="background: none; border: none; color: var(--danger); cursor: pointer; font-size: 1rem;" title="Supprimer">🗑️</button>
</div>
`;
listContainer.insertAdjacentHTML("beforeend", docHtml);
});
} else {
listContainer.innerHTML =
'<p style="text-align: center; font-size: 0.85rem; color: var(--text-muted); font-style: italic;">Aucun document pour cette étape.</p>';
}
})
.catch(() => {
listContainer.innerHTML =
'<p style="text-align: center; color: var(--danger);">Erreur lors du chargement.</p>';
});
}
function closeDocsModal() {
document.getElementById("docsModal").style.display = "none";
document.body.classList.remove("no-scroll");
}
function handleFileUpload(input) {
// 1. LE VERROU : Si un envoi est déjà en cours, on bloque tout !
if (window.isUploadingDocs) return;
if (!input.files || input.files.length === 0) return;
// On ferme le verrou
window.isUploadingDocs = true;
const file = input.files[0];
const holidayId = document.querySelector('input[name="holiday_id"]').value;
const statusDiv = document.getElementById("uploadStatus");
const listContainer = document.getElementById("docsListContainer");
if (file.size > 5 * 1024 * 1024) {
statusDiv.innerHTML =
"<span style='color: var(--danger);'>Fichier trop lourd (Max 5Mo).</span>";
input.value = "";
window.isUploadingDocs = false; // On rouvre le verrou
return;
}
statusDiv.innerHTML =
"<span style='color: var(--primary);'>⏳ Envoi en cours...</span>";
const fd = new FormData();
fd.append("holiday_id", holidayId);
fd.append("item_id", window.currentDocsStepId);
fd.append("file", file);
fetch("/modules/holidays/includes/api/upload_attachment.php", {
method: "POST",
body: fd,
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
statusDiv.innerHTML = `<span style='color: var(--success);'>✅ Sauvegardé !</span>`;
const emptyMsg = listContainer.querySelector("p");
if (emptyMsg) emptyMsg.remove();
const docHtml = `
<div style="display: flex; align-items: center; justify-content: space-between; padding: 10px; background: var(--bg-page); border: 1px solid var(--border-light); border-radius: 6px; margin-bottom: 6px;">
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden;">
<span style="font-size: 1.2rem;">📄</span>
<span style="font-size: 0.9rem; color: var(--text-main); white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" title="${data.file_name}">${data.file_name}</span>
</div>
<button type="button" onclick="deleteAttachment(${data.id}, this)" style="background: none; border: none; color: var(--danger); cursor: pointer; font-size: 1rem;" title="Supprimer">🗑️</button>
</div>
`;
listContainer.insertAdjacentHTML("beforeend", docHtml);
} else {
statusDiv.innerHTML = `<span style='color: var(--danger);'>❌ Erreur: ${data.error}</span>`;
}
})
.catch((err) => {
statusDiv.innerHTML =
"<span style='color: var(--danger);'>❌ Erreur réseau.</span>";
})
.finally(() => {
input.value = "";
// 🔥 2. On rouvre le verrou SEULEMENT quand tout est terminé
setTimeout(() => {
window.isUploadingDocs = false;
}, 500);
});
}
// Fonction pour supprimer un document
function deleteAttachment(fileId, btnElement) {
if (!confirm("Voulez-vous vraiment supprimer ce document définitivement ?"))
return;
const holidayId = document.querySelector('input[name="holiday_id"]').value;
const row = btnElement.closest('div[style*="border: 1px solid"]'); // Cible la ligne d'affichage
row.style.opacity = "0.4"; // Effet visuel d'attente
const fd = new FormData();
fd.append("file_id", fileId);
fd.append("holiday_id", holidayId);
fetch("/modules/holidays/includes/api/delete_attachment.php", {
method: "POST",
body: fd,
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
row.remove(); // On efface la ligne
// Si c'était le dernier fichier, on remet le texte "Aucun document"
const listContainer = document.getElementById("docsListContainer");
if (listContainer.children.length === 0) {
listContainer.innerHTML =
'<p style="text-align: center; font-size: 0.85rem; color: var(--text-muted); font-style: italic;">Aucun document pour cette étape.</p>';
}
} else {
alert("Erreur : " + data.error);
row.style.opacity = "1";
}
})
.catch(() => {
alert("Erreur réseau lors de la suppression.");
row.style.opacity = "1";
});
}
// ============================================================================
// GÉNÉRATION DU CARNET DE VOYAGE (PDF Côté Client) - VERSION TEXTE BRUT
// ============================================================================
window.generateTravelBook = function () {
const element = document.getElementById("travelBookTemplate");
const btn = document.querySelector('button[onclick="generateTravelBook()"]');
if (!element) {
alert("Erreur : Le modèle de carnet de voyage est introuvable.");
return;
}
const originalText = btn.innerHTML;
btn.innerHTML = "⏳ Génération...";
btn.disabled = true;
// Options ajustées avec un fond blanc forcé
const opt = {
margin: 10, // 10mm de marge
filename: "Carnet_de_Route.pdf",
image: { type: "jpeg", quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, backgroundColor: "#ffffff" },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
};
// 🔥 L'ASTUCE MAGIQUE :
// On extrait le HTML en texte brut et on l'encapsule dans un bloc 100% blanc.
// Plus aucun conflit possible avec l'affichage de ta page web !
const htmlString = `
<div style="background-color: #ffffff; color: #000000; width: 100%;">
${element.outerHTML}
</div>
`;
// Génération directe depuis la chaîne de texte
html2pdf()
.set(opt)
.from(htmlString)
.save()
.then(() => {
btn.innerHTML = originalText;
btn.disabled = false;
})
.catch((err) => {
console.error("Erreur html2pdf:", err);
btn.innerHTML = originalText;
btn.disabled = false;
});
};
@@ -0,0 +1,42 @@
<?php
// modules/holidays/includes/api/delete_attachment.php
require dirname(__DIR__, 4) . '/includes/auth.php';
require dirname(__DIR__, 4) . '/includes/db.php';
require_login();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
exit;
}
$file_id = isset($_POST['file_id']) ? (int)$_POST['file_id'] : 0;
$holiday_id = isset($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : 0;
if ($file_id === 0 || $holiday_id === 0) {
echo json_encode(['success' => false, 'error' => 'Paramètres manquants']);
exit;
}
// 1. On récupère le chemin exact du fichier pour le supprimer du NAS
$stmt = $pdo->prepare("SELECT file_path FROM pf_holidays_attachments WHERE id = ? AND holiday_id = ?");
$stmt->execute([$file_id, $holiday_id]);
$file = $stmt->fetch(PDO::FETCH_ASSOC);
if ($file) {
$absolutePath = dirname(__DIR__, 4) . '/' . $file['file_path'];
// 2. On supprime physiquement le fichier s'il existe
if (file_exists($absolutePath)) {
unlink($absolutePath);
}
// 3. On nettoie la base de données
$del = $pdo->prepare("DELETE FROM pf_holidays_attachments WHERE id = ?");
$del->execute([$file_id]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Fichier introuvable ou non autorisé']);
}
@@ -0,0 +1,22 @@
<?php
// modules/holidays/includes/api/get_attachments.php
require dirname(__DIR__, 4) . '/includes/auth.php';
require dirname(__DIR__, 4) . '/includes/db.php';
require_login();
header('Content-Type: application/json');
$holiday_id = isset($_GET['holiday_id']) ? (int)$_GET['holiday_id'] : 0;
$item_id = isset($_GET['item_id']) ? (int)$_GET['item_id'] : 0;
if ($holiday_id === 0) {
echo json_encode(['success' => false, 'error' => 'ID manquant']);
exit;
}
// On récupère les documents de cette étape spécifique
$stmt = $pdo->prepare("SELECT id, file_name, file_path FROM pf_holidays_attachments WHERE holiday_id = ? AND item_id = ? ORDER BY uploaded_at DESC");
$stmt->execute([$holiday_id, $item_id]);
$files = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'files' => $files]);
@@ -0,0 +1,46 @@
<?php
// modules/holidays/includes/api/upload_attachment.php
require dirname(__DIR__, 4) . '/includes/auth.php';
require dirname(__DIR__, 4) . '/includes/db.php';
require_login();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
exit;
}
$holiday_id = isset($_POST['holiday_id']) ? (int)$_POST['holiday_id'] : 0;
$item_id = (isset($_POST['item_id']) && (int)$_POST['item_id'] > 0) ? (int)$_POST['item_id'] : null;
if ($holiday_id === 0 || empty($_FILES['file'])) {
echo json_encode(['success' => false, 'error' => 'Données manquantes ou fichier invalide']);
exit;
}
$file = $_FILES['file'];
// On range les fichiers dans un sous-dossier par voyage pour rester propre sur le NAS
$uploadDir = dirname(__DIR__, 4) . '/uploads/holidays/' . $holiday_id . '/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
// Sécurisation du nom de fichier (retrait des accents et caractères spéciaux)
$fileName = basename($file['name']);
$safeFileName = preg_replace("/[^a-zA-Z0-9.-]/", "_", $fileName);
$uniqueFileName = time() . '_' . $safeFileName;
$destination = $uploadDir . $uniqueFileName;
if (move_uploaded_file($file['tmp_name'], $destination)) {
// Enregistrement en base de données
$stmt = $pdo->prepare("INSERT INTO pf_holidays_attachments (holiday_id, item_id, file_name, file_path) VALUES (?, ?, ?, ?)");
$stmt->execute([$holiday_id, $item_id, $fileName, 'uploads/holidays/' . $holiday_id . '/' . $uniqueFileName]);
$attachmentId = $pdo->lastInsertId();
echo json_encode(['success' => true, 'id' => $attachmentId, 'file_name' => $fileName]);
} else {
echo json_encode(['success' => false, 'error' => 'Erreur lors de l\'écriture du fichier sur le NAS']);
}
+39 -1
View File
@@ -90,6 +90,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<div class="pf-holidays-detail">
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;">
<div style="display: flex; flex-direction: column; gap: 12px;">
<a href="?tab=list" class="pf-btn btn-secondary pf-btn-small" style="width: fit-content; text-decoration: none;"><?= tr('btn_back') ?></a>
<div style="display: flex; align-items: center; gap: 15px;">
@@ -98,14 +99,20 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
</div>
<div>
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<script id="holidayDataJson" type="application/json">
<?= json_encode(['main' => $holiday, 'items' => $generalItems], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>
</script>
<button type="button" class="pf-btn btn-primary pf-btn-small" onclick="generateTravelBook()" style="display: flex; align-items: center; gap: 6px;">
📖 Carnet de Voyage
</button>
<button type="button" class="pf-btn btn-secondary pf-btn-small" onclick="editHoliday(JSON.parse(document.getElementById('holidayDataJson').textContent))">
<?= tr('btn_edit_bases') ?>
</button>
</div>
</div>
<div class="hol-summary-card">
@@ -303,6 +310,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">
🧭
</button>
<button onclick="openDocsModal(<?= $step['sort_order'] ?>)" class="btn-icon-small" title="Documents & Billets" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">📎</button>
<button onclick='openPlanningModal(<?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('hdl_view_planning') ?>" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">📅</button>
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="<?= tr('btn_edit') ?>" style="width:26px!important; height:26px!important; font-size:0.8rem; min-width:26px!important; min-height:26px!important;">✏️</button>
@@ -354,6 +362,8 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
<div id="checkpointModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 600px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
@@ -498,6 +508,30 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</div>
</div>
<div id="docsModal" class="pf-modal">
<div class="pf-modal-content" style="max-width: 450px; padding: 20px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-bottom: 1px solid var(--border-light); padding-bottom: 10px;">
<h3 style="margin:0; font-size: 1.2rem; color: var(--text-main);">📎 Porte-documents</h3>
<button type="button" onclick="closeDocsModal()" class="pf-modal-close">×</button>
</div>
<div style="text-align: center; padding: 20px; background: var(--bg-page); border: 2px dashed var(--border-light); border-radius: 8px; margin-bottom: 15px;">
<p style="margin-top:0; color: var(--text-muted); font-size: 0.9rem;">Ajoutez vos billets, réservations ou PDFs pour cette étape.</p>
<input type="file" id="docFileInput" style="display: none;" accept=".pdf,.png,.jpg,.jpeg" onchange="handleFileUpload(this)">
<button type="button" class="pf-btn btn-primary" onclick="document.getElementById('docFileInput').click()" style="margin: 10px 0;">
+ Sélectionner un fichier
</button>
<div id="uploadStatus" style="font-size: 0.85rem; font-weight: bold; margin-top: 10px;"></div>
</div>
<div id="docsListContainer" style="display: flex; flex-direction: column; gap: 8px;">
<p style="text-align: center; font-size: 0.85rem; color: var(--text-muted); font-style: italic;">Aucun document pour cette étape.</p>
</div>
</div>
</div>
<?php include __DIR__ . '/modal.php'; ?>
<script>
@@ -561,4 +595,8 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<?php include __DIR__ . '/pdf_template.php'; ?>
<script src="/modules/holidays/holidays.js?v=<?= time() ?>"></script>
+140
View File
@@ -0,0 +1,140 @@
<?php
// Calcul du budget global pour la page de synthèse
$totalGeneral = $holiday['budget_food'] + $holiday['budget_extra'];
foreach ($generalItems as $gi) {
$totalGeneral += $gi['amount'];
}
$totalSteps = 0;
foreach ($steps as $step) {
$totalSteps += $step['total_amount'];
}
$grandTotal = $totalGeneral + $totalSteps;
?>
<div style="display: none;">
<div id="travelBookTemplate" style="padding: 40px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #1e293b; background: white; width: 100%; max-width: 800px; margin: 0 auto;">
<div style="text-align: center; padding-top: 50px; margin-bottom: 60px;">
<div style="font-size: 4rem; margin-bottom: 20px;">🗺️</div>
<h1 style="font-size: 2.5rem; color: #0f172a; margin-bottom: 10px;"><?= htmlspecialchars($holiday['title'] ?? $holiday['name'] ?? 'Mon Voyage') ?></h1>
<h2 style="font-size: 1.5rem; color: #64748b; font-weight: normal; margin-top: 0;">
<?php if (!empty($holiday['start_date']) && !empty($holiday['end_date'])): ?>
Du <?= date('d/m/Y', strtotime($holiday['start_date'])) ?> au <?= date('d/m/Y', strtotime($holiday['end_date'])) ?>
<?php else: ?>
Dates à définir
<?php endif; ?>
</h2>
</div>
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; margin-bottom: 40px; page-break-inside: avoid;">
<h3 style="margin-top: 0; color: #0f172a; border-bottom: 2px solid #cbd5e1; padding-bottom: 10px;">💰 Budget Prévisionnel : <?= number_format($grandTotal, 2, ',', ' ') ?> €</h3>
<table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
<tr>
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; color: #475569;">Frais Généraux (Vols, Locations globales...)</td>
<td style="padding: 8px 0; border-bottom: 1px solid #e2e8f0; text-align: right; font-weight: bold;"><?= number_format($totalGeneral, 2, ',', ' ') ?> €</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #475569;">Étapes (Hébergements, Activités...)</td>
<td style="padding: 8px 0; text-align: right; font-weight: bold;"><?= number_format($totalSteps, 2, ',', ' ') ?> €</td>
</tr>
</table>
</div>
<?php if (!empty($generalItems) || $holiday['budget_food'] > 0 || $holiday['budget_extra'] > 0): ?>
<div style="margin-bottom: 40px; page-break-inside: avoid;">
<h3 style="color: #0f172a; border-bottom: 2px solid #0ea5e9; padding-bottom: 5px;">🌍 Réservations & Frais Généraux</h3>
<ul style="list-style-type: none; padding-left: 0; margin-top: 15px;">
<?php if ($holiday['budget_food'] > 0): ?>
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">🍔 Budget Nourriture : <strong><?= number_format($holiday['budget_food'], 2, ',', ' ') ?> €</strong></li>
<?php endif; ?>
<?php if ($holiday['budget_extra'] > 0): ?>
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9;">🎁 Extras & Souvenirs : <strong><?= number_format($holiday['budget_extra'], 2, ',', ' ') ?> €</strong></li>
<?php endif; ?>
<?php foreach ($generalItems as $gi):
$icon = match($gi['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
?>
<li style="padding: 8px 0; border-bottom: 1px solid #f1f5f9; display: flex; justify-content: space-between;">
<span><?= $icon ?> <?= htmlspecialchars($gi['name']) ?></span>
<span><strong><?= number_format($gi['amount'], 2, ',', ' ') ?> €</strong> <span style="font-size: 0.85em; color: #64748b;"><?= $gi['is_paid'] ? '(Payé ✓)' : '(À payer ⏳)' ?></span></span>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<div style="page-break-before: always;"></div>
<h2 style="color: #0f172a; font-size: 2rem; border-bottom: 3px solid #0f172a; padding-bottom: 10px; margin-bottom: 30px;">📍 Itinéraire Détaillé</h2>
<?php foreach ($steps as $index => $step): ?>
<?php if ($index > 0): ?>
<div style="text-align: center; padding: 10px 0; color: #94a3b8; page-break-inside: avoid;">
<div style="border-left: 2px dashed #cbd5e1; height: 25px; margin: 0 auto; width: 2px;"></div>
<div style="margin: 5px 0; font-size: 0.9rem;">🚗 <em>En route vers l'étape suivante...</em></div>
<div style="border-left: 2px dashed #cbd5e1; height: 25px; margin: 0 auto; width: 2px;"></div>
</div>
<?php endif; ?>
<div style="background: #ffffff; border: 1px solid #e2e8f0; border-left: 6px solid #0ea5e9; border-radius: 8px; padding: 20px; margin-bottom: 0px; page-break-inside: avoid;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px;">
<div>
<h3 style="margin: 0; font-size: 1.4rem; color: #0f172a;"><?= htmlspecialchars($step['location_name']) ?></h3>
<?php if (!empty($step['step_start_date']) && !empty($step['step_end_date'])): ?>
<div style="color: #0ea5e9; margin-top: 5px; font-weight: bold; font-size: 0.95rem;">
📅 Du <?= date('d/m', strtotime($step['step_start_date'])) ?> au <?= date('d/m', strtotime($step['step_end_date'])) ?>
</div>
<?php endif; ?>
</div>
<div style="text-align: right;">
<span style="background: #f1f5f9; padding: 5px 10px; border-radius: 6px; font-weight: bold; color: #0f172a;">
<?= number_format($step['total_amount'], 2, ',', ' ') ?> €
</span>
</div>
</div>
<?php $checkpoints = isset($step['items']) ? $step['items'] : []; ?>
<?php if (!empty($checkpoints)): ?>
<div style="margin-top: 15px; border-top: 1px solid #f8fafc; padding-top: 15px;">
<h4 style="margin-top: 0; margin-bottom: 10px; color: #475569; font-size: 1rem;">📋 Planning & Activités :</h4>
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
<?php foreach ($checkpoints as $cp):
$cpIcon = match($cp['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
// Sécurité sur la date d'activité (item_date ou date)
$cpDate = !empty($cp['item_date']) ? $cp['item_date'] : (!empty($cp['date']) ? $cp['date'] : null);
?>
<tr>
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; color: #64748b; width: 80px;">
<?= $cpDate ? date('d/m', strtotime($cpDate)) : '---' ?>
</td>
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; color: #334155;">
<?= $cpIcon ?> <?= htmlspecialchars($cp['name']) ?>
</td>
<td style="padding: 6px 0; border-bottom: 1px solid #f8fafc; text-align: right; font-weight: bold; color: #0f172a;">
<?= number_format($cp['amount'], 2, ',', ' ') ?> €
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<div style="page-break-before: always;"></div>
<h2 style="color: #0f172a; font-size: 2rem; border-bottom: 3px solid #0f172a; padding-bottom: 10px; margin-bottom: 30px;">📝 Notes & Informations utiles</h2>
<p style="color: #64748b; margin-bottom: 40px; font-style: italic;">Espace réservé pour vos numéros d'urgence, codes de cadenas, adresses locales...</p>
<?php for ($i = 0; $i < 15; $i++): ?>
<div style="border-bottom: 1px dotted #cbd5e1; height: 35px; width: 100%;"></div>
<?php endfor; ?>
<div style="text-align: center; margin-top: 60px; font-size: 0.85rem; color: #94a3b8;">
Carnet de route généré automatiquement par <strong>HouseHub</strong>. Bon voyage ! ✈️
</div>
</div>
</div>