feat: enable aggregated tooltips for overlapping timeline milestones and update UI styling

This commit is contained in:
kdeterme 2026-08-24 18:40:34 +02:00
parent 04a118d04d
commit a21aa59fa9
2 changed files with 93 additions and 31 deletions

View file

@ -143,19 +143,20 @@
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
z-index: 1050; z-index: 1050;
background: rgba(15, 23, 42, 0.94); background: rgba(15, 23, 42, 0.95);
color: #fff; color: #fff;
padding: 0.45rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.75rem; font-size: 0.75rem;
line-height: 1.35; line-height: 1.35;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.22); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
backdrop-filter: blur(4px); backdrop-filter: blur(6px);
border: 1px solid rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.15);
transition: opacity 0.12s ease, transform 0.12s ease; transition: opacity 0.12s ease, transform 0.12s ease;
white-space: nowrap;
transform: translate(-50%, -100%); transform: translate(-50%, -100%);
margin-top: -8px; margin-top: -8px;
min-width: 170px;
max-width: 320px;
} }
.canvas-timeline__tooltip.is-hidden { .canvas-timeline__tooltip.is-hidden {

View file

@ -1849,7 +1849,16 @@ document.addEventListener('DOMContentLoaded', function () {
if (milestoneHit) { if (milestoneHit) {
dom.bodyEl.classList.add('is-hovering-item'); dom.bodyEl.classList.add('is-hovering-item');
showTimelineTooltip(milestoneHit); // Trouver tous les jalons de la même intervention qui se superposent (à moins de 14px sur l'axe X)
const overlapThresholdPx = 14;
const overlappingHits = timelineState.hitRegions.filter(region =>
region.isMilestone &&
region.index === milestoneHit.index &&
Math.abs((region.anchorX || (region.x + region.width / 2)) - (milestoneHit.anchorX || (milestoneHit.x + milestoneHit.width / 2))) <= overlapThresholdPx
);
// Trier chronologiquement
overlappingHits.sort((a, b) => (a.date?.getTime?.() || 0) - (b.date?.getTime?.() || 0));
showTimelineTooltip(overlappingHits.length > 0 ? overlappingHits : [milestoneHit], milestoneHit);
return; return;
} }
@ -1870,47 +1879,99 @@ document.addEventListener('DOMContentLoaded', function () {
} }
} }
function showTimelineTooltip(hit) { function showTimelineTooltip(hits, primaryHit) {
const dom = timelineState.dom; const dom = timelineState.dom;
if (!dom?.tooltipEl) return; if (!dom?.tooltipEl) return;
const hitsList = Array.isArray(hits) ? hits : [hits];
if (!hitsList.length) return;
let dateStr = ''; const anchorHit = primaryHit || hitsList[0];
if (hit.date instanceof Date && !Number.isNaN(hit.date.getTime())) {
dateStr = hit.date.toLocaleString(DOCUMENT_LOCALE, { const formatItemDate = (date) => {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '';
return date.toLocaleString(DOCUMENT_LOCALE, {
day: '2-digit', day: '2-digit',
month: '2-digit', month: '2-digit',
year: 'numeric', year: 'numeric',
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
}); });
} };
let content = ''; let content = '';
if (hit.kind === 'status') {
const color = STATUS_COLORS[hit.statusCode] || DEFAULT_STATUS_COLOR; if (hitsList.length === 1) {
const currentBadge = hit.isCurrent ? ` <span class="badge bg-light text-dark ms-1" style="font-size:0.65rem;">${gettext('Actuel')}</span>` : ''; const hit = hitsList[0];
const userStr = hit.userName ? `<div style="font-size:0.7rem;opacity:0.85;margin-top:2px;">${escapeHtml(hit.userName)}</div>` : ''; const dateStr = formatItemDate(hit.date);
content = ` if (hit.kind === 'status') {
<div class="d-flex align-items-center gap-1 mb-1"> const color = STATUS_COLORS[hit.statusCode] || DEFAULT_STATUS_COLOR;
<span style="display:inline-block;width:9px;height:9px;border-radius:50%;background-color:${color};flex-shrink:0;"></span> const currentBadge = hit.isCurrent ? ` <span class="badge bg-light text-dark ms-1" style="font-size:0.65rem;">${gettext('Actuel')}</span>` : '';
<strong>${escapeHtml(hit.statusLabel || hit.statusCode)}</strong>${currentBadge} const userStr = hit.userName ? `<div style="font-size:0.7rem;opacity:0.85;margin-top:2px;">${escapeHtml(hit.userName)}</div>` : '';
content = `
<div class="d-flex align-items-center gap-1 mb-1">
<span style="display:inline-block;width:9px;height:9px;border-radius:50%;background-color:${color};flex-shrink:0;"></span>
<strong>${escapeHtml(hit.statusLabel || hit.statusCode)}</strong>${currentBadge}
</div>
<div style="font-size:0.75rem;opacity:0.9;">${escapeHtml(dateStr)}</div>
${userStr}
`;
} else {
content = `
<div class="d-flex align-items-center gap-1 mb-1">
<i class="bi bi-calendar-event"></i>
<strong>${escapeHtml(hit.label || hit.milestoneType)}</strong>
</div>
<div style="font-size:0.75rem;opacity:0.9;">${escapeHtml(dateStr)}</div>
`;
}
} else {
// Plusieurs événements superposés
const header = `
<div class="d-flex align-items-center justify-content-between gap-2 pb-1 mb-2 border-bottom border-light border-opacity-25" style="font-size:0.75rem;">
<span class="text-white-50">${escapeHtml(anchorHit.itemCode || '')}</span>
<span class="badge rounded-pill bg-light text-dark" style="font-size:0.65rem;">${hitsList.length} ${gettext('étapes')}</span>
</div> </div>
<div style="font-size:0.75rem;opacity:0.9;">${escapeHtml(dateStr)}</div>
${userStr}
`;
} else if (hit.kind === 'milestone') {
content = `
<div class="d-flex align-items-center gap-1 mb-1">
<i class="bi bi-calendar-event"></i>
<strong>${escapeHtml(hit.label || hit.milestoneType)}</strong>
</div>
<div style="font-size:0.75rem;opacity:0.9;">${escapeHtml(dateStr)}</div>
`; `;
const rows = hitsList.map((hit, idx) => {
const dateStr = formatItemDate(hit.date);
const isBorder = idx < hitsList.length - 1 ? 'border-bottom border-light border-opacity-10 pb-1 mb-1' : '';
if (hit.kind === 'status') {
const color = STATUS_COLORS[hit.statusCode] || DEFAULT_STATUS_COLOR;
const currentBadge = hit.isCurrent ? ` <span class="badge bg-light text-dark ms-1" style="font-size:0.6rem;">${gettext('Actuel')}</span>` : '';
const userStr = hit.userName ? `<span class="text-white-50 ms-1" style="font-size:0.68rem;">(${escapeHtml(hit.userName)})</span>` : '';
return `
<div class="d-flex align-items-start gap-2 ${isBorder}">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background-color:${color};margin-top:3px;flex-shrink:0;"></span>
<div class="flex-grow-1">
<div class="d-flex align-items-center justify-content-between gap-1">
<span style="font-size:0.75rem;font-weight:600;">${escapeHtml(hit.statusLabel || hit.statusCode)}</span>
${currentBadge}
</div>
<div style="font-size:0.7rem;opacity:0.85;">${escapeHtml(dateStr)}${userStr}</div>
</div>
</div>
`;
} else {
return `
<div class="d-flex align-items-start gap-2 ${isBorder}">
<i class="bi bi-calendar-event" style="font-size:0.75rem;margin-top:2px;flex-shrink:0;"></i>
<div class="flex-grow-1">
<div style="font-size:0.75rem;font-weight:600;">${escapeHtml(hit.label || hit.milestoneType)}</div>
<div style="font-size:0.7rem;opacity:0.85;">${escapeHtml(dateStr)}</div>
</div>
</div>
`;
}
}).join('');
content = header + rows;
} }
dom.tooltipEl.innerHTML = content; dom.tooltipEl.innerHTML = content;
dom.tooltipEl.style.left = `${hit.anchorX || (hit.x + hit.width / 2)}px`; dom.tooltipEl.style.left = `${anchorHit.anchorX || (anchorHit.x + anchorHit.width / 2)}px`;
dom.tooltipEl.style.top = `${hit.anchorY || hit.y}px`; dom.tooltipEl.style.top = `${anchorHit.anchorY || anchorHit.y}px`;
dom.tooltipEl.classList.remove('is-hidden'); dom.tooltipEl.classList.remove('is-hidden');
} }