refactor(ops/dispatch): single-color Lucide icons + tech-first resource filter

Two issues conflated in the same PR because they touch the same pixels:

1. **Resource filter no longer treats techs and materials as equals.**
   Was a 3-button inline toggle [Tous][👤 45][🔧 6] with all three
   visually similar — and the wrench glyph clashed with the wrench
   used for the filter-settings button. Now:
     • Default = 'human' (techs only). Materials are secondary
       resources; they don't deserve front-of-bar real estate.
     • Single chip [👥 45 ▾] in the toolbar. Click → dropdown:
         · Techs (45)         ← active by default
         · Matériel (6)       (only shown if materialCount > 0)
         · Tous (51)          (only shown if materialCount > 0)
     • Defaults to localStorage 'sbv2-filterResType' if previously
       persisted, otherwise 'human' instead of '' (was '').

2. **Mixed-style icons (emoji + Lucide SVG) replaced with consistent
   single-color Lucide-style strokes.**
   Each is a stroke-only inline <svg> with stroke="currentColor", so
   they inherit the surrounding text color (no green/red/yellow
   tinting). Added to the existing ICON set in useHelpers.js:
     user, users, package, sliders, chevDown, map, clipboard,
     sparkles, signal, rotateCw, alertTri, moreH, pause, play,
     externalLink, target, calendar
   Replaced in the dispatch top toolbar:
     ⚠️  → ICON.alertTri          (overload alert)
     📋  → ICON.clipboard         (unscheduled jobs)
     🗺  → ICON.map               (map toggle)
     🗓  → ICON.calendar          (planning toggle)
     👥  → ICON.users             (team-jobs button + Ressources menu)
     🔧  → ICON.sliders           (filter-settings — was wrench, which
                                   collided with the materials filter)
     👤/🔧 → ICON.users / .package (resource type dropdown)
     ↻  → ICON.rotateCw           (refresh in ⋯ menu)
       → ICON.sparkles           (AI in ⋯ menu)
     📡  → ICON.signal            (offers in ⋯ menu)
     ↗  → ICON.externalLink       (ERPNext link in ⋯ menu)
     ⋯  → ICON.moreH              (the ⋯ button itself)
   .sb-icon-svg gives them consistent sizing (14px in buttons, 15px
   in dropdown items, 16px in the ⋯ trigger) so they're crisp at all
   the spots they appear.

Emojis still in place elsewhere (job-tile chips, status badges, etc.)
will be migrated incrementally — out of scope for this pass which
only targeted the user's visible header.
This commit is contained in:
louispaulb 2026-05-05 14:31:00 -04:00
parent 16343b61e1
commit 66b358d568
4 changed files with 106 additions and 22 deletions

View File

@ -212,6 +212,26 @@ export const ICON = {
clock: _s('<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>'), clock: _s('<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>'),
loader: _s('<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>'), loader: _s('<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>'),
truck: _s('<path d="M5 17h2l3-6h4l3 6h2M7 17a2 2 0 1 1-4 0M21 17a2 2 0 1 1-4 0"/>'), truck: _s('<path d="M5 17h2l3-6h4l3 6h2M7 17a2 2 0 1 1-4 0M21 17a2 2 0 1 1-4 0"/>'),
// ── Single-color toolbar set (used in dispatch top bar). All inherit
// currentColor; CSS sizes them via .sb-icon-svg below. Strokes only,
// 2px stroke for crisp rendering at 14-16px display size.
user: _s('<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>'),
users: _s('<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>'),
package: _s('<path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>'),
sliders: _s('<line x1="21" x2="14" y1="4" y2="4"/><line x1="10" x2="3" y1="4" y2="4"/><line x1="21" x2="12" y1="12" y2="12"/><line x1="8" x2="3" y1="12" y2="12"/><line x1="21" x2="16" y1="20" y2="20"/><line x1="12" x2="3" y1="20" y2="20"/><line x1="14" x2="14" y1="2" y2="6"/><line x1="8" x2="8" y1="10" y2="14"/><line x1="16" x2="16" y1="18" y2="22"/>'),
chevDown: _s('<path d="m6 9 6 6 6-6"/>'),
map: _s('<path d="M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"/><path d="M15 5.764v15"/><path d="M9 3.236v15"/>'),
clipboard:_s('<rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>'),
sparkles: _s('<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>'),
signal: _s('<path d="M2 20h.01"/><path d="M7 20v-4"/><path d="M12 20v-8"/><path d="M17 20V8"/><path d="M22 4v16"/>'),
rotateCw: _s('<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/>'),
alertTri: _s('<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>'),
moreH: _s('<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>'),
pause: _s('<rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/>'),
play: _s('<polygon points="6 3 20 12 6 21 6 3"/>'),
externalLink: _s('<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>'),
target: _s('<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>'),
calendar: _s('<rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/>'),
} }
// Job type icon based on service/subject // Job type icon based on service/subject

View File

@ -5,7 +5,10 @@ export function useResourceFilter (store, opts = {}) {
const filterStatus = ref(localStorage.getItem('sbv2-filterStatus') || '') const filterStatus = ref(localStorage.getItem('sbv2-filterStatus') || '')
const filterGroup = ref(localStorage.getItem('sbv2-filterGroup') || '') const filterGroup = ref(localStorage.getItem('sbv2-filterGroup') || '')
const filterTags = ref(JSON.parse(localStorage.getItem('sbv2-filterTags') || '[]')) const filterTags = ref(JSON.parse(localStorage.getItem('sbv2-filterTags') || '[]'))
const filterResourceType = ref(localStorage.getItem('sbv2-filterResType') || '') // '' | 'human' | 'material' // Default 'human' = show only techs. Materials are secondary and
// accessed via the resource-type dropdown when needed. Once the user
// explicitly picks something else (incl. ''), it's persisted.
const filterResourceType = ref(localStorage.getItem('sbv2-filterResType') ?? 'human') // '' | 'human' | 'material'
const searchQuery = ref('') const searchQuery = ref('')
const techSort = ref(localStorage.getItem('sbv2-techSort') || 'default') const techSort = ref(localStorage.getItem('sbv2-techSort') || 'default')
const manualOrder = ref(JSON.parse(localStorage.getItem('sbv2-techOrder') || '[]')) const manualOrder = ref(JSON.parse(localStorage.getItem('sbv2-techOrder') || '[]'))

View File

@ -403,6 +403,22 @@ const periodEndStr = computed(() => {
const onPublished = jobNames => store.publishJobsLocal(jobNames) const onPublished = jobNames => store.publishJobsLocal(jobNames)
const moreMenuOpen = ref(false) // dropdown in the top toolbar (right side) const moreMenuOpen = ref(false) // dropdown in the top toolbar (right side)
const viewsMenuOpen = ref(false) // "Vue principale " dropdown (left side) const viewsMenuOpen = ref(false) // "Vue principale " dropdown (left side)
const resTypeMenuOpen = ref(false) // Resource type chip dropdown ([👤 45 ])
const resTypeIcon = computed(() =>
filterResourceType.value === 'material' ? ICON.package
: filterResourceType.value === 'human' ? ICON.users
: ICON.users // 'Tous' falls back to the people icon
)
const resTypeLabel = computed(() =>
filterResourceType.value === 'material' ? 'Matériel'
: filterResourceType.value === 'human' ? 'Techs'
: 'Toutes les ressources'
)
const resTypeCount = computed(() =>
filterResourceType.value === 'material' ? materialCount.value
: filterResourceType.value === 'human' ? humanCount.value
: (humanCount.value + materialCount.value)
)
const gpsSettingsOpen = ref(false) const gpsSettingsOpen = ref(false)
const gpsShowInactive = ref(false) const gpsShowInactive = ref(false)
const gpsFilteredTechs = computed(() => const gpsFilteredTechs = computed(() =>
@ -968,6 +984,7 @@ function onKeyDown (e) {
selectedJob.value = null; multiSelect.value = [] selectedJob.value = null; multiSelect.value = []
moreMenuOpen.value = false moreMenuOpen.value = false
viewsMenuOpen.value = false viewsMenuOpen.value = false
resTypeMenuOpen.value = false
if (geoFixTech.value) cancelTechGeoFix() if (geoFixTech.value) cancelTechGeoFix()
} }
if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey) { if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
@ -1181,7 +1198,7 @@ onMounted(async () => {
loadOffers() loadOffers()
loadPresets() loadPresets()
document.addEventListener('keydown', onKeyDown) document.addEventListener('keydown', onKeyDown)
document.addEventListener('click', () => { closeCtxMenu(); assistCtx.value = null; techCtx.value = null; moreMenuOpen.value = false; viewsMenuOpen.value = false }) document.addEventListener('click', () => { closeCtxMenu(); assistCtx.value = null; techCtx.value = null; moreMenuOpen.value = false; viewsMenuOpen.value = false; resTypeMenuOpen.value = false })
if (!document.getElementById('mapbox-css')) { if (!document.getElementById('mapbox-css')) {
const l = document.createElement('link'); l.id='mapbox-css'; l.rel='stylesheet' 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) l.href='https://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css'; document.head.appendChild(l)
@ -1227,10 +1244,33 @@ onUnmounted(() => {
<span v-if="p.type === 'group'" class="sb-qp-icon">👥</span>{{ p.name }} <span v-if="p.type === 'group'" class="sb-qp-icon">👥</span>{{ p.name }}
</button> </button>
</div> </div>
<div v-if="materialCount > 0" class="sb-res-type-toggle"> <!-- Resource type single chip with Lucide icon + count + chevron.
<button :class="{ active: !filterResourceType }" @click="filterResourceType=''">Tous <span class="sbf-count">{{ humanCount + materialCount }}</span></button> Default is 'human' (techs are the primary thing); materials
<button :class="{ active: filterResourceType==='human' }" @click="filterResourceType='human'">👤 <span class="sbf-count">{{ humanCount }}</span></button> are accessed via the dropdown only when needed (and aren't
<button :class="{ active: filterResourceType==='material' }" @click="filterResourceType='material'">🔧 <span class="sbf-count">{{ materialCount }}</span></button> even shown if the customer doesn't have any). -->
<div class="sb-menu-wrap">
<button class="sb-icon-btn sb-tab-current" :class="{ active: resTypeMenuOpen }"
@click.stop="resTypeMenuOpen = !resTypeMenuOpen"
:title="resTypeLabel">
<span class="sb-icon-svg" v-html="resTypeIcon"></span>
<span class="sbf-count">{{ resTypeCount }}</span>
<span class="sb-tab-chev" v-html="ICON.chevDown"></span>
</button>
<div v-if="resTypeMenuOpen" class="sb-menu-dropdown sb-menu-dropdown-left" @click.stop>
<button class="sb-menu-item" :class="{ active: filterResourceType === 'human' }" @click="filterResourceType='human'; resTypeMenuOpen=false">
<span class="sb-icon-svg" v-html="ICON.users"></span> Techs
<span class="sbs-count" style="margin-left:auto">{{ humanCount }}</span>
</button>
<button v-if="materialCount > 0" class="sb-menu-item" :class="{ active: filterResourceType === 'material' }" @click="filterResourceType='material'; resTypeMenuOpen=false">
<span class="sb-icon-svg" v-html="ICON.package"></span> Matériel
<span class="sbs-count" style="margin-left:auto">{{ materialCount }}</span>
</button>
<div v-if="materialCount > 0" class="sb-menu-sep"></div>
<button v-if="materialCount > 0" class="sb-menu-item" :class="{ active: !filterResourceType }" @click="filterResourceType=''; resTypeMenuOpen=false">
Tous
<span class="sbs-count" style="margin-left:auto">{{ humanCount + materialCount }}</span>
</button>
</div>
</div> </div>
<!-- Board view selector a single dropdown instead of all tabs <!-- Board view selector a single dropdown instead of all tabs
inline. Saves header width on narrow laptops; the chevron inline. Saves header width on narrow laptops; the chevron
@ -1250,12 +1290,16 @@ onUnmounted(() => {
<button class="sb-menu-item" disabled title="Bientôt"><span class="sb-menu-icon">+</span> Nouvelle vue</button> <button class="sb-menu-item" disabled title="Bientôt"><span class="sb-menu-icon">+</span> Nouvelle vue</button>
</div> </div>
</div> </div>
<button class="sb-icon-btn" :class="{ active: filterPanelOpen }" @click="filterPanelOpen=!filterPanelOpen" title="Filtres & Ressources"> <button class="sb-icon-btn" :class="{ active: filterPanelOpen }" @click="filterPanelOpen=!filterPanelOpen" title="Filtres &amp; Ressources">
<span v-html="ICON.wrench"></span> <!-- Settings/filter icon (sliders, not wrench) `wrench` is now
reserved for the materials filter to avoid the visual clash
where two distinct concerns shared the same wrench glyph. -->
<span class="sb-icon-svg" v-html="ICON.sliders"></span>
<span v-if="filterStatus||filterGroup||selectedResIds.length||filterTags.length||hideAbsent" class="sbs-badge" style="position:relative;top:-4px;right:2px"></span> <span v-if="filterStatus||filterGroup||selectedResIds.length||filterTags.length||hideAbsent" class="sbs-badge" style="position:relative;top:-4px;right:2px"></span>
</button> </button>
<button v-if="teamJobs.length" class="sb-icon-btn" :class="{ active: projectsPanelOpen }" @click="projectsPanelOpen=!projectsPanelOpen" title="Projets"> <button v-if="teamJobs.length" class="sb-icon-btn" :class="{ active: projectsPanelOpen }" @click="projectsPanelOpen=!projectsPanelOpen" title="Projets">
👥 <span class="sbs-count" style="position:relative;top:-2px;right:auto">{{ teamJobs.length }}</span> <span class="sb-icon-svg" v-html="ICON.users"></span>
<span class="sbs-count" style="position:relative;top:-2px;right:auto">{{ teamJobs.length }}</span>
</button> </button>
</div> </div>
<div class="sb-header-center"> <div class="sb-header-center">
@ -1267,7 +1311,8 @@ onUnmounted(() => {
<button v-for="v in [['day','Jour'],['week','Semaine'],['month','Mois']]" :key="v[0]" :class="{ active: currentView===v[0] }" @click="currentView=v[0]">{{ v[1] }}</button> <button v-for="v in [['day','Jour'],['week','Semaine'],['month','Mois']]" :key="v[0]" :class="{ active: currentView===v[0] }" @click="currentView=v[0]">{{ v[1] }}</button>
</div> </div>
<button class="sb-icon-btn sb-planning-toggle" :class="{ active: planningMode }" @click="planningMode=!planningMode" title="Mode planification — afficher les disponibilités"> <button class="sb-icon-btn sb-planning-toggle" :class="{ active: planningMode }" @click="planningMode=!planningMode" title="Mode planification — afficher les disponibilités">
🗓 <span v-if="currentView!=='month'" style="font-size:0.72rem">Planning</span> <span class="sb-icon-svg" v-html="ICON.calendar"></span>
<span v-if="currentView!=='month'" style="font-size:0.72rem">Planning</span>
</button> </button>
</div> </div>
<div class="sb-header-right"> <div class="sb-header-right">
@ -1276,12 +1321,17 @@ onUnmounted(() => {
(badges, surcharge) ou qui sont des CTA principaux (Publier, + WO). (badges, surcharge) ou qui sont des CTA principaux (Publier, + WO).
Le reste descend dans le menu pour libérer la largeur. --> Le reste descend dans le menu pour libérer la largeur. -->
<span v-if="overloadedTechs.length" class="sb-overload-alert" :title="overloadedTechs.map(o => o.tech.fullName + ' ' + o.pct + '%').join(', ')"> <span v-if="overloadedTechs.length" class="sb-overload-alert" :title="overloadedTechs.map(o => o.tech.fullName + ' ' + o.pct + '%').join(', ')">
{{ overloadedTechs.length }} surchargé{{ overloadedTechs.length > 1 ? 's' : '' }} <span class="sb-icon-svg" v-html="ICON.alertTri"></span>
{{ overloadedTechs.length }} surchargé{{ overloadedTechs.length > 1 ? 's' : '' }}
</span> </span>
<button class="sb-icon-btn" :class="{ active: bottomPanelOpen }" @click="bottomPanelOpen=!bottomPanelOpen" title="Jobs non assignées"> <button class="sb-icon-btn" :class="{ active: bottomPanelOpen }" @click="bottomPanelOpen=!bottomPanelOpen" title="Jobs non assignées">
📋 <span v-if="unscheduledJobs.length" class="sbs-count" style="position:relative;top:-2px;right:auto">{{ unscheduledJobs.length }}</span> <span class="sb-icon-svg" v-html="ICON.clipboard"></span>
<span v-if="unscheduledJobs.length" class="sbs-count" style="position:relative;top:-2px;right:auto">{{ unscheduledJobs.length }}</span>
</button>
<button v-if="currentView==='day'" class="sb-icon-btn" :class="{ active: mapVisible }" @click="mapVisible=!mapVisible" title="Carte">
<span class="sb-icon-svg" v-html="ICON.map"></span>
<span>Carte</span>
</button> </button>
<button v-if="currentView==='day'" class="sb-icon-btn" :class="{ active: mapVisible }" @click="mapVisible=!mapVisible" title="Carte">🗺 Carte</button>
<button class="sb-wo-btn" style="background:#7c3aed" @click="publishModalOpen=true" title="Publier & envoyer l'horaire"> <button class="sb-wo-btn" style="background:#7c3aed" @click="publishModalOpen=true" title="Publier & envoyer l'horaire">
Publier <span v-if="draftCount" class="sbs-count" style="position:relative;top:-2px;right:auto;background:#ef4444">{{ draftCount }}</span> Publier <span v-if="draftCount" class="sbs-count" style="position:relative;top:-2px;right:auto;background:#ef4444">{{ draftCount }}</span>
</button> </button>
@ -1292,25 +1342,27 @@ onUnmounted(() => {
ressources/GPS, lien ERPNext. Tout ce qui n'est pas dans le hot ressources/GPS, lien ERPNext. Tout ce qui n'est pas dans le hot
path du dispatcher au quotidien. --> path du dispatcher au quotidien. -->
<div class="sb-menu-wrap"> <div class="sb-menu-wrap">
<button class="sb-icon-btn sb-menu-btn" :class="{ active: moreMenuOpen }" @click.stop="moreMenuOpen = !moreMenuOpen" title="Plus d'options"></button> <button class="sb-icon-btn sb-menu-btn" :class="{ active: moreMenuOpen }" @click.stop="moreMenuOpen = !moreMenuOpen" title="Plus d'options">
<span class="sb-icon-svg" v-html="ICON.moreH"></span>
</button>
<div v-if="moreMenuOpen" class="sb-menu-dropdown" @click.stop> <div v-if="moreMenuOpen" class="sb-menu-dropdown" @click.stop>
<button class="sb-menu-item" @click="refreshData(); moreMenuOpen=false"> <button class="sb-menu-item" @click="refreshData(); moreMenuOpen=false">
<span class="sb-menu-icon"></span> Actualiser <span class="sb-icon-svg" v-html="ICON.rotateCw"></span> Actualiser
</button> </button>
<button class="sb-menu-item" :class="{ active: nlpVisible }" @click="nlpVisible=!nlpVisible; moreMenuOpen=false"> <button class="sb-menu-item" :class="{ active: nlpVisible }" @click="nlpVisible=!nlpVisible; moreMenuOpen=false">
<span class="sb-menu-icon"></span> Assistant IA <span class="sb-icon-svg" v-html="ICON.sparkles"></span> Assistant IA
</button> </button>
<button class="sb-menu-item" :class="{ active: showOfferPool }" @click="showOfferPool=!showOfferPool; if(showOfferPool) loadOffers(); moreMenuOpen=false"> <button class="sb-menu-item" :class="{ active: showOfferPool }" @click="showOfferPool=!showOfferPool; if(showOfferPool) loadOffers(); moreMenuOpen=false">
<span class="sb-menu-icon">📡</span> Offres aux techs <span class="sb-icon-svg" v-html="ICON.signal"></span> Offres aux techs
<span v-if="activeOfferCount" class="sbs-count" style="margin-left:auto;background:#4ade80;color:#000">{{ activeOfferCount }}</span> <span v-if="activeOfferCount" class="sbs-count" style="margin-left:auto;background:#4ade80;color:#000">{{ activeOfferCount }}</span>
</button> </button>
<div class="sb-menu-sep"></div> <div class="sb-menu-sep"></div>
<button class="sb-menu-item" @click="gpsSettingsOpen=true; moreMenuOpen=false"> <button class="sb-menu-item" @click="gpsSettingsOpen=true; moreMenuOpen=false">
<span class="sb-menu-icon">👥</span> Ressources &amp; GPS <span class="sb-icon-svg" v-html="ICON.users"></span> Ressources &amp; GPS
</button> </button>
<div class="sb-menu-sep"></div> <div class="sb-menu-sep"></div>
<a class="sb-menu-item" :href="erpUrl + '/desk'" target="_blank" @click="moreMenuOpen=false"> <a class="sb-menu-item" :href="erpUrl + '/desk'" target="_blank" @click="moreMenuOpen=false">
<span class="sb-menu-icon"></span> Ouvrir ERPNext <span class="sb-icon-svg" v-html="ICON.externalLink"></span> Ouvrir ERPNext
<span class="sb-erp-dot" :class="{ ok: store.erpStatus==='ok' }" style="margin-left:auto" :title="{ ok:'ERPNext ✓', error:'Hors ligne', loading:'Connexion…' }[store.erpStatus]||'ERPNext'"></span> <span class="sb-erp-dot" :class="{ ok: store.erpStatus==='ok' }" style="margin-left:auto" :title="{ ok:'ERPNext ✓', error:'Hors ligne', loading:'Connexion…' }[store.erpStatus]||'ERPNext'"></span>
</a> </a>
</div> </div>

View File

@ -105,7 +105,16 @@
.sb-menu-dropdown-left { right:auto; left:0; min-width:180px; } .sb-menu-dropdown-left { right:auto; left:0; min-width:180px; }
/* Current-view button: shows the active tab name + a chevron. */ /* Current-view button: shows the active tab name + a chevron. */
.sb-tab-current { display:inline-flex; align-items:center; gap:6px; } .sb-tab-current { display:inline-flex; align-items:center; gap:6px; }
.sb-tab-chev { font-size:0.6rem; opacity:0.65; } .sb-tab-chev { font-size:0.6rem; opacity:0.65; display:inline-flex; align-items:center; }
.sb-tab-chev svg { width:10px; height:10px; }
/* Single-color Lucide-style icon container sizes the inline SVG and
keeps it baseline-aligned with neighboring text. Uses currentColor
on stroke so the icon inherits whatever color the parent applies. */
.sb-icon-svg { display:inline-flex; align-items:center; line-height:0; }
.sb-icon-svg svg { width:14px; height:14px; flex-shrink:0; }
.sb-menu-item .sb-icon-svg svg { width:15px; height:15px; opacity:0.85; }
.sb-icon-btn .sb-icon-svg svg { width:13px; height:13px; }
.sb-menu-btn .sb-icon-svg svg { width:16px; height:16px; }
@keyframes sb-menu-fade { from { opacity:0; transform:translateY(-4px); } to { opacity:1; transform:translateY(0); } } @keyframes sb-menu-fade { from { opacity:0; transform:translateY(-4px); } to { opacity:1; transform:translateY(0); } }
.sb-menu-item { .sb-menu-item {
display:flex; align-items:center; gap:10px; width:100%; display:flex; align-items:center; gap:10px; width:100%;