Implementação do tooltip nas SRs do Manage time

This commit is contained in:
Paulo Porto
2026-05-25 19:21:35 -03:00
parent c989088ab1
commit f38bae4479
6 changed files with 889 additions and 34 deletions

View File

@@ -358,6 +358,11 @@
overlay.addEventListener("change", modalEvents.handleOverlayChange); overlay.addEventListener("change", modalEvents.handleOverlayChange);
overlay.addEventListener("input", modalEvents.handleOverlayInput); overlay.addEventListener("input", modalEvents.handleOverlayInput);
overlay.addEventListener("keydown", modalEvents.handleOverlayKeydown); overlay.addEventListener("keydown", modalEvents.handleOverlayKeydown);
overlay.addEventListener("focusin", modalEvents.handleOverlayFocusIn);
overlay.addEventListener("pointerover", modalEvents.handleOverlayPointerOver);
overlay.addEventListener("pointermove", modalEvents.handleOverlayPointerMove);
overlay.addEventListener("pointerout", modalEvents.handleOverlayPointerOut);
overlay.addEventListener("scroll", modalEvents.handleOverlayScroll, true);
document.body.appendChild(overlay); document.body.appendChild(overlay);
renderModal(); renderModal();
@@ -633,6 +638,7 @@
normalizeTimeManagementEntryRowSelection, normalizeTimeManagementEntryRowSelection,
getActivityRequestSrNumbers, getActivityRequestSrNumbers,
normalizeActivityRequest, normalizeActivityRequest,
normalizeActivityRequestDetails,
normalizeTaskTypeRequest, normalizeTaskTypeRequest,
getTimeManagementTaskTypeOptions, getTimeManagementTaskTypeOptions,
getTimeManagementActivityOptions, getTimeManagementActivityOptions,
@@ -787,6 +793,7 @@
normalizeTimeManagementEntryRowSelection, normalizeTimeManagementEntryRowSelection,
getActivityRequestSrNumbers, getActivityRequestSrNumbers,
normalizeActivityRequest, normalizeActivityRequest,
normalizeActivityRequestDetails,
normalizeTaskTypeRequest, normalizeTaskTypeRequest,
getTimeManagementTaskTypeOptions, getTimeManagementTaskTypeOptions,
getTimeManagementActivityOptions, getTimeManagementActivityOptions,

View File

@@ -29,6 +29,7 @@
normalizeTimeManagementEntryRowSelection, normalizeTimeManagementEntryRowSelection,
getActivityRequestSrNumbers, getActivityRequestSrNumbers,
normalizeActivityRequest, normalizeActivityRequest,
normalizeActivityRequestDetails,
normalizeTaskTypeRequest, normalizeTaskTypeRequest,
getTimeManagementTaskTypeOptions, getTimeManagementTaskTypeOptions,
getTimeManagementActivityOptions, getTimeManagementActivityOptions,
@@ -162,6 +163,10 @@
activityPayload, activityPayload,
serviceRequests serviceRequests
); );
const activityRequestDetailsBySr = normalizeActivityRequestDetails(
activityPayload,
serviceRequests
);
const taskTypes = normalizeTaskTypeRequest(taskTypePayload); const taskTypes = normalizeTaskTypeRequest(taskTypePayload);
const rows = timeEntryRows; const rows = timeEntryRows;
const selectedSrNumber = rows[0]?.selectedSrNumber || ""; const selectedSrNumber = rows[0]?.selectedSrNumber || "";
@@ -188,6 +193,7 @@
serviceRequests, serviceRequests,
selectedSrNumber, selectedSrNumber,
activityOptionsBySr, activityOptionsBySr,
activityRequestDetailsBySr,
activityRequestUrl, activityRequestUrl,
activityRequestPayload: activityPayload, activityRequestPayload: activityPayload,
timeEntryRows: normalizedRows, timeEntryRows: normalizedRows,
@@ -227,6 +233,7 @@
serviceRequests: [], serviceRequests: [],
selectedSrNumber: "", selectedSrNumber: "",
activityOptionsBySr: {}, activityOptionsBySr: {},
activityRequestDetailsBySr: {},
activityRequestUrl: "", activityRequestUrl: "",
activityRequestPayload: null, activityRequestPayload: null,
selectedActivityValue: "", selectedActivityValue: "",

View File

@@ -37,6 +37,7 @@
filterTimeManagementCombobox, filterTimeManagementCombobox,
handleTimeManagementComboboxKeydown, handleTimeManagementComboboxKeydown,
} = dependencies; } = dependencies;
let timeManagementTooltipHideTimer = 0;
function handleOverlayClick(event) { function handleOverlayClick(event) {
const overlay = document.getElementById( const overlay = document.getElementById(
@@ -70,6 +71,16 @@
return; return;
} }
if (
event.target instanceof HTMLInputElement &&
event.target.getAttribute("data-combobox-field")
) {
window.requestAnimationFrame(() => {
positionTimeManagementCombobox(event.target);
scrollSelectedTimeManagementComboboxOptionIntoView(event.target);
});
}
const actionElement = event.target.closest("[data-action]"); const actionElement = event.target.closest("[data-action]");
if (!actionElement) { if (!actionElement) {
@@ -87,6 +98,13 @@
return; return;
} }
if (
action === "time-management-combobox-option" &&
event.target?.closest?.(".arch-panel-extension-service-request-tooltip")
) {
return;
}
event.preventDefault(); event.preventDefault();
if (action === "close") { if (action === "close") {
@@ -406,6 +424,134 @@
} }
filterTimeManagementCombobox(target); filterTimeManagementCombobox(target);
positionTimeManagementCombobox(target);
}
function handleOverlayFocusIn(event) {
const target = event.target;
if (
target instanceof HTMLInputElement &&
target.getAttribute("data-combobox-field")
) {
window.requestAnimationFrame(() => {
clearTimeManagementComboboxTooltipOptions(
target.closest(".arch-panel-extension-combobox")
);
hideTimeManagementServiceRequestTooltipPortal();
positionTimeManagementCombobox(target);
scrollSelectedTimeManagementComboboxOptionIntoView(target);
});
}
}
function handleOverlayPointerOver(event) {
const portal = event.target?.closest?.(
".arch-panel-extension-service-request-tooltip-portal"
);
if (portal instanceof HTMLElement) {
clearTimeManagementTooltipHideTimer();
return;
}
const option = event.target?.closest?.(
".arch-panel-extension-combobox-option.has-tooltip"
);
if (option instanceof HTMLElement) {
setActiveTimeManagementComboboxTooltipOption(option);
}
}
function handleOverlayPointerMove(event) {
if (
event.target?.closest?.(".arch-panel-extension-service-request-tooltip-portal")
) {
return;
}
const option = event.target?.closest?.(
".arch-panel-extension-combobox-option.has-tooltip"
);
if (option instanceof HTMLElement) {
setActiveTimeManagementComboboxTooltipOption(option);
}
}
function handleOverlayPointerOut(event) {
const portal = event.target?.closest?.(
".arch-panel-extension-service-request-tooltip-portal"
);
const nextTarget = event.relatedTarget;
if (portal instanceof HTMLElement) {
if (nextTarget instanceof Node && portal.contains(nextTarget)) {
return;
}
scheduleTimeManagementTooltipHide();
return;
}
const menu = event.target?.closest?.(
".arch-panel-extension-combobox-menu.has-service-request-tooltips"
);
if (!(menu instanceof HTMLElement)) {
return;
}
if (nextTarget instanceof Node && menu.contains(nextTarget)) {
return;
}
scheduleTimeManagementTooltipHide(menu);
}
function scheduleTimeManagementTooltipHide(menu = null) {
clearTimeManagementTooltipHideTimer();
timeManagementTooltipHideTimer = window.setTimeout(() => {
const portal = getTimeManagementServiceRequestTooltipPortal(false);
const hoveredOption = menu
? menu.querySelector(".arch-panel-extension-combobox-option.has-tooltip:hover")
: document.querySelector(".arch-panel-extension-combobox-option.has-tooltip:hover");
if (
!hoveredOption &&
!(portal instanceof HTMLElement && portal.matches(":hover"))
) {
clearTimeManagementComboboxTooltipOptions(menu || document.body);
hideTimeManagementServiceRequestTooltipPortal();
}
}, 500);
}
function clearTimeManagementTooltipHideTimer() {
if (timeManagementTooltipHideTimer) {
window.clearTimeout(timeManagementTooltipHideTimer);
timeManagementTooltipHideTimer = 0;
}
}
function handleOverlayScroll(event) {
const menu = event.target;
if (
!(menu instanceof HTMLElement) ||
!menu.classList.contains("arch-panel-extension-combobox-menu")
) {
return;
}
const activeOption = menu.querySelector(
".arch-panel-extension-combobox-option.has-tooltip.is-tooltip-active"
);
if (activeOption instanceof HTMLElement) {
positionTimeManagementComboboxOptionTooltip(activeOption);
}
} }
function handleOverlayKeydown(event) { function handleOverlayKeydown(event) {
@@ -435,6 +581,188 @@
} }
handleTimeManagementComboboxKeydown(event, target); handleTimeManagementComboboxKeydown(event, target);
positionTimeManagementCombobox(target);
}
function positionTimeManagementCombobox(input) {
const combobox = input.closest(".arch-panel-extension-combobox");
const menu = combobox?.querySelector(".arch-panel-extension-combobox-menu");
if (!(combobox instanceof HTMLElement) || !(menu instanceof HTMLElement)) {
return;
}
const inputRect = input.getBoundingClientRect();
const margin = 16;
const gap = 6;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const spaceBelow = viewportHeight - inputRect.bottom - margin - gap;
const spaceAbove = inputRect.top - margin - gap;
const hasServiceRequestTooltips = menu.classList.contains(
"has-service-request-tooltips"
);
const openAbove = spaceBelow < 240 && spaceAbove > spaceBelow;
const availableHeight = Math.max(180, (openAbove ? spaceAbove : spaceBelow) - 12);
const desiredHeight = hasServiceRequestTooltips ? 430 : 260;
const menuWidth = hasServiceRequestTooltips
? Math.min(1120, Math.max(inputRect.width, viewportWidth - inputRect.left - margin))
: inputRect.width;
menu.style.top = openAbove ? "auto" : `calc(100% + ${gap}px)`;
menu.style.bottom = openAbove ? `calc(100% + ${gap}px)` : "auto";
menu.style.maxHeight = `${Math.min(desiredHeight, availableHeight)}px`;
menu.style.width = `${menuWidth}px`;
menu.style.maxWidth = `calc(100vw - ${Math.max(margin * 2, inputRect.left + margin)}px)`;
const activeOption = menu.querySelector(
".arch-panel-extension-combobox-option.has-tooltip.is-tooltip-active, .arch-panel-extension-combobox-option.has-tooltip:hover, .arch-panel-extension-combobox-option.has-tooltip:focus, .arch-panel-extension-combobox-option.has-tooltip.is-active"
);
if (activeOption instanceof HTMLElement) {
setActiveTimeManagementComboboxTooltipOption(activeOption);
}
}
function scrollSelectedTimeManagementComboboxOptionIntoView(input) {
const combobox = input.closest(".arch-panel-extension-combobox");
const menu = combobox?.querySelector(".arch-panel-extension-combobox-menu");
const selectedOption = menu?.querySelector(
".arch-panel-extension-combobox-option.is-selected:not([hidden])"
);
if (!(menu instanceof HTMLElement) || !(selectedOption instanceof HTMLElement)) {
return;
}
const targetScrollTop =
selectedOption.offsetTop -
Math.max(0, (menu.clientHeight - selectedOption.offsetHeight) / 2);
menu.scrollTop = Math.max(0, targetScrollTop);
}
function setActiveTimeManagementComboboxTooltipOption(option) {
const menu = option.closest(".arch-panel-extension-combobox-menu");
if (!(menu instanceof HTMLElement) || option.hidden) {
return;
}
clearTimeManagementComboboxTooltipOptions(menu, option);
option.classList.add("is-tooltip-active");
positionTimeManagementComboboxOptionTooltip(option);
}
function clearTimeManagementComboboxTooltipOptions(root, exceptOption = null) {
if (!(root instanceof Element)) {
return;
}
root
.querySelectorAll(".arch-panel-extension-combobox-option.is-tooltip-active")
.forEach((item) => {
if (item !== exceptOption) {
item.classList.remove("is-tooltip-active");
}
});
}
function positionTimeManagementComboboxOptionTooltip(option) {
const menu = option.closest(".arch-panel-extension-combobox-menu");
const tooltip = option.querySelector(
".arch-panel-extension-service-request-tooltip"
);
if (!(menu instanceof HTMLElement) || !(tooltip instanceof HTMLElement)) {
return;
}
const menuRect = menu.getBoundingClientRect();
const optionRect = option.getBoundingClientRect();
const padding = 10;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
const portal = getTimeManagementServiceRequestTooltipPortal();
if (!(portal instanceof HTMLElement)) {
return;
}
const tooltipWidth = Math.min(
420,
Math.max(320, Math.min(menuRect.width * 0.38, viewportWidth - padding * 2))
);
const reservedTextWidth = tooltipWidth + 28;
const maxTooltipHeight = Math.max(180, Math.min(380, viewportHeight - padding * 2));
const optionCenter = optionRect.top + optionRect.height / 2;
const availableRight = viewportWidth - optionRect.right - padding * 2;
const availableLeft = optionRect.left - padding * 2;
const verticalPadding = 12;
portal.innerHTML = tooltip.innerHTML;
portal.style.width = `${tooltipWidth}px`;
portal.style.maxHeight = `${maxTooltipHeight}px`;
portal.classList.add("is-visible");
const tooltipHeight = Math.min(
maxTooltipHeight,
Math.max(120, portal.getBoundingClientRect().height || portal.scrollHeight)
);
const preferredRight = Math.min(
menuRect.right - tooltipWidth - padding,
viewportWidth - tooltipWidth - padding
);
const preferredLeft = menuRect.left + padding;
const hasRoomOnRight = availableRight >= tooltipWidth;
const hasRoomOnLeft = availableLeft >= tooltipWidth;
const tooltipLeft = hasRoomOnRight
? optionRect.right + padding
: hasRoomOnLeft
? optionRect.left - padding - tooltipWidth
: Math.max(padding, Math.min(preferredRight, preferredLeft));
const tooltipTop = Math.max(
verticalPadding,
Math.min(
optionCenter - tooltipHeight / 2,
viewportHeight - tooltipHeight - verticalPadding
)
);
portal.style.top = `${tooltipTop}px`;
portal.style.left = `${tooltipLeft}px`;
portal.style.right = "auto";
portal.style.bottom = "auto";
portal.style.maxHeight = `${Math.min(maxTooltipHeight, viewportHeight - tooltipTop - verticalPadding)}px`;
menu.style.setProperty(
"--arch-service-request-tooltip-reserved-width",
`${reservedTextWidth}px`
);
}
function getTimeManagementServiceRequestTooltipPortal(createIfMissing = true) {
const overlay = document.getElementById(globalThis.ArchPanelConfig.IDS.overlay);
let portal = overlay?.querySelector(
".arch-panel-extension-service-request-tooltip-portal"
);
if (!portal && createIfMissing && overlay) {
portal = document.createElement("div");
portal.className =
"arch-panel-extension-service-request-tooltip arch-panel-extension-service-request-tooltip-portal";
portal.setAttribute("role", "tooltip");
overlay.appendChild(portal);
}
return portal;
}
function hideTimeManagementServiceRequestTooltipPortal() {
const portal = getTimeManagementServiceRequestTooltipPortal(false);
if (portal instanceof HTMLElement) {
portal.classList.remove("is-visible");
}
} }
function handleTimeManagementHourKeydown(event, input) { function handleTimeManagementHourKeydown(event, input) {
@@ -524,6 +852,11 @@
handleOverlayChange, handleOverlayChange,
handleOverlayInput, handleOverlayInput,
handleOverlayKeydown, handleOverlayKeydown,
handleOverlayFocusIn,
handleOverlayPointerOver,
handleOverlayPointerMove,
handleOverlayPointerOut,
handleOverlayScroll,
}); });
} }

