feat: map markers — workload progress bar + crew group badge
- Progress bar under each tech avatar showing daily load (0-8h) Green (<4h) → Yellow (4-6h) → Orange (6-8h) → Red (8h+) Includes both primary queue and assistant jobs - Crew badge: purple circle with count (e.g. "2") when tech has assistants on today's jobs — indicates grouped team - Tooltip shows "Name — X.Xh / 8h" - Marker now uses gpsCoords || coords for visibility check (fixes techs with GPS but no static coords) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
af42c6082e
commit
15813e6caf
|
|
@ -165,18 +165,65 @@ export function useMap (deps) {
|
|||
// Tech avatar markers
|
||||
mapMarkers.value.forEach(m => m.remove())
|
||||
mapMarkers.value = []
|
||||
|
||||
// Pre-compute: which techs are assistants on which lead tech's jobs today
|
||||
const groupCounts = {} // leadTechId → total crew size (1 + assistants)
|
||||
store.technicians.forEach(tech => {
|
||||
const todayJobs = tech.queue.filter(j => jobSpansDate(j, dayStr))
|
||||
const assistIds = new Set()
|
||||
todayJobs.forEach(j => (j.assistants || []).forEach(a => assistIds.add(a.techId)))
|
||||
if (assistIds.size > 0) groupCounts[tech.id] = 1 + assistIds.size
|
||||
})
|
||||
|
||||
filteredResources.value.forEach(tech => {
|
||||
if (!tech.coords || (tech.coords[0] === 0 && tech.coords[1] === 0)) return
|
||||
const pos = tech.gpsCoords || tech.coords
|
||||
if (!pos || (pos[0] === 0 && pos[1] === 0)) return
|
||||
const initials = tech.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||
const color = TECH_COLORS[tech.colorIdx]
|
||||
|
||||
// Calculate daily workload (hours)
|
||||
const todayJobs = tech.queue.filter(j => jobSpansDate(j, dayStr))
|
||||
const todayAssist = (tech.assistJobs || []).filter(j => jobSpansDate(j, dayStr))
|
||||
const totalHours = todayJobs.reduce((s, j) => s + (j.duration || 1), 0)
|
||||
+ todayAssist.reduce((s, j) => s + (j.duration || 1), 0)
|
||||
const loadPct = Math.min(totalHours / 8, 1) // 0..1, capped at 8h
|
||||
const barColor = loadPct < 0.5 ? '#10b981' : loadPct < 0.75 ? '#f59e0b' : loadPct < 0.9 ? '#f97316' : '#ef4444'
|
||||
|
||||
// Outer wrapper
|
||||
const outer = document.createElement('div')
|
||||
outer.style.cssText = 'cursor:pointer;'
|
||||
outer.style.cssText = 'cursor:pointer;display:flex;flex-direction:column;align-items:center;'
|
||||
outer.dataset.techId = tech.id
|
||||
|
||||
// Avatar circle
|
||||
const el = document.createElement('div')
|
||||
el.className = 'sb-map-tech-pin'
|
||||
el.style.cssText = `background:${color};border-color:${color};`
|
||||
el.textContent = initials; el.title = tech.fullName
|
||||
el.textContent = initials
|
||||
el.title = `${tech.fullName} — ${totalHours.toFixed(1)}h / 8h`
|
||||
outer.appendChild(el)
|
||||
|
||||
// Group badge (crew size)
|
||||
const crew = groupCounts[tech.id]
|
||||
if (crew && crew > 1) {
|
||||
const badge = document.createElement('div')
|
||||
badge.className = 'sb-map-crew-badge'
|
||||
badge.textContent = String(crew)
|
||||
badge.title = `Équipe de ${crew}`
|
||||
el.appendChild(badge)
|
||||
}
|
||||
|
||||
// Workload progress bar (under avatar)
|
||||
if (totalHours > 0) {
|
||||
const bar = document.createElement('div')
|
||||
bar.className = 'sb-map-load-bar'
|
||||
const fill = document.createElement('div')
|
||||
fill.className = 'sb-map-load-fill'
|
||||
fill.style.cssText = `width:${Math.round(loadPct * 100)}%;background:${barColor};`
|
||||
bar.appendChild(fill)
|
||||
outer.appendChild(bar)
|
||||
}
|
||||
|
||||
// Drag & drop handlers
|
||||
outer.addEventListener('dragover', e => { e.preventDefault(); el.style.transform = 'scale(1.25)' })
|
||||
outer.addEventListener('dragleave', () => { el.style.transform = '' })
|
||||
outer.addEventListener('drop', e => {
|
||||
|
|
@ -191,11 +238,10 @@ export function useMap (deps) {
|
|||
})
|
||||
outer.addEventListener('mouseenter', () => { if (mapDragJob.value) el.style.transform = 'scale(1.3)' })
|
||||
outer.addEventListener('mouseleave', () => { el.style.transform = '' })
|
||||
// Use GPS position if available, else static coords
|
||||
const pos = tech.gpsCoords || tech.coords
|
||||
|
||||
if (tech.gpsCoords) {
|
||||
el.classList.add('sb-map-gps-active')
|
||||
el.title = tech.fullName + ' (GPS)'
|
||||
el.title += ' (GPS)'
|
||||
}
|
||||
const m = new mbgl.Marker({ element: outer, anchor: 'bottom' }).setLngLat(pos).addTo(map)
|
||||
mapMarkers.value.push(m)
|
||||
|
|
|
|||
|
|
@ -1497,6 +1497,10 @@ html, body { margin:0; padding:0; height:100%; overflow:hidden; }
|
|||
.sb-map { flex:1; min-height:0; }
|
||||
.sb-map-tech-pin { width:36px; height:36px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:0.65rem; font-weight:800; color:#fff; border:2.5px solid; box-shadow:0 2px 10px rgba(0,0,0,0.55); cursor:pointer; transition:transform 0.15s; }
|
||||
.sb-map-tech-pin:hover { transform:scale(1.2); }
|
||||
.sb-map-tech-pin { position:relative; }
|
||||
.sb-map-load-bar { width:32px; height:4px; background:rgba(255,255,255,0.15); border-radius:2px; margin-top:3px; overflow:hidden; }
|
||||
.sb-map-load-fill { height:100%; border-radius:2px; transition:width 0.3s; }
|
||||
.sb-map-crew-badge { position:absolute; top:-4px; right:-6px; min-width:16px; height:16px; border-radius:8px; background:#6366f1; color:#fff; font-size:9px; font-weight:800; display:flex; align-items:center; justify-content:center; border:2px solid #0f172a; line-height:1; padding:0 3px; }
|
||||
.sb-map-gps-active { box-shadow:0 0 0 3px rgba(16,185,129,0.5), 0 0 12px rgba(16,185,129,0.4), 0 2px 10px rgba(0,0,0,0.55); animation:gps-glow 2s infinite; }
|
||||
@keyframes gps-glow { 0%,100% { box-shadow:0 0 0 3px rgba(16,185,129,0.5), 0 0 12px rgba(16,185,129,0.4), 0 2px 10px rgba(0,0,0,0.55); } 50% { box-shadow:0 0 0 6px rgba(16,185,129,0.3), 0 0 20px rgba(16,185,129,0.3), 0 2px 10px rgba(0,0,0,0.55); } }
|
||||
.sb-map-drag-ghost { padding:4px 8px; border-radius:6px; background:rgba(99,102,241,0.9); color:#fff; font-size:0.68rem; font-weight:700; box-shadow:0 4px 16px rgba(0,0,0,0.55); max-width:180px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; user-select:none; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user