Skip to content

Commit 717fd35

Browse files
committed
add tooltips to idlestatus indicators
1 parent 5a3a599 commit 717fd35

File tree

3 files changed

+81
-3
lines changed

3 files changed

+81
-3
lines changed

resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ onActivated(() => {
521521
}
522522
523523
.fullcalendar :deep(.fc-event) {
524-
border-radius: var(--radius);
524+
border-radius: calc(var(--radius) - 4px);
525525
padding: 0;
526526
font-size: 0.75rem;
527527
cursor: pointer;

resources/js/packages/ui/src/FullCalendar/idleStatusPlugin.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createPlugin, PluginDef } from '@fullcalendar/core';
2+
import { computePosition, flip, shift, offset } from '@floating-ui/dom';
23

34
export interface ActivityPeriod {
45
start: string;
@@ -17,6 +18,52 @@ declare module '@fullcalendar/core' {
1718
}
1819
}
1920

21+
/**
22+
* Creates and manages a tooltip element for activity status boxes
23+
*/
24+
function createTooltip(): HTMLElement {
25+
const tooltip = document.createElement('div');
26+
tooltip.className =
27+
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground';
28+
tooltip.style.position = 'fixed';
29+
tooltip.style.pointerEvents = 'none';
30+
tooltip.style.opacity = '0';
31+
tooltip.style.whiteSpace = 'nowrap';
32+
tooltip.style.transform = 'scale(0.95)';
33+
tooltip.style.transition = 'opacity 150ms, transform 150ms';
34+
document.body.appendChild(tooltip);
35+
return tooltip;
36+
}
37+
38+
/**
39+
* Shows tooltip for an activity status box
40+
*/
41+
function showTooltip(box: HTMLElement, tooltip: HTMLElement, text: string) {
42+
tooltip.textContent = text;
43+
tooltip.style.opacity = '1';
44+
tooltip.style.transform = 'scale(1)';
45+
46+
const updatePosition = () => {
47+
computePosition(box, tooltip, {
48+
placement: 'right',
49+
middleware: [offset(8), flip(), shift({ padding: 5 })],
50+
}).then(({ x, y }) => {
51+
tooltip.style.left = `${x}px`;
52+
tooltip.style.top = `${y}px`;
53+
});
54+
};
55+
56+
updatePosition();
57+
}
58+
59+
/**
60+
* Hides the tooltip
61+
*/
62+
function hideTooltip(tooltip: HTMLElement) {
63+
tooltip.style.opacity = '0';
64+
tooltip.style.transform = 'scale(0.95)';
65+
}
66+
2067
/**
2168
* Renders activity status boxes in the calendar time grid
2269
*/
@@ -30,6 +77,10 @@ export function renderActivityStatusBoxes(
3077
const existingBoxes = calendarEl.querySelectorAll('.activity-status-box');
3178
existingBoxes.forEach((box) => box.remove());
3279

80+
// Clean up existing tooltips
81+
const existingTooltips = document.querySelectorAll('.activity-status-tooltip');
82+
existingTooltips.forEach((tooltip) => tooltip.remove());
83+
3384
// Remove has-activity-status class from all lanes
3485
const allLanes = calendarEl.querySelectorAll('.fc-timegrid-col');
3586
allLanes.forEach((lane) => lane.classList.remove('has-activity-status'));
@@ -53,6 +104,9 @@ export function renderActivityStatusBoxes(
53104
activityPeriods.length
54105
);
55106

107+
// Create a single tooltip instance to be reused
108+
const tooltip = createTooltip();
109+
56110
// Get the calendar's current view to determine dates
57111
const dateHeaders = calendarEl.querySelectorAll('.fc-col-header-cell');
58112

@@ -115,7 +169,31 @@ export function renderActivityStatusBoxes(
115169
box.style.left = '4px';
116170
box.style.right = '4px';
117171
box.style.zIndex = '10';
118-
box.style.borderRadius = '4px';
172+
box.style.cursor = 'default';
173+
174+
// Calculate duration in minutes
175+
const actualStart = periodStart > laneDateStart ? periodStart : laneDateStart;
176+
const actualEnd = periodEnd < laneDateEnd ? periodEnd : laneDateEnd;
177+
const durationMs = actualEnd.getTime() - actualStart.getTime();
178+
const durationMinutes = Math.round(durationMs / 60000);
179+
180+
// Format duration
181+
const hours = Math.floor(durationMinutes / 60);
182+
const minutes = durationMinutes % 60;
183+
const durationText = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
184+
185+
// Add tooltip text based on status
186+
const status = period.isIdle ? 'Idling' : 'Active';
187+
const tooltipText = `${status} (${durationText})`;
188+
189+
// Add hover event listeners for tooltip
190+
box.addEventListener('mouseenter', () => {
191+
showTooltip(box, tooltip, tooltipText);
192+
});
193+
194+
box.addEventListener('mouseleave', () => {
195+
hideTooltip(tooltip);
196+
});
119197

120198
// Position relative to the lane
121199
const laneFrame = lane.querySelector('.fc-timegrid-col-frame');

resources/js/packages/ui/src/TimeEntry/TimeEntryAggregateRow.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ function onSelectChange(checked: boolean) {
102102
)
103103
"
104104
@update:checked="onSelectChange" />
105-
<div class="flex items-center min-w-0 space-x-1 @lg:space-x-2">
105+
<div class="flex items-center min-w-0">
106106
<GroupedItemsCountButton :expanded="expanded" @click="expanded = !expanded">
107107
{{ timeEntry?.timeEntries?.length }}
108108
</GroupedItemsCountButton>

0 commit comments

Comments
 (0)