This commit is contained in:
2026-04-02 11:57:30 +02:00
parent b425ec78aa
commit e07c57ea56
3 changed files with 357 additions and 65 deletions
+223
View File
@@ -716,3 +716,226 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover {
text-align: left !important; /* On annule l'alignement à droite du total */
}
}
/* ==========================================================================
13. BARRE DE PROGRESSION & FINANCES
========================================================================== */
.hol-progress-bar {
width: 100%;
height: 12px;
background: #e2e8f0;
border-radius: 6px;
margin-bottom: 10px;
display: flex;
overflow: hidden;
}
.hol-progress-paid {
background: #10b981;
}
.hol-progress-saved {
background: #3b82f6;
}
.hol-progress-labels {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
}
.hol-label-paid {
color: #10b981;
font-weight: 600;
}
.hol-label-saved {
color: #3b82f6;
font-weight: 600;
}
.hol-label-left {
color: #ef4444;
font-weight: 700;
}
/* ==========================================================================
14. LÉGENDE DE LA CARTE
========================================================================== */
.hol-panel-header-group {
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.hol-map-legend {
display: flex;
gap: 12px;
font-size: 0.75rem;
color: #64748b;
background: white;
padding: 4px 10px;
border-radius: 12px;
border: 1px solid #e2e8f0;
}
.hol-legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.hol-legend-color {
display: inline-block;
width: 12px;
height: 4px;
border-radius: 2px;
}
.hol-legend-color.aller {
background: #3b82f6;
}
.hol-legend-color.interm {
background: #8b5cf6;
}
.hol-legend-color.retour {
background: transparent;
height: 0;
border-bottom: 3px dashed #f97316;
border-radius: 0;
}
/* ==========================================================================
15. LIGNES DE DÉPENSES (Timeline & Modale JS)
========================================================================== */
.hol-expense-wrapper {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
margin-bottom: 8px;
border-bottom: 1px dashed #f1f5f9;
padding-bottom: 8px;
}
.hol-expense-wrapper:last-child {
margin-bottom: 0;
border-bottom: none;
padding-bottom: 0;
}
.hol-expense-main {
display: flex;
justify-content: space-between;
width: 100%;
font-size: 0.85rem;
}
.hol-expense-name {
color: #475569;
font-weight: 500;
}
.hol-expense-amount {
color: var(--text-main);
font-weight: bold;
}
.status-paid {
color: #10b981;
margin-left: 5px;
}
.status-pending {
color: #f59e0b;
margin-left: 5px;
}
.hol-expense-note {
font-size: 0.75rem;
color: #94a3b8;
padding-left: 24px;
word-break: break-all;
}
.hol-expense-link {
color: #3b82f6;
text-decoration: none;
}
.hol-expense-link:hover {
text-decoration: underline;
}
.hol-empty-step {
font-size: 0.8rem;
color: var(--text-muted);
font-style: italic;
padding: 5px 0;
}
/* Boutons de petite taille */
.pf-btn-small {
padding: 6px 14px !important;
font-size: 0.85rem !important;
width: auto !important;
height: auto !important;
margin: 0 !important;
}
/* Modale JS d'ajout de dépense */
.hol-form-row {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.hol-form-inner {
display: flex;
gap: 8px;
align-items: center;
width: 100%;
}
.hol-form-select {
width: 50px;
padding: 8px 4px;
font-size: 1.2rem;
cursor: pointer;
}
.hol-form-text {
flex: 2;
padding: 8px;
font-size: 0.9rem;
}
.hol-form-number {
width: 80px;
text-align: right;
padding: 8px;
font-size: 0.9rem;
}
.hol-form-paid-label {
display: flex;
align-items: center;
cursor: pointer;
width: 55px;
}
.hol-form-paid-text {
font-size: 0.75rem;
margin-left: 4px;
font-weight: bold;
color: #64748b;
}
.hol-form-notes-input {
font-size: 0.8rem;
padding: 6px 8px;
border-style: dashed;
color: #475569;
width: calc(100% - 58px);
margin-left: 58px;
}
.btn-remove-expense {
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;
transition: 0.2s;
}
.btn-remove-expense:hover {
background: #fca5a5;
color: #b91c1c;
}
+96 -28
View File
@@ -200,13 +200,12 @@ function initDetailMap() {
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);
// Carte standard : Affiche toujours la langue locale exacte (Français en France, Catalan en Catalogne)
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(detailMap);
if (MAP_POINTS.length === 0) {
detailMap.setView([46.6, 2.4], 5); // Centré sur la France par défaut si vide
@@ -263,12 +262,87 @@ function initDetailMap() {
});
});
// On dessine le trait en pointillés pour relier le roadtrip !
// On dessine le vrai trajet par la route avec gestion des superpositions
if (latlngs.length > 1) {
L.polyline(latlngs, {
color: "#3b82f6",
weight: 3,
dashArray: "8, 8",
const routePromises = [];
// 1. On lance toutes les requêtes de calcul de route en même temps
for (let i = 0; i < latlngs.length - 1; i++) {
const startPt = MAP_POINTS[i];
const endPt = MAP_POINTS[i + 1];
const coordsString = `${startPt.lng},${startPt.lat};${endPt.lng},${endPt.lat}`;
const promise = fetch(
`https://router.project-osrm.org/route/v1/driving/${coordsString}?overview=full&geometries=geojson`,
)
.then((response) => response.json())
.then((data) => ({
index: i,
data: data,
coords: [latlngs[i], latlngs[i + 1]],
}))
.catch((err) => ({
index: i,
error: true,
coords: [latlngs[i], latlngs[i + 1]],
}));
routePromises.push(promise);
}
// 2. On attend que TOUTES les routes soient calculées pour les dessiner dans le bon ordre
Promise.all(routePromises).then((results) => {
// On s'assure de dessiner l'Étape 1 d'abord, puis la 2, etc.
results.sort((a, b) => a.index - b.index);
results.forEach((res) => {
const i = res.index;
// Styles par défaut (Aller / Étape 1)
let routeColor = "#3b82f6"; // Bleu
let routeWeight = 6; // Épais
let routeDash = null; // Ligne continue
// Modification des styles selon le segment
if (i === latlngs.length - 2) {
// DERNIER SEGMENT (Retour)
routeColor = "#f97316"; // Orange vif (Tranche parfaitement avec le bleu)
routeWeight = 3; // Plus fin pour se superposer sans cacher
routeDash = "10, 10"; // En pointillés pour laisser voir le bleu en dessous
} else if (i > 0) {
// SEGMENTS INTERMÉDIAIRES
routeColor = "#8b5cf6"; // Violet
routeWeight = 5;
}
// Dessin du trait
if (res.data && res.data.code === "Ok" && res.data.routes.length > 0) {
const routeCoords = res.data.routes[0].geometry.coordinates.map(
(c) => [c[1], c[0]],
);
L.polyline(routeCoords, {
color: routeColor,
weight: routeWeight,
dashArray: routeDash,
opacity: 0.9,
lineCap: "round",
lineJoin: "round",
}).addTo(detailMap);
} else {
// Plan B si pas de route carrossable (ex: avion/bateau)
drawFallbackLine(res.coords, routeColor, routeWeight);
}
});
});
}
// Plan B : La ligne droite
function drawFallbackLine(coords, color, weight) {
L.polyline(coords, {
color: color,
weight: weight || 3,
dashArray: "8, 8", // Toujours en pointillés pour le plan B
opacity: 0.7,
}).addTo(detailMap);
}
@@ -395,35 +469,29 @@ function addCpExpenseLine(
) {
const container = document.getElementById("cpExpensesContainer");
const div = document.createElement("div");
div.style.display = "flex";
div.style.flexDirection = "column";
div.style.gap = "6px";
div.style.padding = "10px";
div.style.background = "#f8fafc";
div.style.borderRadius = "8px";
div.style.border = "1px solid #e2e8f0";
div.className = "hol-form-row"; // Classe globale de la ligne
const isChecked = isPaid == 1 ? "checked" : "";
div.innerHTML = `
<div style="display:flex; gap:8px; align-items:center; width:100%;">
<select name="items[cat][]" class="pf-input" style="width:50px; padding:8px 4px; font-size:1.2rem; cursor:pointer;" title="Catégorie">
<div class="hol-form-inner">
<select name="items[cat][]" class="pf-input hol-form-select" 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;">
<input type="text" name="items[name][]" class="pf-input hol-form-text" placeholder="Libellé (Optionnel)" value="${name}">
<input type="number" step="0.01" name="items[amount][]" class="pf-input hol-form-number" placeholder="0.00" value="${amount}">
<label title="Payé ?" style="display:flex; align-items:center; cursor:pointer; width:55px;">
<input type="checkbox" ${isChecked} onchange="this.nextElementSibling.value = this.checked ? 1 : 0" style="margin:0;">
<label class="hol-form-paid-label" title="Payé ?">
<input type="checkbox" ${isChecked} onchange="this.nextElementSibling.value = this.checked ? 1 : 0">
<input type="hidden" name="items[paid][]" value="${isPaid}">
<span style="font-size:0.75rem; margin-left:4px; font-weight:bold; color:#64748b;">Payé</span>
<span class="hol-form-paid-text">Payé</span>
</label>
<button type="button" onclick="this.parentElement.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;">&times;</button>
<button type="button" class="btn-remove-expense" onclick="this.parentElement.parentElement.remove()" title="Supprimer">&times;</button>
</div>
<input type="text" name="items[notes][]" class="pf-input" placeholder="🔗 Lien de réservation ou adresse exacte..." value="${notes}" style="font-size:0.8rem; padding:6px 8px; border-style:dashed; color:#475569; width:calc(100% - 58px); margin-left:58px;">
<input type="text" name="items[notes][]" class="pf-input hol-form-notes-input" placeholder="🔗 Lien de réservation ou notes..." value="${notes}">
`;
container.appendChild(div);
}
+37 -36
View File
@@ -18,22 +18,19 @@ $stmt = $pdo->prepare("
$stmt->execute([$id]);
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
// IMPORTANT : Le tri se fait maintenant sur "sort_order" pour que le glisser-déposer fonctionne
$stmtItems = $pdo->prepare("SELECT * FROM pf_holidays_items WHERE holiday_id = ? ORDER BY sort_order ASC, 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 INDEX D'ORDRE (Pour autoriser les doublons de lieux comme "Maison")
$steps = [];
$generalItems = [];
foreach ($items as $it) {
if ($it['location_name'] !== null) {
$orderKey = $it['sort_order']; // On utilise l'ordre comme clé unique de l'étape
$orderKey = $it['sort_order'];
if (!isset($steps[$orderKey])) {
$steps[$orderKey] = [
'location_name' => $it['location_name'],
@@ -53,7 +50,6 @@ foreach ($items as $it) {
ksort($steps);
$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']));
@@ -75,11 +71,11 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<div class="hol-detail-header">
<div class="hol-detail-title-group">
<a href="?tab=list" class="pf-btn btn-secondary" style="padding:6px 12px; height:auto; width:auto; text-decoration:none;">◀ Retour</a>
<a href="?tab=list" class="pf-btn btn-secondary">◀ Retour</a>
<h1><?= htmlspecialchars($holiday['title']) ?></h1>
<span class="hol-badge-status"><?= 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; margin:0;">⚙️ Modifier les bases</button>
<button onclick='editHoliday(<?= htmlspecialchars(json_encode(['main' => $holiday, 'items' => $generalItems]), ENT_QUOTES, 'UTF-8') ?>)' class="pf-btn btn-secondary">⚙️ Modifier les bases</button>
</div>
<div class="hol-summary-card">
@@ -92,36 +88,43 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<div class="hol-summary-label">Budget Food & Extras</div>
<div class="hol-summary-value">🍔 <?= number_format($holiday['budget_food'], 0) ?> € | 🎁 <?= number_format($holiday['budget_extra'], 0) ?> €</div>
</div>
<div class="hol-summary-item" style="text-align:right;">
<div class="hol-summary-item">
<div class="hol-summary-label">Coût Total Estimé</div>
<div class="hol-summary-value total"><?= 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 class="hol-progress-bar">
<div class="hol-progress-paid" style="width:<?= $pctPaid ?>%;" title="Payé"></div>
<div class="hol-progress-saved" style="width:<?= $pctSaved ?>%;" 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 class="hol-progress-labels">
<span class="hol-label-paid">✓ Payé : <?= number_format($paid, 0, ',', ' ') ?> €</span>
<span class="hol-label-saved">💼 Provisionné : <?= number_format($saved, 0, ',', ' ') ?> €</span>
<span class="hol-label-left">⏳ Reste à payer : <?= number_format($leftToPay, 0, ',', ' ') ?> €</span>
</div>
</div>
<div class="hol-layout-grid">
<div class="hol-panel">
<div class="hol-panel-header">
<h3>🗺️ Itinéraire & Checkpoints</h3>
<button class="pf-btn" onclick="openCheckpointModal('add')" style="padding:6px 12px; height:auto; width:auto; font-size:0.85rem; margin:0;">📍 Placer une étape</button>
<div class="hol-panel-header" style="flex-wrap: wrap; gap: 10px;">
<div class="hol-panel-header-group">
<h3 style="margin:0;">🗺️ Itinéraire</h3>
<div class="hol-map-legend">
<span class="hol-legend-item" title="Trajet aller"><span class="hol-legend-color aller"></span> Aller</span>
<span class="hol-legend-item" title="Étapes intermédiaires"><span class="hol-legend-color interm"></span> Interm.</span>
<span class="hol-legend-item" title="Trajet retour"><span class="hol-legend-color retour"></span> Retour</span>
</div>
</div>
<button class="pf-btn pf-btn-small" onclick="openCheckpointModal('add')">📍 Placer une étape</button>
</div>
<div id="tripMap" style="flex:1; width:100%; background:#f1f5f9;"></div>
</div>
<div class="hol-panel">
<div class="hol-panel-header">
<h3>📝 Détail des étapes</h3>
<h3 style="margin:0;">📝 Détail des étapes</h3>
</div>
<div class="hol-panel-body">
@@ -137,12 +140,10 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
📍 <?= htmlspecialchars($step['location_name']) ?>
</div>
</div>
<div class="hol-cp-actions-group">
<div class="hol-cp-total"><?= number_format($step['total_amount'], 2, ',', ' ') ?> €</div>
<button onclick='openCheckpointModal("edit", <?= htmlspecialchars(json_encode($step), ENT_QUOTES, "UTF-8") ?>)' class="btn-icon-small" title="Modifier">✏️</button>
</div>
</div>
<div class="hol-cp-body">
@@ -153,18 +154,18 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
$visibleItemsCount++;
$icon = match($it['category']) { 'transport' => '🚗', 'accommodation' => '🏨', 'activity' => '🎫', default => '🏷️' };
?>
<div class="hol-expense-line" style="display:flex; flex-direction:column; align-items:flex-start; gap:4px;">
<div style="display:flex; justify-content:space-between; width:100%;">
<span style="color:#475569; font-weight:500;"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
<div class="hol-expense-wrapper">
<div class="hol-expense-main">
<span class="hol-expense-name"><?= $icon ?> <?= htmlspecialchars($it['name']) ?></span>
<span>
<strong style="color:var(--text-main);"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
<span style="margin-left:5px; color:<?= $it['is_paid'] ? '#10b981' : '#f59e0b' ?>;"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
<strong class="hol-expense-amount"><?= number_format($it['amount'], 2, ',', ' ') ?> €</strong>
<span class="<?= $it['is_paid'] ? 'status-paid' : 'status-pending' ?>" title="<?= $it['is_paid'] ? 'Payé' : 'À payer' ?>"><?= $it['is_paid'] ? '✓' : '⏳' ?></span>
</span>
</div>
<?php if (!empty($it['notes'])): ?>
<div style="font-size:0.75rem; color:#94a3b8; padding-left: 24px; word-break: break-all;">
<div class="hol-expense-note">
<?php if (filter_var($it['notes'], FILTER_VALIDATE_URL)): ?>
🔗 <a href="<?= htmlspecialchars($it['notes']) ?>" target="_blank" style="color:#3b82f6; text-decoration:none;">Voir le lien</a>
🔗 <a href="<?= htmlspecialchars($it['notes']) ?>" target="_blank" class="hol-expense-link">Voir le lien</a>
<?php else: ?>
📝 <?= htmlspecialchars($it['notes']) ?>
<?php endif; ?>
@@ -174,7 +175,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<?php endforeach; ?>
<?php if ($visibleItemsCount === 0): ?>
<div style="font-size:0.8rem; color:var(--text-muted); font-style:italic; padding: 5px 0;">
<div class="hol-empty-step">
📍 Point de passage (Aucune dépense)
</div>
<?php endif; ?>
@@ -205,7 +206,7 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<?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'])) ?>')">
<button type="button" class="pf-btn btn-secondary" style="padding:4px 10px; font-size:0.8rem; height:auto; width:auto; border-radius:20px;" onclick="selectPlace(<?= $fav['lat'] ?>, <?= $fav['lng'] ?>, '<?= htmlspecialchars(addslashes($fav['name'])) ?>')">
⭐ <?= htmlspecialchars($fav['name']) ?>
</button>
<?php endforeach; ?>
@@ -228,12 +229,12 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<input type="hidden" name="old_sort_order" id="cp_old_sort_order">
<div class="form-group" style="margin-bottom:15px;">
<label class="pf-label">Nom de l'étape (Ce qui s'affichera sur la carte)</label>
<label class="pf-label">Nom de l'étape</label>
<input type="text" name="location_name" id="cp_name" class="pf-input" style="font-weight:bold; color:var(--primary);" 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>
<label class="pf-label" style="margin:0;">Dépenses prévues</label>
<button type="button" class="pf-btn btn-secondary" onclick="addCpExpenseLine()" style="padding:4px 8px; font-size:0.8rem; height:auto; width:auto; margin:0;">+ Ajouter une dépense</button>
</div>
@@ -242,18 +243,18 @@ $pctSaved = $cost > 0 ? min(100 - $pctPaid, ($saved / $cost) * 100) : 0;
<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
<input type="checkbox" name="save_favorite" value="1" style="margin-right:8px;">
⭐ Sauvegarder dans mes favoris
</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>
<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</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>
<button type="submit" class="pf-btn" style="width:auto; margin:0;">Enregistrer</button>
</div>
</div>
</form>