View File

@@ -1619,6 +1619,8 @@ body.arch-panel-extension-modal-open {
} }
.arch-panel-extension-time-management-card { .arch-panel-extension-time-management-card {
position: relative;
z-index: 1;
display: grid; display: grid;
--arch-time-management-scrollbar-gutter: 16px; --arch-time-management-scrollbar-gutter: 16px;
gap: 4px; gap: 4px;
@@ -1630,6 +1632,10 @@ body.arch-panel-extension-modal-open {
border-color: rgba(217, 79, 43, 0.42); border-color: rgba(217, 79, 43, 0.42);
} }
.arch-panel-extension-time-management-card:has(.arch-panel-extension-combobox:focus-within) {
z-index: 40;
}
.arch-panel-extension-time-management-toolbar { .arch-panel-extension-time-management-toolbar {
display: flex; display: flex;
align-items: end; align-items: end;
@@ -1875,6 +1881,10 @@ body.arch-panel-extension-modal-open {
overflow: visible; overflow: visible;
} }
.arch-panel-extension-time-management-rows:has(.arch-panel-extension-combobox-option.has-tooltip:hover) {
overflow: visible;
}
.arch-panel-extension-time-management-empty { .arch-panel-extension-time-management-empty {
grid-column: 1 / -1; grid-column: 1 / -1;
text-align: center; text-align: center;
@@ -1901,6 +1911,7 @@ body.arch-panel-extension-modal-open {
} }
.arch-panel-extension-time-management-entry-row { .arch-panel-extension-time-management-entry-row {
position: relative;
display: grid; display: grid;
grid-template-columns: grid-template-columns:
minmax(420px, 3.8fr) minmax(420px, 3.8fr)
@@ -1912,6 +1923,10 @@ body.arch-panel-extension-modal-open {
gap: 6px; gap: 6px;
} }
.arch-panel-extension-time-management-entry-row:has(.arch-panel-extension-combobox:focus-within) {
z-index: 60;
}
.arch-panel-extension-time-management-entry-row.is-compact { .arch-panel-extension-time-management-entry-row.is-compact {
align-items: center; align-items: center;
} }
@@ -1950,9 +1965,14 @@ body.arch-panel-extension-modal-open {
.arch-panel-extension-combobox { .arch-panel-extension-combobox {
position: relative; position: relative;
z-index: 0;
display: block; display: block;
} }
.arch-panel-extension-combobox:focus-within {
z-index: 70;
}
.arch-panel-extension-combobox input { .arch-panel-extension-combobox input {
min-height: 40px; min-height: 40px;
padding-left: 12px; padding-left: 12px;
@@ -1991,6 +2011,13 @@ body.arch-panel-extension-modal-open {
white-space: nowrap; white-space: nowrap;
} }
.arch-panel-extension-combobox-value-text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arch-panel-extension-combobox:focus-within .arch-panel-extension-combobox-value { .arch-panel-extension-combobox:focus-within .arch-panel-extension-combobox-value {
display: none; display: none;
} }
@@ -2042,9 +2069,10 @@ body.arch-panel-extension-modal-open {
.arch-panel-extension-combobox-menu { .arch-panel-extension-combobox-menu {
position: absolute; position: absolute;
top: calc(100% + 6px); top: calc(100% + 6px);
bottom: auto;
left: 0; left: 0;
right: 0; right: 0;
z-index: 10000; z-index: 2147483000;
display: none; display: none;
max-height: 260px; max-height: 260px;
overflow: auto; overflow: auto;
@@ -2055,6 +2083,15 @@ body.arch-panel-extension-modal-open {
padding: 6px; padding: 6px;
} }
.arch-panel-extension-combobox-menu.has-service-request-tooltips {
right: auto;
width: min(1120px, calc(100vw - 72px));
max-height: min(58vh, 430px);
overscroll-behavior: contain;
padding-bottom: 12px;
scroll-padding-block: 10px 18px;
}
.arch-panel-extension-combobox:focus-within .arch-panel-extension-combobox-menu { .arch-panel-extension-combobox:focus-within .arch-panel-extension-combobox-menu {
display: grid; display: grid;
gap: 4px; gap: 4px;
@@ -2126,12 +2163,202 @@ body.arch-panel-extension-modal-open {
display: none; display: none;
} }
.arch-panel-extension-combobox-option.has-tooltip {
position: static;
}
.arch-panel-extension-combobox-option.has-tooltip.is-selected::after {
display: none;
}
.arch-panel-extension-combobox-empty { .arch-panel-extension-combobox-empty {
color: var(--wb-text-muted); color: var(--wb-text-muted);
font-size: 12px; font-size: 12px;
padding: 8px 9px; padding: 8px 9px;
} }
.arch-panel-extension-service-request-tooltip {
--arch-tooltip-panel: var(--wb-panel, #ffffff);
--arch-tooltip-border: var(--wb-border-strong, #c8c8c8);
--arch-tooltip-text: var(--wb-text, #1f1f1f);
--arch-tooltip-muted: var(--wb-text-muted, #6f7782);
--arch-tooltip-link: var(--wb-link, #006b9a);
position: fixed;
top: 10px;
right: auto;
bottom: auto;
left: 10px;
z-index: 2147483647;
display: grid;
gap: 10px;
width: 420px;
max-width: min(420px, calc(100vw - 96px));
max-height: min(360px, calc(100vh - 20px));
overflow: auto;
visibility: hidden;
opacity: 0;
pointer-events: auto;
border: 1px solid var(--arch-tooltip-border);
border-radius: 8px;
background: var(--arch-tooltip-panel);
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.24);
color: var(--arch-tooltip-text);
padding: 12px;
transform: translateY(4px);
transition:
opacity 120ms ease 500ms,
transform 120ms ease 500ms,
visibility 0ms linear 500ms;
}
.arch-panel-extension-combobox-option.has-tooltip > .arch-panel-extension-service-request-tooltip {
display: none;
}
.arch-panel-extension-service-request-tooltip-portal.is-visible {
visibility: visible;
opacity: 1;
transform: translateY(0);
transition-delay: 0ms;
}
.arch-panel-extension-combobox-option.has-tooltip:hover .arch-panel-extension-service-request-tooltip,
.arch-panel-extension-combobox-option.has-tooltip:focus .arch-panel-extension-service-request-tooltip,
.arch-panel-extension-combobox-option.has-tooltip.is-tooltip-active .arch-panel-extension-service-request-tooltip {
visibility: visible;
opacity: 1;
transform: translateY(0);
transition-delay: 0ms;
}
.arch-panel-extension-combobox-menu.has-service-request-tooltips:has(.arch-panel-extension-combobox-option.has-tooltip:hover)
.arch-panel-extension-combobox-option-main,
.arch-panel-extension-combobox-menu.has-service-request-tooltips:has(.arch-panel-extension-combobox-option.has-tooltip:focus)
.arch-panel-extension-combobox-option-main,
.arch-panel-extension-combobox-menu.has-service-request-tooltips:has(.arch-panel-extension-combobox-option.has-tooltip.is-tooltip-active)
.arch-panel-extension-combobox-option-main {
max-width: calc(100% - var(--arch-service-request-tooltip-reserved-width, 448px));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arch-panel-extension-service-request-tooltip-title {
display: block;
color: var(--arch-tooltip-text);
font-size: 12px;
font-weight: 800;
line-height: 1.35;
}
.arch-panel-extension-service-request-tooltip-grid {
display: grid;
grid-template-columns: minmax(110px, 0.42fr) minmax(0, 1fr);
gap: 7px 10px;
align-items: start;
}
.arch-panel-extension-service-request-tooltip-label {
color: var(--arch-tooltip-muted);
font-size: 10px;
font-weight: 800;
line-height: 1.35;
text-transform: uppercase;
}
.arch-panel-extension-service-request-tooltip-value {
min-width: 0;
color: var(--arch-tooltip-text);
font-size: 11px;
font-weight: 600;
line-height: 1.4;
overflow-wrap: anywhere;
}
.arch-panel-extension-service-request-tooltip-excerpt {
display: block;
}
.arch-panel-extension-service-request-tooltip-more {
display: grid;
gap: 6px;
margin-top: 4px;
}
.arch-panel-extension-service-request-tooltip-more summary {
width: max-content;
color: var(--arch-tooltip-link);
cursor: pointer;
font-size: 11px;
font-weight: 800;
list-style: none;
}
.arch-panel-extension-service-request-tooltip-more summary::-webkit-details-marker {
display: none;
}
.arch-panel-extension-service-request-tooltip-more[open] summary {
margin-bottom: 2px;
}
.arch-panel-extension-service-request-tooltip-value:has(.arch-panel-extension-service-request-tooltip-more[open])
.arch-panel-extension-service-request-tooltip-excerpt {
display: none;
}
@media (max-width: 1180px) {
.arch-panel-extension-combobox-menu.has-service-request-tooltips {
width: min(760px, calc(100vw - 48px));
}
.arch-panel-extension-service-request-tooltip {
right: auto;
bottom: auto;
left: 10px;
width: min(340px, calc(100% - 24px));
transform: translateY(4px);
}
.arch-panel-extension-combobox-menu.has-service-request-tooltips:has(.arch-panel-extension-combobox-option.has-tooltip:hover)
.arch-panel-extension-combobox-option-main,
.arch-panel-extension-combobox-menu.has-service-request-tooltips:has(.arch-panel-extension-combobox-option.has-tooltip:focus)
.arch-panel-extension-combobox-option-main,
.arch-panel-extension-combobox-menu.has-service-request-tooltips:has(.arch-panel-extension-combobox-option.has-tooltip.is-tooltip-active)
.arch-panel-extension-combobox-option-main {
max-width: calc(100% - var(--arch-service-request-tooltip-reserved-width, 368px));
}
.arch-panel-extension-combobox-option.has-tooltip:hover .arch-panel-extension-service-request-tooltip,
.arch-panel-extension-combobox-option.has-tooltip:focus .arch-panel-extension-service-request-tooltip,
.arch-panel-extension-combobox-option.has-tooltip.is-tooltip-active .arch-panel-extension-service-request-tooltip {
transform: translateY(0);
}
}
@media (max-width: 760px) {
.arch-panel-extension-combobox-menu.has-service-request-tooltips {
width: min(520px, calc(100vw - 32px));
}
.arch-panel-extension-combobox-menu.has-service-request-tooltips:has(.arch-panel-extension-combobox-option.has-tooltip:hover)
.arch-panel-extension-combobox-option-main,
.arch-panel-extension-combobox-menu.has-service-request-tooltips:has(.arch-panel-extension-combobox-option.has-tooltip:focus)
.arch-panel-extension-combobox-option-main,
.arch-panel-extension-combobox-menu.has-service-request-tooltips:has(.arch-panel-extension-combobox-option.has-tooltip.is-tooltip-active)
.arch-panel-extension-combobox-option-main {
max-width: 100%;
}
.arch-panel-extension-service-request-tooltip {
right: auto;
bottom: auto;
left: 8px;
width: min(420px, calc(100vw - 48px));
max-height: min(320px, calc(100vh - 120px));
}
}
.arch-panel-extension-time-management-days { .arch-panel-extension-time-management-days {
display: grid; display: grid;
grid-template-columns: repeat(7, minmax(110px, 1fr)); grid-template-columns: repeat(7, minmax(110px, 1fr));

View File

@@ -14,6 +14,34 @@
getTimeManagementWeekdayName, getTimeManagementWeekdayName,
createTimeManagementRowSignature, createTimeManagementRowSignature,
} = dependencies; } = dependencies;
const ACTIVITY_REQUEST_DETAIL_FIELDS = [
"SrNumber",
"ProblemDescription",
"AccountPartyUniqueName",
"ServiceNew_c",
"OptyOwner_c",
"OptyName_c",
"OptyNumber_c",
"ReportedByPartyUniqueName",
"StatusCdMeaning",
"CountryText_c",
"AssigneePersonName",
"OptyStatus_c",
];
const ACTIVITY_REQUEST_DETAIL_LABELS = {
SrNumber: "SR Number",
ProblemDescription: "Problem Description",
AccountPartyUniqueName: "Account",
ServiceNew_c: "Service",
OptyOwner_c: "Opportunity Owner",
OptyName_c: "Opportunity",
OptyNumber_c: "Opportunity Number",
ReportedByPartyUniqueName: "Reported By",
StatusCdMeaning: "Status",
CountryText_c: "Country",
AssigneePersonName: "Assignee",
OptyStatus_c: "Opportunity Status",
};
function normalizeInfoWeekResponse(payload) { function normalizeInfoWeekResponse(payload) {
return extractArray(payload, ["items", "data", "results", "content"]) return extractArray(payload, ["items", "data", "results", "content"])
@@ -275,16 +303,7 @@
function normalizeActivityRequest(payload, serviceRequests) { function normalizeActivityRequest(payload, serviceRequests) {
const activityOptionsBySr = {}; const activityOptionsBySr = {};
const activityItemsBySr = new Map(); const activityItemsBySr = getActivityRequestItemsBySr(payload);
for (const item of extractArray(payload, ["items", "data", "results", "content"])) {
const srNumber = cleanString(getFieldValue(item, ["SrNumber", "srNumber"]));
const srKey = normalizeString(srNumber);
if (srKey && !activityItemsBySr.has(srKey)) {
activityItemsBySr.set(srKey, item);
}
}
for (const serviceRequest of serviceRequests || []) { for (const serviceRequest of serviceRequests || []) {
const srNumber = cleanString(serviceRequest?.srNumber); const srNumber = cleanString(serviceRequest?.srNumber);
@@ -300,6 +319,140 @@
return activityOptionsBySr; return activityOptionsBySr;
} }
function normalizeActivityRequestDetails(payload, serviceRequests) {
const detailsBySr = {};
const activityItemsBySr = getActivityRequestItemsBySr(payload);
const srNumbers = getActivityRequestDetailSrNumbers(
activityItemsBySr,
serviceRequests
);
for (const srNumber of srNumbers) {
const srKey = normalizeString(srNumber);
const activityItem = activityItemsBySr.get(srKey);
const details = normalizeActivityRequestDetailRows(activityItem);
if (details.length) {
detailsBySr[srNumber] = details;
}
}
return detailsBySr;
}
function getActivityRequestItemsBySr(payload) {
const activityItemsBySr = new Map();
for (const item of extractArray(payload, ["items", "data", "results", "content"])) {
const srNumber = cleanString(getFieldValue(item, ["SrNumber", "srNumber"]));
const srKey = normalizeString(srNumber);
if (srKey && !activityItemsBySr.has(srKey)) {
activityItemsBySr.set(srKey, item);
}
}
return activityItemsBySr;
}
function getActivityRequestDetailSrNumbers(activityItemsBySr, serviceRequests) {
const seen = new Set();
return [
...(serviceRequests || []).map((item) => cleanString(item?.srNumber)),
...Array.from(activityItemsBySr.values()).map((item) =>
cleanString(getFieldValue(item, ["SrNumber", "srNumber"]))
),
].filter((srNumber) => {
const key = normalizeString(srNumber);
if (!key || seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function normalizeActivityRequestDetailRows(item) {
if (!item || typeof item !== "object" || Array.isArray(item)) {
return [];
}
const seen = new Set();
const preferredRows = ACTIVITY_REQUEST_DETAIL_FIELDS.map((fieldName) =>
createActivityRequestDetailRow(fieldName, getActivityRequestDetailValue(item, fieldName))
).filter(Boolean);
const extraRows = flattenDisplayRecord(item)
.map(([fieldName, value]) => createActivityRequestDetailRow(fieldName, value))
.filter(Boolean);
return [...preferredRows, ...extraRows].filter((row) => {
const key = normalizeActivityRequestDetailKey(row.key);
if (!key || seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function createActivityRequestDetailRow(fieldName, value) {
if (isActivityRequestDetailExcludedKey(fieldName) || isObjectLike(value)) {
return null;
}
const detailValue = cleanString(value);
if (!detailValue) {
return null;
}
return {
key: cleanString(fieldName),
label: formatActivityRequestDetailLabel(fieldName),
value: detailValue,
};
}
function getActivityRequestDetailValue(item, fieldName) {
const lowerFirstFieldName = `${fieldName.charAt(0).toLowerCase()}${fieldName.slice(1)}`;
return getFieldValue(item, [fieldName, lowerFirstFieldName]);
}
function isActivityRequestDetailExcludedKey(fieldName) {
return normalizeActivityRequestDetailKey(fieldName).includes("SRACTIVITIESC");
}
function normalizeActivityRequestDetailKey(fieldName) {
return normalizeString(fieldName).replace(/[^A-Z0-9]/g, "");
}
function isObjectLike(value) {
return value && typeof value === "object";
}
function formatActivityRequestDetailLabel(fieldName) {
const cleanFieldName = cleanString(fieldName);
const directLabel = ACTIVITY_REQUEST_DETAIL_LABELS[cleanFieldName];
if (directLabel) {
return directLabel;
}
return cleanFieldName
.split(".")
.pop()
.replace(/_c$/i, "")
.replace(/_/g, " ")
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.trim();
}
function normalizeSrActivityFieldOptions(item) { function normalizeSrActivityFieldOptions(item) {
const activityText = firstNonEmptyString([ const activityText = firstNonEmptyString([
getFieldValue(item, ["SRActivities_c", "srActivities_c"]), getFieldValue(item, ["SRActivities_c", "srActivities_c"]),
@@ -811,6 +964,7 @@
normalizeTimeManagementEntryRowSelection, normalizeTimeManagementEntryRowSelection,
getActivityRequestSrNumbers, getActivityRequestSrNumbers,
normalizeActivityRequest, normalizeActivityRequest,
normalizeActivityRequestDetails,
normalizeTaskTypeRequest, normalizeTaskTypeRequest,
getTimeManagementTaskTypeOptions, getTimeManagementTaskTypeOptions,
getTimeManagementActivityOptions, getTimeManagementActivityOptions,

View File

@@ -228,17 +228,12 @@
<div class="arch-panel-extension-time-management-entry-row is-compact${isHighlighted ? " is-duplicate-highlight" : ""}" data-row-id="${escapeHtml(rowId)}"> <div class="arch-panel-extension-time-management-entry-row is-compact${isHighlighted ? " is-duplicate-highlight" : ""}" data-row-id="${escapeHtml(rowId)}">
<input type="hidden" name="timeEntryId" value="${escapeHtml(row?.timeEntryId || "")}"> <input type="hidden" name="timeEntryId" value="${escapeHtml(row?.timeEntryId || "")}">
<label class="arch-panel-extension-form-field"> <label class="arch-panel-extension-form-field">
${renderSearchableCombobox({ ${renderTimeManagementServiceRequestField({
field: "sr", modal,
rowId, rowId,
selectedValue: selectedSr, selectedSr,
value: selectedSrLabel, selectedSrLabel,
placeholder: "Search SR", isNonServiceRequest,
options: modal.serviceRequests || [],
valueKey: "srNumber",
labelKey: "label",
fallbackLabelKey: "srTitle",
disabled: isNonServiceRequest,
})} })}
</label> </label>
<label class="arch-panel-extension-form-field"> <label class="arch-panel-extension-form-field">
@@ -288,6 +283,101 @@
`; `;
} }
function renderTimeManagementServiceRequestField({
modal,
rowId,
selectedSr,
selectedSrLabel,
isNonServiceRequest,
}) {
return renderSearchableCombobox({
field: "sr",
rowId,
selectedValue: selectedSr,
value: selectedSrLabel,
placeholder: "Search SR",
options: modal.serviceRequests || [],
valueKey: "srNumber",
labelKey: "label",
fallbackLabelKey: "srTitle",
disabled: isNonServiceRequest,
optionTooltipHtml: (option) => {
const optionSrNumber = cleanString(option?.srNumber);
const details = getTimeManagementServiceRequestDetails(modal, optionSrNumber);
return details.length
? renderTimeManagementServiceRequestTooltip(
option?.label ||
formatTimeManagementServiceRequestLabel(option) ||
optionSrNumber,
details
)
: "";
},
});
}
function renderTimeManagementServiceRequestTooltip(title, details) {
return `
<span class="arch-panel-extension-service-request-tooltip" role="tooltip">
<span class="arch-panel-extension-service-request-tooltip-title">
${escapeHtml(title || "Service Request")}
</span>
<span class="arch-panel-extension-service-request-tooltip-grid">
${details
.map(
(detail) => `
<span class="arch-panel-extension-service-request-tooltip-label">
${escapeHtml(detail.label || detail.key || "")}
</span>
<span class="arch-panel-extension-service-request-tooltip-value">
${renderTimeManagementServiceRequestTooltipValue(detail)}
</span>
`
)
.join("")}
</span>
</span>
`;
}
function renderTimeManagementServiceRequestTooltipValue(detail) {
const value = cleanString(detail?.value);
if (!isLongProblemDescriptionDetail(detail, value)) {
return escapeHtml(value);
}
return `
<span class="arch-panel-extension-service-request-tooltip-excerpt">
${escapeHtml(value.slice(0, 180).trim())}...
</span>
<details class="arch-panel-extension-service-request-tooltip-more">
<summary>ver mais</summary>
<span>${escapeHtml(value)}</span>
</details>
`;
}
function isLongProblemDescriptionDetail(detail, value) {
const key = normalizeString(detail?.key || detail?.label).replace(/[^A-Z0-9]/g, "");
return key === "PROBLEMDESCRIPTION" && value.length > 220;
}
function getTimeManagementServiceRequestDetails(modal, srNumber) {
const normalizedSrNumber = normalizeString(srNumber);
const matchedSrKey = Object.keys(modal?.activityRequestDetailsBySr || {}).find(
(key) => normalizeString(key) === normalizedSrNumber
);
return (
modal?.activityRequestDetailsBySr?.[srNumber] ||
modal?.activityRequestDetailsBySr?.[matchedSrKey] ||
[]
);
}
function renderTimeManagementSkeleton() { function renderTimeManagementSkeleton() {
return ` return `
<div class="arch-panel-extension-time-management-card" aria-busy="true"> <div class="arch-panel-extension-time-management-card" aria-busy="true">
@@ -335,27 +425,62 @@
fallbackLabelKey, fallbackLabelKey,
metaKey, metaKey,
disabled = false, disabled = false,
optionTooltipHtml = null,
}) { }) {
const safeField = escapeHtml(field); const safeField = escapeHtml(field);
const safeRowId = escapeHtml(rowId || ""); const safeRowId = escapeHtml(rowId || "");
const isDisabled = Boolean(disabled); const isDisabled = Boolean(disabled);
const normalizedSelectedValue = normalizeString(selectedValue); const normalizedSelectedValue = normalizeString(selectedValue);
let hasOptionTooltips = false;
const normalizedOptions = (options || []) const normalizedOptions = (options || [])
.map((item) => ({ .map((item) => {
value: cleanString(item?.[valueKey]), const value = cleanString(item?.[valueKey]);
label: const label =
cleanString(item?.[labelKey]) || cleanString(item?.[labelKey]) ||
cleanString(item?.[fallbackLabelKey]) || cleanString(item?.[fallbackLabelKey]) ||
cleanString(item?.[valueKey]), value;
return {
original: item,
value,
label,
meta: cleanString(item?.[metaKey]), meta: cleanString(item?.[metaKey]),
})) tooltipHtml: "",
.filter((item) => item.value || item.label); };
})
.filter((item) => item.value || item.label)
.map((item) => {
const tooltipHtml =
typeof optionTooltipHtml === "function"
? optionTooltipHtml(item.original || item, item)
: "";
if (tooltipHtml) {
hasOptionTooltips = true;
}
return {
original: item.original,
value: item.value,
label: item.label,
meta: item.meta,
tooltipHtml,
};
});
const menuClassName = [
"arch-panel-extension-combobox-menu",
hasOptionTooltips ? "has-service-request-tooltips" : "",
]
.filter(Boolean)
.join(" ");
return ` return `
<span class="arch-panel-extension-combobox${isDisabled ? " is-disabled" : ""}" data-combobox-field="${safeField}" data-row-id="${safeRowId}"> <span class="arch-panel-extension-combobox${isDisabled ? " is-disabled" : ""}" data-combobox-field="${safeField}" data-row-id="${safeRowId}">
<span class="arch-panel-extension-combobox-value" title="${escapeHtml(value || "")}"> <span class="arch-panel-extension-combobox-value" title="${escapeHtml(value || "")}">
<span class="arch-panel-extension-combobox-value-text">
${escapeHtml(value || placeholder || "")} ${escapeHtml(value || placeholder || "")}
</span> </span>
</span>
<input <input
type="search" type="search"
value="" value=""
@@ -374,7 +499,7 @@
<path d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.16l3.71-3.93a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"></path> <path d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.16l3.71-3.93a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"></path>
</svg> </svg>
</span> </span>
<span class="arch-panel-extension-combobox-menu" role="listbox"> <span class="${menuClassName}" role="listbox">
${ ${
normalizedOptions.length normalizedOptions.length
? normalizedOptions ? normalizedOptions
@@ -383,6 +508,7 @@
const isSelected = const isSelected =
normalizedSelectedValue && normalizedSelectedValue &&
normalizeString(item.value) === normalizedSelectedValue; normalizeString(item.value) === normalizedSelectedValue;
const hasTooltip = Boolean(item.tooltipHtml);
const meta = const meta =
item.meta && normalizeString(item.meta) !== normalizeString(item.label) item.meta && normalizeString(item.meta) !== normalizeString(item.label)
? item.meta ? item.meta
@@ -390,10 +516,9 @@
const optionId = `arch-panel-extension-combobox-${safeField}-${index}`; const optionId = `arch-panel-extension-combobox-${safeField}-${index}`;
return ` return `
<button <span
id="${optionId}" id="${optionId}"
type="button" class="arch-panel-extension-combobox-option${isSelected ? " is-selected" : ""}${hasTooltip ? " has-tooltip" : ""}"
class="arch-panel-extension-combobox-option${isSelected ? " is-selected" : ""}"
data-action="time-management-combobox-option" data-action="time-management-combobox-option"
data-field="${safeField}" data-field="${safeField}"
data-row-id="${safeRowId}" data-row-id="${safeRowId}"
@@ -401,6 +526,7 @@
data-search="${escapeHtml(`${item.label} ${item.value}`.toLowerCase())}" data-search="${escapeHtml(`${item.label} ${item.value}`.toLowerCase())}"
role="option" role="option"
aria-selected="${isSelected ? "true" : "false"}" aria-selected="${isSelected ? "true" : "false"}"
tabindex="-1"
> >
<span class="arch-panel-extension-combobox-option-main"> <span class="arch-panel-extension-combobox-option-main">
${escapeHtml(item.label || item.value)} ${escapeHtml(item.label || item.value)}
@@ -410,7 +536,8 @@
? `<span class="arch-panel-extension-combobox-option-meta">${escapeHtml(meta)}</span>` ? `<span class="arch-panel-extension-combobox-option-meta">${escapeHtml(meta)}</span>`
: "" : ""
} }
</button> ${item.tooltipHtml}
</span>
`; `;
} }
) )