feat: enable aggregated tooltips for overlapping timeline milestones and update UI styling
This commit is contained in:
parent
04a118d04d
commit
a21aa59fa9
2 changed files with 93 additions and 31 deletions
|
|
@ -143,19 +143,20 @@
|
|||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 1050;
|
||||
background: rgba(15, 23, 42, 0.94);
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
color: #fff;
|
||||
padding: 0.45rem 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.35;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.22);
|
||||
backdrop-filter: blur(4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: blur(6px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
transition: opacity 0.12s ease, transform 0.12s ease;
|
||||
white-space: nowrap;
|
||||
transform: translate(-50%, -100%);
|
||||
margin-top: -8px;
|
||||
min-width: 170px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.canvas-timeline__tooltip.is-hidden {
|
||||
|
|
|
|||
|
|
@ -1849,7 +1849,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
|
||||
if (milestoneHit) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1870,47 +1879,99 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
}
|
||||
}
|
||||
|
||||
function showTimelineTooltip(hit) {
|
||||
function showTimelineTooltip(hits, primaryHit) {
|
||||
const dom = timelineState.dom;
|
||||
if (!dom?.tooltipEl) return;
|
||||
|
||||
const hitsList = Array.isArray(hits) ? hits : [hits];
|
||||
if (!hitsList.length) return;
|
||||
|
||||
let dateStr = '';
|
||||
if (hit.date instanceof Date && !Number.isNaN(hit.date.getTime())) {
|
||||
dateStr = hit.date.toLocaleString(DOCUMENT_LOCALE, {
|
||||
const anchorHit = primaryHit || hitsList[0];
|
||||
|
||||
const formatItemDate = (date) => {
|
||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '';
|
||||
return date.toLocaleString(DOCUMENT_LOCALE, {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let content = '';
|
||||
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.65rem;">${gettext('Actuel')}</span>` : '';
|
||||
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}
|
||||
|
||||
if (hitsList.length === 1) {
|
||||
const hit = hitsList[0];
|
||||
const dateStr = formatItemDate(hit.date);
|
||||
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.65rem;">${gettext('Actuel')}</span>` : '';
|
||||
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 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.style.left = `${hit.anchorX || (hit.x + hit.width / 2)}px`;
|
||||
dom.tooltipEl.style.top = `${hit.anchorY || hit.y}px`;
|
||||
dom.tooltipEl.style.left = `${anchorHit.anchorX || (anchorHit.x + anchorHit.width / 2)}px`;
|
||||
dom.tooltipEl.style.top = `${anchorHit.anchorY || anchorHit.y}px`;
|
||||
dom.tooltipEl.classList.remove('is-hidden');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue