fix(ops/dispatch): top bar polish — visible ⋯ menu, collapsed AI, fly-to tech, views dropdown

Four fixes around the dispatch header following dispatcher feedback:

1. **⋯ overflow menu was invisible**: .sb-header had `overflow:hidden`,
   which clipped the absolutely-positioned dropdown right at the
   header's bottom edge. Switched the header to `overflow:visible`
   (children all have flex-shrink:0 + a flex:1 center, so the layout
   doesn't actually overflow horizontally). Bumped z-index to 5000
   for safety on top of map/calendar layers.

2. **NLP/Assistant IA bar hidden by default**: was eagerly rendering
   on every page load, with the long French placeholder polluting
   the header below the toolbar. The user just wanted the icon. Now
   `nlpVisible` defaults to false, persisted in localStorage so power
   users who flip it on keep it open across sessions. Toggle still
   lives in the ⋯ menu.

3. **Click a tech in the resource list now flies the map to them**:
   selectTechOnBoard previously only opened the map panel. Now it
   also `map.flyTo({ center })` using `tech.gpsCoords ?? tech.coords`
   — live Traccar position wins when the tech is online; falls back
   to the saved home base. Animated, deferred a tick so map.resize()
   happens first, otherwise flyTo can land on garbage coords during
   the panel's open transition.

4. **Board view tabs collapsed into a "Vue principale ▾" dropdown**:
   was [Vue principale][Par région][+] inline. Now a single button
   showing the active view; click reveals the others + the future
   "+ Nouvelle vue" entry. Same dropdown component as the ⋯ menu
   (shared CSS, click-outside + ESC close).
This commit is contained in:
louispaulb 2026-05-05 14:17:33 -04:00
parent 96a84c3e48
commit 16343b61e1
3 changed files with 54 additions and 9 deletions

View File

@ -411,6 +411,19 @@ export function useMap (deps) {
localStorage.setItem('sbv2-mapW', String(mapPanelW.value))
mapVisible.value = true
}
// Live GPS wins over the ERPNext-saved home base, so the rep
// sees where the tech actually IS right now if Traccar reports
// them online. Fall back to home coords when offline. We
// flyTo (animated pan+zoom) so the dispatcher gets a clear
// visual cue, instead of a hard jump.
const pos = tech.gpsCoords || tech.coords
if (pos && map && Number.isFinite(pos[0]) && Number.isFinite(pos[1])) {
// Defer one tick so the map panel has time to be visible
// and `map.resize()` has run before the camera animation.
nextTick(() => {
try { map.flyTo({ center: pos, zoom: Math.max(map.getZoom(), 12), speed: 1.2, essential: true }) } catch (_e) {}
})
}
}
if (map) { drawMapMarkers(); drawSelectedRoute() }
}

View File

