feat: dual progress bar on map markers (load + completion)

- Background bar (faded): planned hours / 8h capacity
  Color codes: green <4h, yellow 4-6h, orange 6-7h, red 7h+
- Foreground bar (solid green): completed hours / 8h
  Shows real-time job completion progress
- Tooltip: "Name — X.Xh complété / X.Xh planifié"
- Both bars stacked with absolute positioning for clean overlay

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-03-27 12:22:11 -04:00
parent 15813e6caf
commit f7fea2b8e5
2 changed files with 25 additions and 13 deletions

View File

@ -181,13 +181,16 @@ export function useMap (deps) {
const initials = tech.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) const initials = tech.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
const color = TECH_COLORS[tech.colorIdx] const color = TECH_COLORS[tech.colorIdx]
// Calculate daily workload (hours) // Calculate daily workload + completion
const todayJobs = tech.queue.filter(j => jobSpansDate(j, dayStr)) const todayJobs = tech.queue.filter(j => jobSpansDate(j, dayStr))
const todayAssist = (tech.assistJobs || []).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) const allToday = [...todayJobs, ...todayAssist]
+ todayAssist.reduce((s, j) => s + (j.duration || 1), 0) const totalHours = allToday.reduce((s, j) => s + (j.duration || 1), 0)
const loadPct = Math.min(totalHours / 8, 1) // 0..1, capped at 8h const doneHours = allToday.filter(j => (j.status || '').toLowerCase() === 'completed')
const barColor = loadPct < 0.5 ? '#10b981' : loadPct < 0.75 ? '#f59e0b' : loadPct < 0.9 ? '#f97316' : '#ef4444' .reduce((s, j) => s + (j.duration || 1), 0)
const loadPct = Math.min(totalHours / 8, 1)
const donePct = totalHours > 0 ? Math.min(doneHours / 8, 1) : 0
const loadColor = loadPct < 0.5 ? '#10b981' : loadPct < 0.75 ? '#f59e0b' : loadPct < 0.9 ? '#f97316' : '#ef4444'
// Outer wrapper // Outer wrapper
const outer = document.createElement('div') const outer = document.createElement('div')
@ -199,7 +202,7 @@ export function useMap (deps) {
el.className = 'sb-map-tech-pin' el.className = 'sb-map-tech-pin'
el.style.cssText = `background:${color};border-color:${color};` el.style.cssText = `background:${color};border-color:${color};`
el.textContent = initials el.textContent = initials
el.title = `${tech.fullName}${totalHours.toFixed(1)}h / 8h` el.title = `${tech.fullName}${doneHours.toFixed(1)}h complété / ${totalHours.toFixed(1)}h planifié`
outer.appendChild(el) outer.appendChild(el)
// Group badge (crew size) // Group badge (crew size)
@ -212,14 +215,22 @@ export function useMap (deps) {
el.appendChild(badge) el.appendChild(badge)
} }
// Workload progress bar (under avatar) // Dual progress bar: load (background) + done (foreground)
if (totalHours > 0) { if (totalHours > 0) {
const bar = document.createElement('div') const bar = document.createElement('div')
bar.className = 'sb-map-load-bar' bar.className = 'sb-map-load-bar'
const fill = document.createElement('div') // Load bar (planned hours — full width = 8h)
fill.className = 'sb-map-load-fill' const loadFill = document.createElement('div')
fill.style.cssText = `width:${Math.round(loadPct * 100)}%;background:${barColor};` loadFill.className = 'sb-map-load-fill'
bar.appendChild(fill) loadFill.style.cssText = `width:${Math.round(loadPct * 100)}%;background:${loadColor};opacity:0.35;`
bar.appendChild(loadFill)
// Done bar (completed hours — overlaid on top)
if (doneHours > 0) {
const doneFill = document.createElement('div')
doneFill.className = 'sb-map-done-fill'
doneFill.style.cssText = `width:${Math.round(donePct * 100)}%;background:#10b981;`
bar.appendChild(doneFill)
}
outer.appendChild(bar) outer.appendChild(bar)
} }

View File

@ -1498,8 +1498,9 @@ html, body { margin:0; padding:0; height:100%; overflow:hidden; }
.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 { 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:hover { transform:scale(1.2); }
.sb-map-tech-pin { position:relative; } .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-bar { width:32px; height:5px; background:rgba(255,255,255,0.12); border-radius:3px; margin-top:3px; overflow:hidden; position:relative; }
.sb-map-load-fill { height:100%; border-radius:2px; transition:width 0.3s; } .sb-map-load-fill { position:absolute; top:0; left:0; height:100%; border-radius:3px; }
.sb-map-done-fill { position:absolute; top:0; left:0; height:100%; border-radius:3px; }
.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-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; } .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); } } @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); } }