diff --git a/apps/ops/src/composables/useTechManagement.js b/apps/ops/src/composables/useTechManagement.js
index 2d68767..a704f13 100644
--- a/apps/ops/src/composables/useTechManagement.js
+++ b/apps/ops/src/composables/useTechManagement.js
@@ -212,12 +212,29 @@ export function useTechManagement (store, invalidateRoutes) {
invalidateRoutes()
}
+ // Persist a tech's "home base" coordinates (where they start the day from
+ // when no live GPS is available). Coords are stored in ERPNext as
+ // separate Float fields `latitude` / `longitude`; the dispatch store
+ // reads them back as `tech.coords = [longitude, latitude]` (the
+ // [lng, lat] order is what Mapbox/MapLibre + GeoJSON expect).
+ // Route optimization is recomputed because changing the start point
+ // shifts every tech's optimal job sequence for the day.
+ async function saveTechHome (tech, longitude, latitude) {
+ const lng = Number(longitude); const lat = Number(latitude)
+ if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
+ throw new Error('Coordonnées invalides')
+ }
+ tech.coords = [lng, lat]
+ await updateTech(tech.name || tech.id, { longitude: lng, latitude: lat })
+ invalidateRoutes()
+ }
+
return {
editingTech, newTechName, newTechPhone, newTechDevice, addingTech,
absenceModalOpen, absenceModalTech, absenceForm, absenceProcessing,
saveTechField, addTech,
openAbsenceModal, confirmAbsence, endAbsence,
- deactivateTech, reactivateTech, removeTech, saveTraccarLink, saveWeeklySchedule,
+ deactivateTech, reactivateTech, removeTech, saveTraccarLink, saveWeeklySchedule, saveTechHome,
ABSENCE_REASONS,
}
}
diff --git a/apps/ops/src/pages/DispatchPage.vue b/apps/ops/src/pages/DispatchPage.vue
index ef93f2f..e756995 100644
--- a/apps/ops/src/pages/DispatchPage.vue
+++ b/apps/ops/src/pages/DispatchPage.vue
@@ -401,7 +401,7 @@ const {
absenceModalOpen, absenceModalTech, absenceForm, absenceProcessing,
saveTechField, addTech,
openAbsenceModal, confirmAbsence, endAbsence,
- deactivateTech, reactivateTech, removeTech, saveTraccarLink, saveWeeklySchedule,
+ deactivateTech, reactivateTech, removeTech, saveTraccarLink, saveWeeklySchedule, saveTechHome,
ABSENCE_REASONS,
} = useTechManagement(store, invalidateRoutes)
@@ -411,6 +411,88 @@ const scheduleModalTech = ref(null)
const scheduleForm = ref({})
const extraShiftsForm = ref([]) // On-call / garde shifts
+// Edit a tech's home/departure coordinates. Two paths converge here:
+// • Type an address → free Nominatim geocode → confirm → save
+// • Use the live GPS position (if Traccar device is online) as the
+// new home base — handy after a tech moves
+// • Or paste lat/lng directly (advanced)
+// We don't proxy the geocode through targo-hub on purpose: Nominatim
+// allows browser calls with a sane User-Agent and there's no secret
+// involved. Hitting it from the SPA also keeps the hub free of the
+// tile-usage policy responsibility.
+async function openTechHomeDialog (tech) {
+ const cur = tech.coords || [-73.6756177, 45.1599145]
+ const dialog = $q.dialog({
+ title: `Position de départ — ${tech.fullName}`,
+ message: `
+
+ Adresse à géocoder, ou colle directement lat, lng.
+
+
+ Actuelle : ${cur[1].toFixed(5)}, ${cur[0].toFixed(5)}
+
+ `,
+ html: true,
+ prompt: {
+ model: '',
+ type: 'text',
+ outlined: true,
+ label: 'Adresse OU "lat, lng"',
+ placeholder: '1867 chemin de la Rivière, Sainte-Clotilde',
+ counter: false,
+ },
+ cancel: { flat: true, label: 'Annuler' },
+ ok: { color: 'primary', unelevated: true, label: 'Géocoder + Enregistrer' },
+ persistent: false,
+ })
+ dialog.onOk(async (input) => {
+ const txt = (input || '').trim()
+ if (!txt) return
+ // Direct lat/lng paste? Pattern: "45.16, -73.67"
+ const direct = txt.match(/^(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)$/)
+ if (direct) {
+ const lat = parseFloat(direct[1]); const lng = parseFloat(direct[2])
+ try {
+ await saveTechHome(tech, lng, lat)
+ Notify.create({ type: 'positive', message: `Position mise à jour pour ${tech.fullName}`, position: 'top', timeout: 2500 })
+ } catch (e) {
+ Notify.create({ type: 'negative', message: 'Erreur: ' + e.message, position: 'top' })
+ }
+ return
+ }
+ // Geocode via Nominatim
+ try {
+ const r = await fetch(
+ 'https://nominatim.openstreetmap.org/search?format=json&limit=1&q=' + encodeURIComponent(txt),
+ { headers: { 'Accept-Language': 'fr-CA,fr,en' } }
+ )
+ const data = await r.json()
+ if (!data?.length) {
+ Notify.create({ type: 'negative', message: 'Adresse introuvable', position: 'top' })
+ return
+ }
+ const hit = data[0]
+ const lat = parseFloat(hit.lat); const lng = parseFloat(hit.lon)
+ $q.dialog({
+ title: 'Confirmer la position',
+ message: `${hit.display_name}
${lat.toFixed(5)}, ${lng.toFixed(5)}
`,
+ html: true,
+ cancel: { flat: true, label: 'Annuler' },
+ ok: { color: 'primary', unelevated: true, label: 'Enregistrer' },
+ }).onOk(async () => {
+ try {
+ await saveTechHome(tech, lng, lat)
+ Notify.create({ type: 'positive', message: `Position mise à jour pour ${tech.fullName}`, position: 'top', timeout: 2500 })
+ } catch (e) {
+ Notify.create({ type: 'negative', message: 'Erreur: ' + e.message, position: 'top' })
+ }
+ })
+ } catch (e) {
+ Notify.create({ type: 'negative', message: 'Géocodage indisponible: ' + e.message, position: 'top' })
+ }
+ })
+}
+
function openScheduleModal (tech) {
scheduleModalTech.value = tech
scheduleForm.value = {}
@@ -1721,6 +1803,7 @@ onUnmounted(() => {
—
+
|
diff --git a/apps/ops/src/pages/dispatch-styles.scss b/apps/ops/src/pages/dispatch-styles.scss
index 1586950..1cec366 100644
--- a/apps/ops/src/pages/dispatch-styles.scss
+++ b/apps/ops/src/pages/dispatch-styles.scss
@@ -725,6 +725,8 @@
.sb-gps-absence-btn:hover { color:#f59e0b; background:rgba(245,158,11,0.1); }
.sb-gps-react-btn { background:none; border:none; color:#22c55e; font-size:14px; cursor:pointer; padding:2px 6px; border-radius:4px; }
.sb-gps-react-btn:hover { background:rgba(34,197,94,0.1); }
+.sb-gps-loc-btn { background:none; border:none; color:var(--sb-muted); font-size:13px; cursor:pointer; padding:2px 6px; border-radius:4px; }
+.sb-gps-loc-btn:hover { color:#3b82f6; background:rgba(59,130,246,0.1); }
.sb-gps-inactive-row { opacity:0.45; }
.sb-gps-inactive-row:hover { opacity:0.75; }
.sb-gps-toggle-row { margin-bottom:10px; }
diff --git a/apps/ops/src/stores/dispatch.js b/apps/ops/src/stores/dispatch.js
index beeace3..217da6c 100644
--- a/apps/ops/src/stores/dispatch.js
+++ b/apps/ops/src/stores/dispatch.js
@@ -78,7 +78,11 @@ export const useDispatchStore = defineStore('dispatch', () => {
resourceCategory: t.resource_category || '', // 'Véhicule' | 'Outil' | 'Salle' | etc.
user: t.user || null,
colorIdx: idx % TECH_COLORS.length,
- coords: [t.longitude || -73.5673, t.latitude || 45.5017],
+ // Default departure point = Gigafibre HQ (1867 chemin de la Rivière,
+ // Sainte-Clotilde, QC). Used when a tech has no explicit longitude
+ // /latitude saved in ERPNext, so route optimization has a sensible
+ // starting point even before the dispatcher sets it.
+ coords: [t.longitude || -73.6756177, t.latitude || 45.1599145],
gpsCoords: null,
gpsSpeed: 0,
gpsTime: null,