@ -372,7 +372,12 @@ const bookingOverlay = ref(null)
const woModalOpen = ref(false)
const woModalCtx = ref({})
const publishModalOpen = ref(false)
const nlpVisible = ref(true) // NLP bar always visible
// NLP bar is hidden by default; toggled from the menu (Assistant IA).
// Showing it eagerly bloats the header on narrow laptops and the
// example placeholder text added visual noise. Persist the user's
// preference in localStorage so power users keep it open if they want.
const nlpVisible = ref(localStorage.getItem('sbv2-nlp-visible') === '1')
watch(nlpVisible, v => localStorage.setItem('sbv2-nlp-visible', v ? '1' : '0'))
const draftCount = computed(() => store.jobs.filter(j => !j.published && j.status !== 'completed' && j.status !== 'cancelled').length)
function onNlpAction (result) {
@ -397,6 +402,7 @@ const periodEndStr = computed(() => {
})
const onPublished = jobNames => store.publishJobsLocal(jobNames)
const moreMenuOpen = ref(false) // dropdown in the top toolbar (right side)
const viewsMenuOpen = ref(false) // "Vue principale " dropdown (left side)
const gpsSettingsOpen = ref(false)
const gpsShowInactive = ref(false)
const gpsFilteredTechs = computed(() =>
@ -961,6 +967,7 @@ function onKeyDown (e) {
filterPanelOpen.value = false; projectsPanelOpen.value = false
selectedJob.value = null; multiSelect.value = []
moreMenuOpen.value = false
viewsMenuOpen.value = false
if (geoFixTech.value) cancelTechGeoFix()
}
if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
@ -1174,7 +1181,7 @@ onMounted(async () => {
loadOffers()
loadPresets()
document.addEventListener('keydown', onKeyDown)
document.addEventListener('click', () => { closeCtxMenu(); assistCtx.value = null; techCtx.value = null; moreMenuOpen.value = false })
document.addEventListener('click', () => { closeCtxMenu(); assistCtx.value = null; techCtx.value = null; moreMenuOpen.value = false; viewsMenuOpen.value = false })
if (!document.getElementById('mapbox-css')) {
const l = document.createElement('link'); l.id='mapbox-css'; l.rel='stylesheet'
l.href='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css'; document.head.appendChild(l)
@ -1225,9 +1232,23 @@ onUnmounted(() => {
<button :class="{ active: filterResourceType==='human' }" @click="filterResourceType='human'">👤 <span class="sbf-count">{{ humanCount }}</span></button>
<button :class="{ active: filterResourceType==='material' }" @click="filterResourceType='material'">🔧 <span class="sbf-count">{{ materialCount }}</span></button>
</div>
<div class="sb-tabs">
<button v-for="tab in boardTabs" :key="tab" class="sb-tab" :class="{ active: activeTab===tab }" @click="activeTab=tab">{{ tab }}</button>
<button class="sb-tab sb-tab-add" title="Nouvelle vue">+</button>
<!-- Board view selector a single dropdown instead of all tabs
inline. Saves header width on narrow laptops; the chevron
hints there are more views available. -->
<div class="sb-menu-wrap">
<button class="sb-icon-btn sb-tab-current" :class="{ active: viewsMenuOpen }"
@click.stop="viewsMenuOpen = !viewsMenuOpen" title="Changer de vue">
{{ activeTab }} <span class="sb-tab-chev"></span>
</button>
<div v-if="viewsMenuOpen" class="sb-menu-dropdown sb-menu-dropdown-left" @click.stop>
<button v-for="tab in boardTabs" :key="tab" class="sb-menu-item"
:class="{ active: activeTab===tab }"
@click="activeTab=tab; viewsMenuOpen=false">
{{ tab }}
</button>
<div class="sb-menu-sep"></div>
<button class="sb-menu-item" disabled title="Bientôt"><span class="sb-menu-icon">+</span> Nouvelle vue</button>
</div>
</div>
<button class="sb-icon-btn" :class="{ active: filterPanelOpen }" @click="filterPanelOpen=!filterPanelOpen" title="Filtres & Ressources">
<span v-html="ICON.wrench"></span>

View File

@ -13,7 +13,10 @@
}
/* ── Header ── */
.sb-header { display:flex; align-items:center; gap:0.5rem; padding:0 0.75rem; background:var(--sb-sidebar); border-bottom:1px solid var(--sb-border); box-shadow:0 1px 4px rgba(0,0,0,0.06); z-index:30; overflow:hidden; }
/* `overflow:visible` (was hidden) so dropdowns rooted in the header
can render below it. Children all have flex-shrink:0 with a flex:1
center, so the layout doesn't visually overflow horizontally. */
.sb-header { display:flex; align-items:center; gap:0.5rem; padding:0 0.75rem; background:var(--sb-sidebar); border-bottom:1px solid var(--sb-border); box-shadow:0 1px 4px rgba(0,0,0,0.06); z-index:30; overflow:visible; }
.sb-header-left { display:flex; align-items:center; gap:0.4rem; flex-shrink:0; }
.sb-header-center { display:flex; align-items:center; gap:0.35rem; flex:1; justify-content:center; }
.sb-header-right { display:flex; align-items:center; gap:0.35rem; flex-shrink:0; margin-left:auto; }
@ -86,15 +89,23 @@
.sb-erp-dot { width:7px; height:7px; border-radius:50%; background:var(--sb-red); transition:background 0.3s; }
.sb-erp-dot.ok { background:var(--sb-green); }
/* ⋯ overflow menu in the top toolbar — secondary/admin actions live here. */
/* overflow menu in the top toolbar secondary/admin actions live here.
Uses position:absolute now that .sb-header is overflow:visible.
Right-side variant (default) anchors to the right of the wrapper;
left-side variant (sb-menu-dropdown-left) anchors to the left edge. */
.sb-menu-wrap { position:relative; display:inline-block; }
.sb-menu-btn { font-size:0.95rem; line-height:0.85rem; padding:0.05rem 0.5rem 0.18rem; }
.sb-menu-dropdown {
position:absolute; top:calc(100% + 4px); right:0; z-index:200;
position:absolute; top:calc(100% + 6px); right:0; z-index:5000;
background:var(--sb-card); border:1px solid var(--sb-border);
border-radius:6px; box-shadow:0 6px 22px rgba(0,0,0,0.35);
min-width:220px; padding:4px 0; animation:sb-menu-fade 0.12s ease-out;
min-width:230px; padding:4px 0; animation:sb-menu-fade 0.12s ease-out;
}
/* Left-anchored variant for the views dropdown ("Vue principale ▾") */
.sb-menu-dropdown-left { right:auto; left:0; min-width:180px; }
/* Current-view button: shows the active tab name + a chevron. */
.sb-tab-current { display:inline-flex; align-items:center; gap:6px; }
.sb-tab-chev { font-size:0.6rem; opacity:0.65; }
@keyframes sb-menu-fade { from { opacity:0; transform:translateY(-4px); } to { opacity:1; transform:translateY(0); } }
.sb-menu-item {
display:flex; align-items:center; gap:10px; width:100%;