feat: implement GPS navigation integration with modal selection and persistent provider preferences

This commit is contained in:
kdeterme 2026-08-05 15:30:06 +02:00
parent 0492d5ca1d
commit 845933c33c
5 changed files with 245 additions and 15 deletions

View file

@ -0,0 +1,128 @@
/**
* Helper JS pour la gestion de la navigation GPS dans StreetUp Mobile.
*/
(function(window) {
'use strict';
let activeGpsLat = null;
let activeGpsLon = null;
/**
* Ouvre le modal de sélection GPS ou lance le GPS préféré s'il est enregistré.
* @param {number|string} lat Latitude
* @param {number|string} lon Longitude
* @param {string} [label] Libellé/Code de l'intervention
* @param {boolean} [forceModal=false] Forcer l'affichage du modal même si une préférence existe
* @param {Event} [event=null] Événement clic pour stopper la propagation
*/
function openGpsNavigation(lat, lon, label, forceModal, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
if (!lat || !lon || isNaN(parseFloat(lat)) || isNaN(parseFloat(lon))) {
if (window.toast || window.alert) {
alert("Aucune coordonnée GPS disponible pour cette intervention.");
}
return;
}
activeGpsLat = parseFloat(lat);
activeGpsLon = parseFloat(lon);
const preferredProvider = localStorage.getItem('streetup_preferred_gps');
if (preferredProvider && !forceModal) {
launchGpsProvider(preferredProvider);
return;
}
const modalEl = document.getElementById('gpsNavigationModal');
if (!modalEl) {
console.error("Modal #gpsNavigationModal introuvable.");
return;
}
const destLabelEl = document.getElementById('gpsModalDestinationLabel');
if (destLabelEl) {
if (label) {
destLabelEl.textContent = "Itinéraire vers : " + label;
} else {
destLabelEl.textContent = "Choisissez votre application de navigation :";
}
}
const rememberCheckbox = document.getElementById('gpsRememberChoice');
if (rememberCheckbox) {
rememberCheckbox.checked = !!preferredProvider;
}
const bsModal = bootstrap.Modal.getOrCreateInstance(modalEl);
bsModal.show();
}
/**
* Lance la navigation avec l'application choisie.
* @param {string} provider 'google', 'waze', 'apple', ou 'geo'
*/
function launchGpsProvider(provider) {
if (!activeGpsLat || !activeGpsLon) return;
const rememberCheckbox = document.getElementById('gpsRememberChoice');
if (rememberCheckbox && rememberCheckbox.checked) {
localStorage.setItem('streetup_preferred_gps', provider);
} else {
localStorage.removeItem('streetup_preferred_gps');
}
let url = '';
const lat = activeGpsLat;
const lon = activeGpsLon;
switch (provider) {
case 'google':
url = `https://www.google.com/maps/dir/?api=1&destination=${lat},${lon}`;
break;
case 'waze':
url = `https://waze.com/ul?ll=${lat},${lon}&navigate=yes`;
break;
case 'apple':
url = `https://maps.apple.com/?daddr=${lat},${lon}`;
break;
case 'geo':
url = `geo:${lat},${lon}?q=${lat},${lon}`;
break;
default:
url = `https://www.google.com/maps/dir/?api=1&destination=${lat},${lon}`;
}
const modalEl = document.getElementById('gpsNavigationModal');
if (modalEl) {
const bsModal = bootstrap.Modal.getInstance(modalEl);
if (bsModal) {
bsModal.hide();
}
}
// Pour les schémas personnalisés/intentions comme geo:, window.location.href fonctionne mieux
if (provider === 'geo' || provider === 'apple' || provider === 'waze') {
window.location.href = url;
} else {
window.open(url, '_blank', 'noopener,noreferrer');
}
}
/**
* Réinitialise la préférence de GPS sélectionnée.
*/
function resetGpsPreference() {
localStorage.removeItem('streetup_preferred_gps');
}
// Exposer les fonctions globalement
window.openGpsNavigation = openGpsNavigation;
window.launchGpsProvider = launchGpsProvider;
window.resetGpsPreference = resetGpsPreference;
})(window);

View file

@ -69,12 +69,24 @@
</div>
{% endif %}
</div>
<div class="d-flex align-items-center gap-2">
{% if intervention.lat and intervention.lon %}
<button type="button"
class="btn btn-outline-primary btn-sm rounded-circle p-0 d-flex align-items-center justify-content-center"
style="width: 34px; height: 34px;"
title="{% translate 'Itinéraire GPS' %}"
aria-label="{% translate 'Itinéraire GPS' %}"
onclick="openGpsNavigation('{{ intervention.lat|stringformat:'f' }}', '{{ intervention.lon|stringformat:'f' }}', '{{ intervention.code|escapejs }}', false, event)">
<i class="bi bi-sign-turn-right-fill fs-6"></i>
</button>
{% endif %}
{% if not intervention.assigned_member %}
<i class="bi bi-person-x fs-5 s-4" title="Non assignee"></i>
{% else %}
<i class="bi bi-person-check fs-5 {% if request.user == intervention.assigned_member.user %}text-primary{% else %}text-info{% endif %}" title="Assignee"></i>
{% endif %}
</div>
</div>
</div>
{% empty %}
<p class="text-muted">{% translate "Aucune intervention trouvée." %}</p>

View file

@ -0,0 +1,62 @@
{% load i18n %}
<!-- Modal Sélection GPS Navigation -->
<div class="modal fade" id="gpsNavigationModal" tabindex="-1" aria-labelledby="gpsNavigationModalLabel" aria-hidden="true" style="z-index: 2050;">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content shadow-lg border-0 rounded-4">
<div class="modal-header border-bottom-0 pb-1">
<h5 class="modal-title fw-bold fs-6 d-flex align-items-center gap-2" id="gpsNavigationModalLabel">
<i class="bi bi-sign-turn-right-fill text-primary"></i>
{% translate "Itinéraire GPS" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body pt-2 pb-3">
<p class="text-muted small mb-3" id="gpsModalDestinationLabel">
{% translate "Choisissez votre application de navigation :" %}
</p>
<div class="d-grid gap-2 mb-3">
<!-- Google Maps -->
<button type="button" class="btn btn-outline-dark d-flex align-items-center justify-content-between py-2.5 px-3 rounded-3" onclick="launchGpsProvider('google')">
<span class="d-flex align-items-center gap-2 fw-semibold">
<i class="bi bi-google text-danger fs-5"></i> Google Maps
</span>
<i class="bi bi-box-arrow-up-right text-muted small"></i>
</button>
<!-- Waze -->
<button type="button" class="btn btn-outline-dark d-flex align-items-center justify-content-between py-2.5 px-3 rounded-3" onclick="launchGpsProvider('waze')">
<span class="d-flex align-items-center gap-2 fw-semibold">
<i class="bi bi-compass text-info fs-5"></i> Waze
</span>
<i class="bi bi-box-arrow-up-right text-muted small"></i>
</button>
<!-- Apple Maps -->
<button type="button" class="btn btn-outline-dark d-flex align-items-center justify-content-between py-2.5 px-3 rounded-3" id="gpsBtnApple" onclick="launchGpsProvider('apple')">
<span class="d-flex align-items-center gap-2 fw-semibold">
<i class="bi bi-apple text-body fs-5"></i> Apple Maps
</span>
<i class="bi bi-box-arrow-up-right text-muted small"></i>
</button>
<!-- Application système par défaut (geo:) -->
<button type="button" class="btn btn-outline-secondary d-flex align-items-center justify-content-between py-2.5 px-3 rounded-3" onclick="launchGpsProvider('geo')">
<span class="d-flex align-items-center gap-2 fw-semibold">
<i class="bi bi-phone-vibrate text-primary fs-5"></i> {% translate "App système par défaut" %}
</span>
<i class="bi bi-box-arrow-up-right text-muted small"></i>
</button>
</div>
<div class="form-check form-switch pt-2 border-top">
<input class="form-check-input" type="checkbox" role="switch" id="gpsRememberChoice">
<label class="form-check-label small text-secondary" for="gpsRememberChoice">
{% translate "Se souvenir de mon choix" %}
</label>
</div>
</div>
</div>
</div>
</div>

View file

@ -506,13 +506,23 @@
<p class="mb-1 text-muted">
<i class="bi bi-calendar-event"></i> {{ intervention.planned_begin_time|date:"d/m/Y H:i"|default:"/" }}
</p>
<p class="mb-3 text-muted">
<p class="mb-3 text-muted d-flex justify-content-between align-items-center flex-wrap gap-2">
<span>
<i class="bi bi-gear
{% if intervention.maintain_type == 'corrective' %}text-danger
{% elif intervention.maintain_type == 'preventive' %}text-warning
{% elif intervention.maintain_type == 'ameliorative' %}text-success
{% endif %}"
></i> {{ intervention.get_maintain_type_display }}
</span>
{% if intervention.lat and intervention.lon %}
<button type="button"
class="btn btn-outline-primary btn-sm rounded-pill fw-semibold px-3 py-1 d-inline-flex align-items-center gap-1.5 shadow-sm"
onclick="openGpsNavigation('{{ intervention.lat|stringformat:'f' }}', '{{ intervention.lon|stringformat:'f' }}', '{{ intervention.code|escapejs }}', false, event)">
<i class="bi bi-sign-turn-right-fill text-primary"></i>
<span>{% translate "Itinéraire GPS" %}</span>
</button>
{% endif %}
</p>
{% include "interventions/includes/_operation_comment_modal.html" %}
@ -2331,10 +2341,19 @@
<span class="fw-semibold d-flex align-items-center gap-2 small">
<i class="bi bi-map-fill"></i> {% translate "Carte" %}
</span>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-sm btn-light py-1 px-2.5 text-primary fw-semibold rounded-pill d-inline-flex align-items-center gap-1 shadow-sm"
onclick="openGpsNavigation('{{ intervention.lat|stringformat:'f' }}', '{{ intervention.lon|stringformat:'f' }}', '{{ intervention.code|escapejs }}', false, event)">
<i class="bi bi-sign-turn-right-fill"></i>{% translate "GPS" %}
</button>
<button id="map-close-btn" type="button" class="btn-close btn-close-white btn-sm" aria-label="{% translate 'Fermer la carte' %}"></button>
</div>
</div>
<div id="intervention-map"></div>
</div>
{% include "interventions/includes/_gps_navigation_modal.html" %}
<script src="{% static 'interventions/mobile/gps_navigation.js' %}"></script>
{% endif %}
<!-- ===== Bouton flottant Discussion (au-dessus du bouton carte) ===== -->

View file

@ -253,9 +253,13 @@
</button>
<div id="intervention-map"></div>
</div>
<!-- Modal GPS -->
{% include "interventions/includes/_gps_navigation_modal.html" %}
{% endblock content %}
{% block extra_scripts %}
<script src="{% static 'interventions/mobile/gps_navigation.js' %}"></script>
<script>
/* =========================
1) SUBMIT AJAX + PERSISTANCE DES PARAMS (dont 'sort')
@ -624,7 +628,6 @@
status: m.status,
maintain_type: m.maintain_type,
url: m.url,
fill: hexColor(m.status, m.status_color),
},
})),
};
@ -637,6 +640,11 @@
const fill = hexColor(p.status, p.status_color);
const txtCol = isTextDark(p.status, p.status_color) ? '#000' : '#fff';
const sep = idx > 0 ? '<hr style="margin:5px 0">' : '';
const coords = f.geometry && f.geometry.coordinates ? f.geometry.coordinates : null;
const gpsBtn = (coords && coords.length >= 2) ?
`<button type="button" class="btn btn-sm btn-outline-primary border-0 p-0 ms-2" title="Itinéraire GPS" onclick="openGpsNavigation('${coords[1]}', '${coords[0]}', '${p.code}', false, event)">
<i class="bi bi-sign-turn-right-fill"></i>
</button>` : '';
return `${sep}
<div style="margin:2px 0">
<a href="${p.url}">${p.code}</a>
@ -644,6 +652,7 @@
border-radius:4px;padding:1px 6px;font-size:0.7rem;margin-left:4px">
${p.status_label}
</span>
${gpsBtn}
<div style="font-size:0.82rem;color:#555;margin-top:2px">${p.title}</div>
</div>`;
}).join('');