feat: implement GPS navigation integration with modal selection and persistent provider preferences
This commit is contained in:
parent
0492d5ca1d
commit
845933c33c
5 changed files with 245 additions and 15 deletions
|
|
@ -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);
|
||||||
|
|
@ -69,12 +69,24 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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 %}
|
{% if not intervention.assigned_member %}
|
||||||
<i class="bi bi-person-x fs-5 s-4" title="Non assignee"></i>
|
<i class="bi bi-person-x fs-5 s-4" title="Non assignee"></i>
|
||||||
{% else %}
|
{% 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>
|
<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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p class="text-muted">{% translate "Aucune intervention trouvée." %}</p>
|
<p class="text-muted">{% translate "Aucune intervention trouvée." %}</p>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -506,13 +506,23 @@
|
||||||
<p class="mb-1 text-muted">
|
<p class="mb-1 text-muted">
|
||||||
<i class="bi bi-calendar-event"></i> {{ intervention.planned_begin_time|date:"d/m/Y H:i"|default:"/" }}
|
<i class="bi bi-calendar-event"></i> {{ intervention.planned_begin_time|date:"d/m/Y H:i"|default:"/" }}
|
||||||
</p>
|
</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
|
<i class="bi bi-gear
|
||||||
{% if intervention.maintain_type == 'corrective' %}text-danger
|
{% if intervention.maintain_type == 'corrective' %}text-danger
|
||||||
{% elif intervention.maintain_type == 'preventive' %}text-warning
|
{% elif intervention.maintain_type == 'preventive' %}text-warning
|
||||||
{% elif intervention.maintain_type == 'ameliorative' %}text-success
|
{% elif intervention.maintain_type == 'ameliorative' %}text-success
|
||||||
{% endif %}"
|
{% endif %}"
|
||||||
></i> {{ intervention.get_maintain_type_display }}
|
></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>
|
</p>
|
||||||
|
|
||||||
{% include "interventions/includes/_operation_comment_modal.html" %}
|
{% include "interventions/includes/_operation_comment_modal.html" %}
|
||||||
|
|
@ -2331,10 +2341,19 @@
|
||||||
<span class="fw-semibold d-flex align-items-center gap-2 small">
|
<span class="fw-semibold d-flex align-items-center gap-2 small">
|
||||||
<i class="bi bi-map-fill"></i> {% translate "Carte" %}
|
<i class="bi bi-map-fill"></i> {% translate "Carte" %}
|
||||||
</span>
|
</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>
|
<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>
|
||||||
<div id="intervention-map"></div>
|
<div id="intervention-map"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include "interventions/includes/_gps_navigation_modal.html" %}
|
||||||
|
<script src="{% static 'interventions/mobile/gps_navigation.js' %}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- ===== Bouton flottant Discussion (au-dessus du bouton carte) ===== -->
|
<!-- ===== Bouton flottant Discussion (au-dessus du bouton carte) ===== -->
|
||||||
|
|
|
||||||
|
|
@ -253,9 +253,13 @@
|
||||||
</button>
|
</button>
|
||||||
<div id="intervention-map"></div>
|
<div id="intervention-map"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal GPS -->
|
||||||
|
{% include "interventions/includes/_gps_navigation_modal.html" %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
|
<script src="{% static 'interventions/mobile/gps_navigation.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
/* =========================
|
/* =========================
|
||||||
1) SUBMIT AJAX + PERSISTANCE DES PARAMS (dont 'sort')
|
1) SUBMIT AJAX + PERSISTANCE DES PARAMS (dont 'sort')
|
||||||
|
|
@ -624,7 +628,6 @@
|
||||||
status: m.status,
|
status: m.status,
|
||||||
maintain_type: m.maintain_type,
|
maintain_type: m.maintain_type,
|
||||||
url: m.url,
|
url: m.url,
|
||||||
fill: hexColor(m.status, m.status_color),
|
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
@ -637,6 +640,11 @@
|
||||||
const fill = hexColor(p.status, p.status_color);
|
const fill = hexColor(p.status, p.status_color);
|
||||||
const txtCol = isTextDark(p.status, p.status_color) ? '#000' : '#fff';
|
const txtCol = isTextDark(p.status, p.status_color) ? '#000' : '#fff';
|
||||||
const sep = idx > 0 ? '<hr style="margin:5px 0">' : '';
|
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}
|
return `${sep}
|
||||||
<div style="margin:2px 0">
|
<div style="margin:2px 0">
|
||||||
<a href="${p.url}">${p.code}</a>
|
<a href="${p.url}">${p.code}</a>
|
||||||
|
|
@ -644,6 +652,7 @@
|
||||||
border-radius:4px;padding:1px 6px;font-size:0.7rem;margin-left:4px">
|
border-radius:4px;padding:1px 6px;font-size:0.7rem;margin-left:4px">
|
||||||
${p.status_label}
|
${p.status_label}
|
||||||
</span>
|
</span>
|
||||||
|
${gpsBtn}
|
||||||
<div style="font-size:0.82rem;color:#555;margin-top:2px">${p.title}</div>
|
<div style="font-size:0.82rem;color:#555;margin-top:2px">${p.title}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue