From c96092e9e86407c055d9e24761ec68aa7aa34ea9 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Tue, 5 May 2026 14:02:26 -0400 Subject: [PATCH] feat(ops/dispatch): right-click tech pin + click-on-map home picker + center map on HQ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three connected UX changes: 1. **Map centered on Gigafibre HQ on first load** — Sainte-Clotilde (lng=-73.6756, lat=45.1599), zoom 10 — covers the service area (Sainte-Clotilde + Châteauguay + Napierville + Hemmingford). Was downtown Montréal. 2. **Right-click on a tech pin** opens the existing techCtx menu (already used from the calendar via @ctx-tech). New entries: • 📍 Adresse de départ… → openTechHomeDialog • 🎯 Choisir sur la carte → startTechGeoFix (mirrors the existing geoFixJob flow used for jobs) 3. **The 📍 button in the GPS sidebar** now offers a 2-option chooser first: "Saisir une adresse" or "Cliquer sur la carte". Picking the map option drops the user into geoFixTech mode. Implementation: • useMap.js: new geoFixTech ref + startTechGeoFix/cancelTechGeoFix + a contextmenu listener on each tech outer wrapper that calls openTechCtx(e, tech). The map's main click handler now branches: if geoFixTech is set, persist the lng/lat via saveTechHome (passed in via deps as a forward-bound arrow because saveTechHome is destructured below the useMap call in DispatchPage). • DispatchPage.vue: new banner shown while in pick mode (animated indigo bar at top, "Cliquez sur la carte pour {tech}", with a cancel button); ESC also cancels. • dispatch-styles.scss: .sb-geofix-banner styles + reusing the existing pulse keyframe. --- apps/ops/src/composables/useMap.js | 53 ++++++++++++++++-- apps/ops/src/pages/DispatchPage.vue | 74 +++++++++++++++++++------ apps/ops/src/pages/dispatch-styles.scss | 11 ++++ 3 files changed, 114 insertions(+), 24 deletions(-) diff --git a/apps/ops/src/composables/useMap.js b/apps/ops/src/composables/useMap.js index be120df..417d2b6 100644 --- a/apps/ops/src/composables/useMap.js +++ b/apps/ops/src/composables/useMap.js @@ -8,7 +8,8 @@ export function useMap (deps) { currentView, periodStart, filteredResources, mapVisible, routeLegs, routeGeometry, getJobDate, jobColor, pushUndo, smartAssign, invalidateRoutes, - dragJob, dragIsAssist, rightPanel, openCtxMenu, + dragJob, dragIsAssist, rightPanel, openCtxMenu, openTechCtx, + saveTechHome, } = deps let map = null @@ -18,6 +19,7 @@ export function useMap (deps) { const mapMarkers = ref([]) const mapPanelW = ref(parseInt(localStorage.getItem('sbv2-mapW')) || 340) const geoFixJob = ref(null) + const geoFixTech = ref(null) // ← analog of geoFixJob, but for tech home base const mapDragJob = ref(null) let _mapGhost = null @@ -33,6 +35,20 @@ export function useMap (deps) { } watch(geoFixJob, v => { if (map) map.getCanvas().style.cursor = v ? 'crosshair' : '' }) + // Tech home-base "click on the map" picker. Same pattern as geoFixJob: + // user enters the mode, cursor turns to crosshair, next click on the + // map captures the lng/lat and persists it to ERPNext. + function startTechGeoFix (tech) { + geoFixTech.value = tech + if (!mapVisible.value) mapVisible.value = true + if (map) map.getCanvas().style.cursor = 'crosshair' + } + function cancelTechGeoFix () { + geoFixTech.value = null + if (map) map.getCanvas().style.cursor = '' + } + watch(geoFixTech, v => { if (map) map.getCanvas().style.cursor = v ? 'crosshair' : '' }) + // ── Panel resize ───────────────────────────────────────────────────────────── function startMapResize (e) { e.preventDefault() @@ -67,7 +83,10 @@ export function useMap (deps) { map = new mapboxgl.Map({ container: mapContainer.value, style: 'mapbox://styles/mapbox/dark-v11', - center: [-73.567, 45.502], zoom: 10, + // Default centered on Gigafibre HQ (1867 chemin de la Rivière, + // Sainte-Clotilde, QC). Zoom 10 shows Sainte-Clotilde + the + // surrounding service area (Châteauguay, Napierville, Hemmingford…). + center: [-73.6756177, 45.1599145], zoom: 10, }) if (mapResizeObs) mapResizeObs.disconnect() mapResizeObs = new ResizeObserver(() => { if (map) map.resize() }) @@ -112,8 +131,20 @@ export function useMap (deps) { map.on('mouseenter', 'sb-route-line', () => { if (mapDragJob.value) map.getCanvas().style.cursor = 'copy' }) map.on('mouseleave', 'sb-route-line', () => { if (!mapDragJob.value) map.getCanvas().style.cursor = '' }) - // Geo-fix click - map.on('click', e => { + // Geo-fix click — handles BOTH job geofix and tech home-base pick. + // Tech mode wins if both are somehow active simultaneously (defensive; + // shouldn't happen in practice). + map.on('click', async e => { + if (geoFixTech.value) { + const tech = geoFixTech.value + geoFixTech.value = null + map.getCanvas().style.cursor = '' + if (typeof saveTechHome === 'function') { + try { await saveTechHome(tech, e.lngLat.lng, e.lngLat.lat) } catch (_e) {} + } + nextTick(() => drawMapMarkers()) + return + } if (!geoFixJob.value) return const job = geoFixJob.value const saved = JSON.parse(localStorage.getItem('dispatch-job-coords') || '{}') @@ -255,6 +286,15 @@ export function useMap (deps) { el.appendChild(badge) } + // Right-click → open tech context menu (DispatchPage handles the + // q-menu). We pre-position by passing the original click event; + // useContextMenus reads e.clientX/Y to anchor the menu. + outer.addEventListener('contextmenu', e => { + e.preventDefault() + e.stopPropagation() + if (typeof openTechCtx === 'function') openTechCtx(e, tech) + }) + // Drag & drop handlers outer.addEventListener('dragover', e => { e.preventDefault(); el.style.transform = 'scale(1.25)' }) outer.addEventListener('dragleave', () => { el.style.transform = '' }) @@ -413,8 +453,9 @@ export function useMap (deps) { function getMap () { return map } return { - mapContainer, selectedTechId, mapMarkers, mapPanelW, geoFixJob, mapDragJob, - startGeoFix, cancelGeoFix, startMapResize, initMap, + mapContainer, selectedTechId, mapMarkers, mapPanelW, geoFixJob, geoFixTech, mapDragJob, + startGeoFix, cancelGeoFix, startTechGeoFix, cancelTechGeoFix, + startMapResize, initMap, drawMapMarkers, drawSelectedRoute, computeDayRoute, selectTechOnBoard, destroyMap, loadMapboxCss, getMap, } diff --git a/apps/ops/src/pages/DispatchPage.vue b/apps/ops/src/pages/DispatchPage.vue index e756995..1f3c4f9 100644 --- a/apps/ops/src/pages/DispatchPage.vue +++ b/apps/ops/src/pages/DispatchPage.vue @@ -318,10 +318,16 @@ const _map = useMap({ currentView, periodStart, filteredResources, mapVisible, routeLegs, routeGeometry, getJobDate, jobColor, pushUndo, smartAssign, invalidateRoutes, - dragJob, dragIsAssist, rightPanel, openCtxMenu, + dragJob, dragIsAssist, rightPanel, openCtxMenu, openTechCtx, + // Forward-binding through an arrow: `saveTechHome` is destructured + // from useTechManagement BELOW this call, so we can't pass the + // function value directly here (TDZ). The arrow defers the lookup + // to invocation time — by which point the const is defined. + saveTechHome: (tech, lng, lat) => saveTechHome(tech, lng, lat), }) -const { mapContainer, selectedTechId, mapMarkers, mapPanelW, geoFixJob, mapDragJob, - startGeoFix, cancelGeoFix, startMapResize, initMap, selectTechOnBoard, destroyMap, loadMapboxCss } = _map +const { mapContainer, selectedTechId, mapMarkers, mapPanelW, geoFixJob, geoFixTech, mapDragJob, + startGeoFix, cancelGeoFix, startTechGeoFix, cancelTechGeoFix, + startMapResize, initMap, selectTechOnBoard, destroyMap, loadMapboxCss } = _map computeDayRoute = _map.computeDayRoute drawMapMarkers = _map.drawMapMarkers drawSelectedRoute = _map.drawSelectedRoute @@ -411,27 +417,48 @@ 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: +// Edit a tech's home/departure coordinates. Three paths now converge: // • 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) +// • Type "lat, lng" directly (advanced) +// • Pick the location by clicking on the map (geoFixTech mode) // 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. +// involved. async function openTechHomeDialog (tech) { const cur = tech.coords || [-73.6756177, 45.1599145] - const dialog = $q.dialog({ + // Step 1: ask the user how they want to set it. + $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)} -
- `, + message: `
+ Actuelle: ${cur[1].toFixed(5)}, ${cur[0].toFixed(5)} +
+
Comment veux-tu la définir ?
`, + html: true, + options: { + type: 'radio', + model: 'address', + items: [ + { label: '📝 Saisir une adresse (ou "lat, lng")', value: 'address' }, + { label: '🎯 Cliquer sur la carte', value: 'map' }, + ], + }, + cancel: { flat: true, label: 'Annuler' }, + ok: { color: 'primary', unelevated: true, label: 'Continuer' }, + }).onOk(method => { + if (method === 'map') { + startTechGeoFix(tech) + Notify.create({ type: 'info', message: `Cliquez sur la carte pour ${tech.fullName}`, position: 'top', timeout: 3000 }) + return + } + // method === 'address' → ask for the address string + promptTechHomeAddress(tech) + }) +} + +async function promptTechHomeAddress (tech) { + const dialog = $q.dialog({ + title: `Adresse de départ — ${tech.fullName}`, + message: `
Adresse à géocoder, ou colle directement lat, lng.
`, html: true, prompt: { model: '', @@ -932,6 +959,7 @@ function onKeyDown (e) { dispatchCriteriaModal.value = false; bookingOverlay.value = null filterPanelOpen.value = false; projectsPanelOpen.value = false selectedJob.value = null; multiSelect.value = [] + if (geoFixTech.value) cancelTechGeoFix() } if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey) { if (['INPUT','TEXTAREA','SELECT'].includes(document.activeElement?.tagName)) return @@ -1555,9 +1583,19 @@ onUnmounted(() => {
+ + +
+ +
+ 🎯 Cliquez sur la carte pour définir le point de départ de + {{ geoFixTech.fullName }} + +
+