diff --git a/src/composables/useMap.js b/src/composables/useMap.js index 0f3cc18..4e22386 100644 --- a/src/composables/useMap.js +++ b/src/composables/useMap.js @@ -192,17 +192,51 @@ export function useMap (deps) { 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' + // SVG ring dimensions + const SIZE = 46, STROKE = 3.5, R = (SIZE - STROKE) / 2 + const CIRC = 2 * Math.PI * R + const completedJobs = allToday.filter(j => (j.status || '').toLowerCase() === 'completed').length + const totalJobs = allToday.length + const completionPct = totalJobs > 0 ? completedJobs / totalJobs : 0 + // Outer wrapper const outer = document.createElement('div') - outer.style.cssText = 'cursor:pointer;display:flex;flex-direction:column;align-items:center;' + outer.style.cssText = 'cursor:pointer;position:relative;' outer.dataset.techId = tech.id - // Avatar circle + // SVG ring (load arc + completion arc) + if (totalHours > 0) { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svg.setAttribute('width', SIZE); svg.setAttribute('height', SIZE) + svg.style.cssText = 'position:absolute;top:0;left:0;transform:rotate(-90deg);pointer-events:none;' + // Load arc (how full the day is — faded) + const loadArc = document.createElementNS('http://www.w3.org/2000/svg', 'circle') + loadArc.setAttribute('cx', SIZE/2); loadArc.setAttribute('cy', SIZE/2); loadArc.setAttribute('r', R) + loadArc.setAttribute('fill', 'none'); loadArc.setAttribute('stroke', loadColor) + loadArc.setAttribute('stroke-width', STROKE); loadArc.setAttribute('opacity', '0.3') + loadArc.setAttribute('stroke-dasharray', `${CIRC * loadPct} ${CIRC}`) + loadArc.setAttribute('stroke-linecap', 'round') + svg.appendChild(loadArc) + // Completion arc (jobs done — solid bright) + if (completionPct > 0) { + const doneArc = document.createElementNS('http://www.w3.org/2000/svg', 'circle') + doneArc.setAttribute('cx', SIZE/2); doneArc.setAttribute('cy', SIZE/2); doneArc.setAttribute('r', R) + doneArc.setAttribute('fill', 'none'); doneArc.setAttribute('stroke', '#10b981') + doneArc.setAttribute('stroke-width', STROKE); doneArc.setAttribute('opacity', '1') + doneArc.setAttribute('stroke-dasharray', `${CIRC * completionPct * loadPct} ${CIRC}`) + doneArc.setAttribute('stroke-linecap', 'round') + svg.appendChild(doneArc) + } + outer.appendChild(svg) + } + + // Avatar circle (centered inside SVG ring) const el = document.createElement('div') el.className = 'sb-map-tech-pin' - el.style.cssText = `background:${color};border-color:${color};` + const pad = totalHours > 0 ? STROKE + 1 : 0 + el.style.cssText = `background:${color};border-color:${color};margin:${pad}px;` el.textContent = initials - el.title = `${tech.fullName} — ${doneHours.toFixed(1)}h complété / ${totalHours.toFixed(1)}h planifié` + el.title = `${tech.fullName} — ${completedJobs}/${totalJobs} jobs (${doneHours.toFixed(1)}h / ${totalHours.toFixed(1)}h)` outer.appendChild(el) // Group badge (crew size) @@ -215,25 +249,6 @@ export function useMap (deps) { el.appendChild(badge) } - // Dual progress bar: load (background) + done (foreground) - if (totalHours > 0) { - const bar = document.createElement('div') - bar.className = 'sb-map-load-bar' - // Load bar (planned hours — full width = 8h) - const loadFill = document.createElement('div') - loadFill.className = 'sb-map-load-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) - } - // Drag & drop handlers outer.addEventListener('dragover', e => { e.preventDefault(); el.style.transform = 'scale(1.25)' }) outer.addEventListener('dragleave', () => { el.style.transform = '' }) diff --git a/src/pages/DispatchV2Page.vue b/src/pages/DispatchV2Page.vue index cc5d298..ddf1d26 100644 --- a/src/pages/DispatchV2Page.vue +++ b/src/pages/DispatchV2Page.vue @@ -1498,9 +1498,7 @@ 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:hover { transform:scale(1.2); } .sb-map-tech-pin { position:relative; } -.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 { 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; } +/* ring markers — no linear bars needed, SVG handles it */ .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); } }