From 551854ee14f908b82b45fb0ff87da1d3816f6b30 Mon Sep 17 00:00:00 2001 From: Paulo Porto Date: Sun, 3 May 2026 23:41:52 -0300 Subject: [PATCH] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20inicial=20do=20Manage?= =?UTF-8?q?=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 483 +++++++- content-script.js | 2943 ++++++++++++++++++++++++++++++++++++++++++++- manifest.json | 67 +- 3 files changed, 3484 insertions(+), 9 deletions(-) diff --git a/background.js b/background.js index 706f3c3..05100c4 100644 --- a/background.js +++ b/background.js @@ -5,12 +5,17 @@ const COMCIP_QUERY_URL = `${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPIElastic/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/latest/custom-actions/queries`; const COMCIP_CLIENT_ID_PROBE_URL = `${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/countries`; +const COMCIP_TIME_ENTRY_APP_URL = + `${COMCIP_ORIGIN}/ic/builder/rt/oalset_timeentrymobile/live/webApps/timeentry/`; +const COMCIP_TIME_ENTRY_CLIENT_ID_PROBE_URL = + `${COMCIP_ORIGIN}/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/lookups?lookupType=SCTA_TASK_TYPE&type=CUSTOM`; +const COMCIP_TIME_ENTRY_APP_VERSION = "version_1774016034953"; const COMCIP_TAB_URL_PATTERN = `${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live*`; const COMCIP_TIME_ENTRY_TAB_URL_PATTERN = `${COMCIP_ORIGIN}/ic/builder/rt/oalset_timeentrymobile/live*`; -chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if ( !message || (message.type !== "ARCH_PANEL_COMCIP_POST" && @@ -24,6 +29,8 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { method: message.method || "POST", payload: message.payload, headers: message.headers, + senderTabId: sender?.tab?.id, + useTimeEntryFrame: Boolean(message.useTimeEntryFrame), }) .then(sendResponse) .catch((error) => { @@ -37,8 +44,47 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { return true; }); -async function requestComcipFromPage({ url, method = "POST", payload, headers = {} }) { +async function requestComcipFromPage({ + url, + method = "POST", + payload, + headers = {}, + senderTabId, + useTimeEntryFrame = false, +}) { const route = getComcipRoute(url); + + if (route.useSenderFrame || useTimeEntryFrame) { + if (!senderTabId) { + throw new Error("Unable to locate the current dashboard tab for COMCIP Time Entry."); + } + + await waitForComcipFrameSessionReady(senderTabId, headers, route); + let response = await executeComcipFetchFromFrame( + senderTabId, + url, + method, + payload, + headers, + route + ); + + if (isAuthorizationFailure(response) || isOutdatedApplicationFailure(response)) { + await delay(1500); + await waitForComcipFrameSessionReady(senderTabId, headers, route); + response = await executeComcipFetchFromFrame( + senderTabId, + url, + method, + payload, + headers, + route + ); + } + + return response; + } + const activeTab = await captureActiveTab(); const target = await getComcipTab(route, activeTab); @@ -84,9 +130,11 @@ function getComcipRoute(requestUrl) { if (normalizedUrl.includes("/ic/builder/rt/oalset_timeentrymobile/live")) { return { - appUrl: normalizedUrl, - clientIdProbeUrl: normalizedUrl, + appUrl: COMCIP_TIME_ENTRY_APP_URL, + clientIdProbeUrl: COMCIP_TIME_ENTRY_CLIENT_ID_PROBE_URL, tabPattern: COMCIP_TIME_ENTRY_TAB_URL_PATTERN, + useSenderFrame: true, + appVersion: COMCIP_TIME_ENTRY_APP_VERSION, }; } @@ -99,7 +147,11 @@ function getComcipRoute(requestUrl) { async function getComcipTab(route, activeTab) { const tabs = await chrome.tabs.query({ url: route.tabPattern }); - const existing = tabs.find((tab) => tab.id && !tab.discarded); + const existing = tabs.find((tab) => { + const tabUrl = String(tab.url || ""); + + return tab.id && !tab.discarded && !tabUrl.includes("/services/"); + }); if (existing) { return { @@ -108,6 +160,12 @@ async function getComcipTab(route, activeTab) { }; } + if (route.requireExistingTab) { + throw new Error( + "A sessao COMCIP Time Entry nao esta pronta. Abra/autentique o Time Entry uma vez e tente novamente." + ); + } + const created = await chrome.tabs.create({ url: route.appUrl, active: false, @@ -188,6 +246,113 @@ async function waitForComcipSessionReady(tabId, headers = {}, route) { throw new Error(`Timed out while waiting for COMCIP authenticated session (${lastStatus}).`); } +async function waitForComcipFrameSessionReady(tabId, headers = {}, route) { + const startedAt = Date.now(); + let lastStatus = ""; + + while (Date.now() - startedAt < 60000) { + const probe = await executeComcipFrameProbe(tabId, headers, route).catch((error) => ({ + ok: false, + status: 0, + error: error instanceof Error ? error.message : String(error), + })); + + if (probe?.ok && probe?.matched) { + return probe; + } + + lastStatus = probe?.status + ? `${probe.status} ${probe.statusText || ""}`.trim() + : probe?.error || "not ready"; + await delay(1000); + } + + throw new Error( + `Timed out while waiting for COMCIP Time Entry frame session (${lastStatus}).` + ); +} + +async function executeComcipFrameProbe(tabId, headers = {}, route) { + const injectionResults = await chrome.scripting.executeScript({ + target: { tabId, allFrames: true }, + world: "MAIN", + args: [ + route?.clientIdProbeUrl || COMCIP_CLIENT_ID_PROBE_URL, + headers || {}, + route?.appVersion || "", + ], + func: async (probeUrl, sourceHeaders, routeAppVersion) => { + function isTimeEntryFrame() { + const href = String(window.location.href || ""); + const frameName = String(window.name || ""); + const isComcipOrigin = href.startsWith( + "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/" + ); + + return ( + (frameName === "arch-panel-extension-comcip-timeentry-frame" && + isComcipOrigin) || + href.includes("oalset_timeentrymobile") || + href.includes("/webApps/timeentry") + ); + } + + if (!isTimeEntryFrame()) { + return { + matched: false, + href: window.location.href, + frameName: window.name || "", + }; + } + + const response = await fetch(probeUrl, { + method: "GET", + credentials: "include", + cache: "no-store", + headers: { + accept: "*/*", + authorization: "Session", + "accept-language": + sourceHeaders["accept-language"] || navigator.language || "pt-BR", + "vb-proxy-header-rest-framework-version": "3", + "x-vb-application-version": + routeAppVersion || sourceHeaders["x-vb-application-version"] || "", + }, + }); + + return { + matched: true, + ok: response.ok, + status: response.status, + statusText: response.statusText, + appBuilderClientId: response.headers.get("x-appbuilder-client-id") || "", + href: window.location.href, + frameName: window.name || "", + }; + }, + }); + + const matchedResult = (injectionResults || []) + .map((item) => item.result) + .find((result) => result?.matched); + + if (!matchedResult) { + const frameHints = (injectionResults || []) + .map((item) => item.result) + .filter(Boolean) + .map((result) => result.href || result.frameName || "unknown") + .slice(0, 5) + .join(" | "); + throw new Error( + `COMCIP Time Entry frame was not found in the dashboard tab.${ + frameHints ? ` Frames: ${frameHints}` : "" + }` + ); + } + + return matchedResult; +} + async function executeComcipProbe(tabId, headers = {}, route) { const [injectionResult] = await chrome.scripting.executeScript({ target: { tabId }, @@ -236,13 +401,15 @@ async function executeComcipFetch(tabId, requestUrl, method, payload, headers, r payload || null, headers || {}, route?.clientIdProbeUrl || COMCIP_CLIENT_ID_PROBE_URL, + route?.appVersion || "", ], func: async ( requestUrl, requestMethod, requestPayload, sourceHeaders, - clientIdProbeUrl + clientIdProbeUrl, + routeAppVersion ) => { const APP_VERSION = "version_1754044416761"; @@ -358,6 +525,294 @@ async function executeComcipFetch(tabId, requestUrl, method, payload, headers, r return injectionResult.result; } +async function executeComcipFetchFromFrame( + tabId, + requestUrl, + method, + payload, + headers, + route +) { + const injectionResults = await chrome.scripting.executeScript({ + target: { tabId, allFrames: true }, + world: "MAIN", + args: [ + requestUrl, + method || "POST", + payload || null, + headers || {}, + route?.clientIdProbeUrl || COMCIP_CLIENT_ID_PROBE_URL, + route?.appVersion || "", + ], + func: async ( + requestUrl, + requestMethod, + requestPayload, + sourceHeaders, + clientIdProbeUrl, + routeAppVersion + ) => { + function isTimeEntryFrame() { + const href = String(window.location.href || ""); + const frameName = String(window.name || ""); + const isComcipOrigin = href.startsWith( + "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/" + ); + + return ( + (frameName === "arch-panel-extension-comcip-timeentry-frame" && + isComcipOrigin) || + href.includes("oalset_timeentrymobile") || + href.includes("/webApps/timeentry") + ); + } + + if (!isTimeEntryFrame()) { + return { + matched: false, + href: window.location.href, + frameName: window.name || "", + }; + } + + function parseResponseText(text) { + if (!text) { + return null; + } + + try { + return JSON.parse(text); + } catch { + return text; + } + } + + function pickResponseHeaders(responseHeaders) { + const safeHeaders = {}; + const exposedHeaders = [ + "content-type", + "x-appbuilder-client-id", + "x-invalid-appbuilder-client-id", + "x-appbuilder-repeat-request", + "vb-proxy-status-actual", + "vb-proxy-version", + ]; + + for (const key of exposedHeaders) { + const value = responseHeaders.get(key); + + if (value) { + safeHeaders[key] = value; + } + } + + return safeHeaders; + } + + async function resolveAppBuilderClientId() { + try { + const response = await fetch(clientIdProbeUrl, { + method: "GET", + credentials: "include", + cache: "no-store", + headers: { + accept: "*/*", + authorization: "Session", + "accept-language": + sourceHeaders["accept-language"] || navigator.language || "pt-BR", + "vb-proxy-header-rest-framework-version": "3", + "x-vb-application-version": + routeAppVersion || sourceHeaders["x-vb-application-version"] || "", + }, + }); + + return response.headers.get("x-appbuilder-client-id") || ""; + } catch { + return ""; + } + } + + const appBuilderClientId = + sourceHeaders["x-appbuilder-client-id"] || (await resolveAppBuilderClientId()); + const requestHeaders = { + accept: "*/*", + authorization: "Session", + "vb-proxy-header-preference": "transient", + "vb-proxy-header-rest-framework-version": "3", + "x-vb-application-version": + routeAppVersion || sourceHeaders["x-vb-application-version"] || "", + "accept-language": + sourceHeaders["accept-language"] || navigator.language || "pt-BR", + }; + + if (appBuilderClientId) { + requestHeaders["x-appbuilder-client-id"] = appBuilderClientId; + } + + const normalizedMethod = String(requestMethod || "POST").toUpperCase(); + const fetchOptions = { + method: normalizedMethod, + credentials: "include", + cache: "no-store", + headers: requestHeaders, + }; + + if (normalizedMethod !== "GET" && normalizedMethod !== "HEAD") { + requestHeaders["content-type"] = "application/json"; + fetchOptions.body = JSON.stringify(requestPayload || {}); + } + + const response = await fetch(requestUrl, fetchOptions); + const text = await response.text(); + + return { + matched: true, + ok: response.ok, + status: response.status, + statusText: response.statusText, + headers: pickResponseHeaders(response.headers), + payload: parseResponseText(text), + debug: { + href: window.location.href, + origin: window.location.origin, + frameName: window.name || "", + headerKeys: Object.keys(requestHeaders), + frameFetch: true, + }, + }; + }, + }); + + const matchedResult = (injectionResults || []) + .map((item) => item.result) + .find((result) => result?.matched); + + if (!matchedResult) { + const frameHints = (injectionResults || []) + .map((item) => item.result) + .filter(Boolean) + .map((result) => result.href || result.frameName || "unknown") + .slice(0, 5) + .join(" | "); + throw new Error( + `COMCIP Time Entry frame did not return a result.${ + frameHints ? ` Frames: ${frameHints}` : "" + }` + ); + } + + return matchedResult; +} + +async function executeComcipDirectFetch( + requestUrl, + method = "GET", + payload = null, + headers = {}, + route +) { + const APP_VERSION = "version_1754044416761"; + + function parseResponseText(text) { + if (!text) { + return null; + } + + try { + return JSON.parse(text); + } catch { + return text; + } + } + + function pickResponseHeaders(responseHeaders) { + const safeHeaders = {}; + const exposedHeaders = [ + "content-type", + "x-appbuilder-client-id", + "x-invalid-appbuilder-client-id", + "x-appbuilder-repeat-request", + "vb-proxy-status-actual", + "vb-proxy-version", + ]; + + for (const key of exposedHeaders) { + const value = responseHeaders.get(key); + + if (value) { + safeHeaders[key] = value; + } + } + + return safeHeaders; + } + + async function resolveAppBuilderClientId() { + try { + const response = await fetch(route?.clientIdProbeUrl || COMCIP_CLIENT_ID_PROBE_URL, { + method: "GET", + credentials: "include", + cache: "no-store", + headers: { + accept: "*/*", + authorization: "Session", + "accept-language": headers["accept-language"] || "pt-BR", + }, + }); + + return response.headers.get("x-appbuilder-client-id") || ""; + } catch { + return ""; + } + } + + const appBuilderClientId = + headers["x-appbuilder-client-id"] || (await resolveAppBuilderClientId()); + const requestHeaders = { + accept: "*/*", + authorization: "Session", + "vb-proxy-header-preference": "transient", + "accept-language": headers["accept-language"] || "pt-BR", + }; + + if (!route?.omitAppVersion && headers["x-vb-application-version"]) { + requestHeaders["x-vb-application-version"] = headers["x-vb-application-version"]; + } + + if (appBuilderClientId) { + requestHeaders["x-appbuilder-client-id"] = appBuilderClientId; + } + + const normalizedMethod = String(method || "GET").toUpperCase(); + const fetchOptions = { + method: normalizedMethod, + credentials: "include", + cache: "no-store", + headers: requestHeaders, + }; + + if (normalizedMethod !== "GET" && normalizedMethod !== "HEAD") { + requestHeaders["content-type"] = "application/json"; + fetchOptions.body = JSON.stringify(payload || {}); + } + + const response = await fetch(requestUrl, fetchOptions); + const text = await response.text(); + + return { + ok: response.ok, + status: response.status, + statusText: response.statusText, + headers: pickResponseHeaders(response.headers), + payload: parseResponseText(text), + debug: { + directFetch: true, + origin: chrome.runtime.getURL(""), + headerKeys: Object.keys(requestHeaders), + }, + }; +} + function isAuthorizationFailure(response) { if (!response) { return false; @@ -370,11 +825,27 @@ function isAuthorizationFailure(response) { return ( response.status === 401 || + response.status === 500 && /vb:\/\/trap\/model\/no_auth/i.test(payloadText) || + /No user authentication for the service/i.test(payloadText) || + /missingAnonymous/i.test(payloadText) || /401 Authorization Required/i.test(payloadText) || /Authorization Required/i.test(payloadText) ); } +function isOutdatedApplicationFailure(response) { + if (!response) { + return false; + } + + const payloadText = + typeof response.payload === "string" + ? response.payload + : JSON.stringify(response.payload || ""); + + return /out-dated version of the application/i.test(payloadText); +} + function delay(milliseconds) { return new Promise((resolve) => { setTimeout(resolve, milliseconds); diff --git a/content-script.js b/content-script.js index 321ae48..94025a1 100644 --- a/content-script.js +++ b/content-script.js @@ -36,6 +36,7 @@ dbVersion: 1, storeName: "datasets", datasetKey: "workload-dashboard", + taskTypeKey: "time-management-task-types", }; const DETAIL_SORT_KEYS = { @@ -79,8 +80,62 @@ "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_semc/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries/summary", resourceCurrentUserUrl: "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers", + timeEntryWeekDetailsUrl: + "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries/weekDetails", + activeServiceRequestsUrl: + "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries/activeServiceRequests", + timeEntriesUrl: + "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/timeEntries", + activityServiceRequestsUrl: + "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/crmRestApiSearchResourcesLatestCustomActions/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests", + taskTypeUrl: + "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/lookups?lookupType=SCTA_TASK_TYPE&type=CUSTOM", + timeEntryAppUrl: + "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/oalset_timeentrymobile/live/webApps/timeentry/", appVersion: "version_1754044416761", }; + const COMCIP_TIME_ENTRY_FRAME_ID = "arch-panel-extension-comcip-timeentry-frame"; + const ACTIVITY_REQUEST_REFERENCE_SR_NUMBERS = [ + "SR0001440593", + "SR0001440475", + "SR0001434642", + "SR0001434671", + "SR0001387564", + "SR0001439267", + "SR0001411935", + "SR0001428065", + "SR0001433200", + "SR0001433217", + "SR0001433030", + "SR0001430407", + "SR0001432386", + "SR0001431923", + "SR0001418256", + "SR0001398520", + "SR0001377462", + "SR0001367044", + "SR0001419998", + "SR0001399117", + "SR0001413684", + "SR0001422541", + "SR0001415397", + "SR0001418535", + "SR0001421040", + "SR0001421074", + "SR0001388866", + "SR0001335148", + "SR0001416756", + "SR0001380511", + "SR0001413965", + "SR0001413598", + "SR0001409121", + "SR0001404828", + "SR0001404549", + "SR0001402376", + "SR0001399121", + "SR0001294405", + "SR0001308472", + ]; const ACTION_TEAM_OPTIONS = [ { @@ -149,6 +204,15 @@ rampComparisonModal: null, forecastUpdateConfirmModal: null, timeEntriesDrawer: null, + timeManagementModal: null, + taskTypeCache: { + promise: null, + }, + timeManagementDatePicker: { + isOpen: false, + year: new Date().getFullYear(), + month: new Date().getMonth(), + }, pendingFocusSelector: "", }; @@ -291,6 +355,9 @@ overlay.hidden = true; overlay.setAttribute("aria-hidden", "true"); overlay.addEventListener("click", handleOverlayClick); + overlay.addEventListener("change", handleOverlayChange); + overlay.addEventListener("input", handleOverlayInput); + overlay.addEventListener("keydown", handleOverlayKeydown); document.body.appendChild(overlay); renderModal(); @@ -309,6 +376,9 @@ if (appState.actionFormModal) { appState.actionFormModal = null; renderModal(); + } else if (appState.timeManagementModal) { + appState.timeManagementModal = null; + renderModal(); } else if (appState.forecastUpdateConfirmModal) { appState.forecastUpdateConfirmModal = null; renderModal(); @@ -333,6 +403,15 @@ const action = actionElement.getAttribute("data-action"); + if ( + action === "time-management-date-change" || + action === "time-management-sr-change" || + action === "time-management-activity-change" || + action === "time-management-task-type-change" + ) { + return; + } + event.preventDefault(); if (action === "close") { @@ -486,6 +565,79 @@ return; } + if (action === "open-time-management") { + void openTimeManagementModal(); + return; + } + + if (action === "close-time-management") { + appState.timeManagementModal = null; + renderModal(); + return; + } + + if (action === "time-management-add-sr") { + addTimeManagementEntryRow("sr"); + return; + } + + if (action === "time-management-add-non-service") { + addTimeManagementEntryRow("non-service"); + return; + } + + if (action === "time-management-remove-row") { + removeTimeManagementEntryRow(actionElement.getAttribute("data-row-id")); + return; + } + + if (action === "open-time-management-date-picker") { + if (!actionElement.matches("button.arch-panel-extension-date-picker-button")) { + return; + } + + toggleTimeManagementDatePicker(); + return; + } + + if (action === "time-management-date-week-prev") { + void shiftTimeManagementDateByWeeks(-1); + return; + } + + if (action === "time-management-date-week-next") { + void shiftTimeManagementDateByWeeks(1); + return; + } + + if (action === "time-management-date-picker-prev") { + shiftTimeManagementDatePickerMonth(-1); + return; + } + + if (action === "time-management-date-picker-next") { + shiftTimeManagementDatePickerMonth(1); + return; + } + + if (action === "time-management-date-picker-select") { + const selectedDate = actionElement.getAttribute("data-date-value"); + + if (selectedDate) { + void selectTimeManagementDate(selectedDate); + } + return; + } + + if (action === "time-management-combobox-option") { + selectTimeManagementComboboxOption( + actionElement.getAttribute("data-field"), + actionElement.getAttribute("data-value"), + actionElement.getAttribute("data-row-id") + ); + return; + } + if (action === "submit-action-form") { const form = actionElement.closest("form"); @@ -511,6 +663,232 @@ } } + function handleOverlayChange(event) { + const target = event.target; + + if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) { + return; + } + + const action = target.getAttribute("data-action"); + + if (action === "time-management-date-change") { + void loadTimeManagementData(target.value); + return; + } + + if (action === "time-management-sr-change") { + updateTimeManagementSelectedSr(target.value, target.getAttribute("data-row-id")); + return; + } + + if (action === "time-management-activity-change") { + updateTimeManagementField( + "selectedActivityValue", + target.value, + target.getAttribute("data-row-id") + ); + return; + } + + if (action === "time-management-task-type-change") { + updateTimeManagementField( + "selectedTaskTypeValue", + target.value, + target.getAttribute("data-row-id") + ); + return; + } + + if (action === "time-management-hours-change") { + updateTimeManagementDayHours( + target.getAttribute("data-row-id"), + target.getAttribute("data-date-key"), + target.value + ); + } + } + + function handleOverlayInput(event) { + const target = event.target; + + if (!(target instanceof HTMLInputElement)) { + return; + } + + const field = target.getAttribute("data-combobox-field"); + + if (!field) { + return; + } + + filterTimeManagementCombobox(target); + } + + function handleOverlayKeydown(event) { + const target = event.target; + + if (!(target instanceof HTMLInputElement)) { + return; + } + + if (target.getAttribute("data-action") === "time-management-hours-change") { + handleTimeManagementHourKeydown(event, target); + return; + } + + const field = target.getAttribute("data-combobox-field"); + + if (!field) { + return; + } + + if (![ + "ArrowDown", + "ArrowUp", + "Home", + "End", + "Enter", + "Escape", + ].includes(event.key)) { + return; + } + + handleTimeManagementComboboxKeydown(event, target); + } + + function handleTimeManagementHourKeydown(event, input) { + if ( + ![ + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "ArrowDown", + "Home", + "End", + "Enter", + ].includes(event.key) + ) { + return; + } + + const nextInput = getNextTimeManagementHourInput(input, event); + + if (!nextInput) { + return; + } + + event.preventDefault(); + nextInput.focus(); + nextInput.select(); + } + + function getNextTimeManagementHourInput(currentInput, event) { + const rows = Array.from( + document.querySelectorAll( + ".arch-panel-extension-time-management-entry-row[data-row-id]" + ) + ); + const currentRow = currentInput.closest( + ".arch-panel-extension-time-management-entry-row" + ); + const rowIndex = rows.indexOf(currentRow); + + if (rowIndex < 0) { + return null; + } + + const currentInputs = getTimeManagementHourInputs(rows[rowIndex]); + const columnIndex = currentInputs.indexOf(currentInput); + + if (columnIndex < 0) { + return null; + } + + let nextRowIndex = rowIndex; + let nextColumnIndex = columnIndex; + + if (event.key === "ArrowLeft") { + nextColumnIndex -= 1; + } else if (event.key === "ArrowRight") { + nextColumnIndex += 1; + } else if (event.key === "ArrowUp") { + nextRowIndex -= 1; + } else if (event.key === "ArrowDown" || event.key === "Enter") { + nextRowIndex += event.shiftKey ? -1 : 1; + } else if (event.key === "Home") { + nextColumnIndex = 0; + } else if (event.key === "End") { + nextColumnIndex = currentInputs.length - 1; + } + + const nextRow = rows[nextRowIndex]; + + if (!nextRow) { + return null; + } + + const nextInputs = getTimeManagementHourInputs(nextRow); + + return nextInputs[nextColumnIndex] || null; + } + + function getTimeManagementHourInputs(row) { + return Array.from( + row.querySelectorAll('input[data-action="time-management-hours-change"]') + ); + } + + function toggleTimeManagementDatePicker() { + const selectedDate = getTimeManagementSelectedDate(); + const pickerDate = parseDatePreservingDateOnly(selectedDate); + const fallback = Number.isNaN(pickerDate.getTime()) ? new Date() : pickerDate; + + appState.timeManagementDatePicker = { + isOpen: !appState.timeManagementDatePicker.isOpen, + year: fallback.getFullYear(), + month: fallback.getMonth(), + }; + renderModal(); + } + + function shiftTimeManagementDatePickerMonth(delta) { + const picker = appState.timeManagementDatePicker; + const nextDate = new Date(picker.year, picker.month + delta, 1); + + appState.timeManagementDatePicker = { + isOpen: true, + year: nextDate.getFullYear(), + month: nextDate.getMonth(), + }; + renderModal(); + } + + async function selectTimeManagementDate(selectedDate) { + appState.timeManagementDatePicker = { + ...appState.timeManagementDatePicker, + isOpen: false, + }; + await loadTimeManagementData(selectedDate); + } + + async function shiftTimeManagementDateByWeeks(delta) { + const selectedDate = getTimeManagementSelectedDate(); + const parsedDate = parseDatePreservingDateOnly(selectedDate); + const baseDate = Number.isNaN(parsedDate.getTime()) ? new Date() : parsedDate; + + baseDate.setDate(baseDate.getDate() + (Number(delta) || 0) * 7); + appState.timeManagementDatePicker = { + ...appState.timeManagementDatePicker, + isOpen: false, + }; + await loadTimeManagementData(getDateInputValue(baseDate)); + } + + function getTimeManagementSelectedDate() { + return appState.timeManagementModal?.selectedDate || getDateInputValue(new Date()); + } + function renderModal() { const overlay = document.getElementById(IDS.overlay); @@ -533,6 +911,7 @@ !appState.actionFormModal && !appState.rampComparisonModal && !appState.forecastUpdateConfirmModal && + !appState.timeManagementModal && requestedFocusSelector ) { const focusTarget = overlay.querySelector(requestedFocusSelector); @@ -700,6 +1079,7 @@ ${renderRampComparisonModal()} ${renderForecastUpdateConfirmModal()} ${renderTimeEntriesDrawer()} + ${renderTimeManagementModal()} `; @@ -1392,6 +1772,17 @@ ) }
+ ${ + isServiceRequestDetail + ? `` + : "" + } + + + + ${renderTimeManagementDatePicker(selectedDate)} + + + + +
+ + ${renderTimeManagementBody(modal)} + +
+ + +
+ + + `; + } + + function renderTimeManagementBody(modal) { + if (modal.status === "loading") { + return renderTimeManagementSkeleton(); + } + + if (modal.status === "error") { + return ` +
+

Erro na requisição

+

${escapeHtml( + modal.errorMessage || "Não foi possível carregar o apontamento." + )}

+
+ `; + } + + const rows = getTimeManagementRows(modal); + + return ` +
+ ${renderTimeManagementHeaderRow(modal.weekDays || [])} +
+ ${rows.map((row, index) => renderTimeManagementEntryRow(modal, row, index)).join("")} +
+
+ `; + } + + function renderTimeManagementHeaderRow(weekDays) { + return ` + + `; + } + + function renderTimeManagementDayHeader(day) { + return ` + + ${escapeHtml(day.dayLabel || "-")} + ${escapeHtml(day.monthLabel || day.date || "-")} + + `; + } + + function renderTimeManagementEntryRow(modal, row, index) { + const rowId = cleanString(row?.id) || `row-${index}`; + const selectedSr = cleanString(row?.selectedSrNumber); + const activityOptions = getTimeManagementActivityOptions(modal, selectedSr); + const selectedSrOption = findTimeManagementServiceRequestByNumber( + modal, + selectedSr + ); + const isNonServiceRequest = isNonServiceRequestSrNumber(selectedSr); + const selectedSrLabel = + selectedSrOption?.srTitle || selectedSrOption?.label || selectedSr; + const selectedActivityValue = + findOptionValue(activityOptions, row?.selectedActivityValue) || + cleanString(row?.selectedActivityValue) || + ""; + const selectedActivityLabel = + findOptionLabel(activityOptions, selectedActivityValue) || selectedActivityValue; + const taskTypeOptions = getTimeManagementTaskTypeOptions(modal, selectedSr); + const selectedTaskTypeValue = + findOptionValue(taskTypeOptions, row?.selectedTaskTypeValue) || + cleanString(row?.selectedTaskTypeValue) || + ""; + const selectedTaskTypeLabel = + findOptionLabel(taskTypeOptions, selectedTaskTypeValue) || selectedTaskTypeValue; + + return ` +
+ + + + ${(modal.weekDays || []) + .map((day) => + renderTimeManagementDayInput(day, rowId, row?.dayHours) + ) + .join("")} + +
+ `; + } + + function renderTimeManagementSkeleton() { + return ` +
+
+ ${["Service Request", "Activity", "Task Type"] + .map( + (label) => + `${label}` + ) + .join("")} + ${Array.from({ length: 7 }, () => ` + + + + + `).join("")} + +
+
+ ${Array.from({ length: 3 }, () => ` +
+ +
+ `).join("")} + ${Array.from({ length: 7 }, () => ` +
+ +
+ `).join("")} + +
+
+ `; + } + + function renderSearchableCombobox({ + field, + rowId, + selectedValue, + value, + placeholder, + options, + valueKey, + labelKey, + fallbackLabelKey, + metaKey, + disabled = false, + }) { + const safeField = escapeHtml(field); + const safeRowId = escapeHtml(rowId || ""); + const isDisabled = Boolean(disabled); + const normalizedSelectedValue = normalizeString(selectedValue); + const normalizedOptions = (options || []) + .map((item) => ({ + value: cleanString(item?.[valueKey]), + label: + cleanString(item?.[labelKey]) || + cleanString(item?.[fallbackLabelKey]) || + cleanString(item?.[valueKey]), + meta: cleanString(item?.[metaKey]), + })) + .filter((item) => item.value || item.label); + + return ` + + + ${escapeHtml(value || placeholder || "")} + + + + + ${ + normalizedOptions.length + ? normalizedOptions + .map( + (item, index) => { + const isSelected = + normalizedSelectedValue && + normalizeString(item.value) === normalizedSelectedValue; + const meta = + item.meta && normalizeString(item.meta) !== normalizeString(item.label) + ? item.meta + : ""; + const optionId = `arch-panel-extension-combobox-${safeField}-${index}`; + + return ` + + `; + } + ) + .join("") + : `No options` + } + + + `; + } + + function renderTimeManagementDatePicker(selectedDate) { + const picker = appState.timeManagementDatePicker; + + if (!picker.isOpen) { + return ""; + } + + const monthStart = new Date(picker.year, picker.month, 1); + const calendarStart = getWeekStartDate(monthStart); + const selectedDateKey = getLocalDateKey(selectedDate); + const todayKey = getLocalDateKey(new Date()); + const monthLabel = new Intl.DateTimeFormat("en-US", { + month: "long", + year: "numeric", + }).format(monthStart); + const days = Array.from({ length: 42 }, (_, index) => { + const date = new Date(calendarStart); + date.setDate(calendarStart.getDate() + index); + const dateKey = getLocalDateKey(date); + const className = [ + "arch-panel-extension-date-picker-day", + date.getMonth() === picker.month ? "" : "is-muted", + dateKey === todayKey ? "is-today" : "", + dateKey === selectedDateKey ? "is-selected" : "", + ] + .filter(Boolean) + .join(" "); + + return ` + + `; + }); + + return ` +
+
+ + ${escapeHtml(monthLabel)} + +
+
+ ${["S", "M", "T", "W", "T", "F", "S"].map((day) => `${day}`).join("")} +
+
+ ${days.join("")} +
+
+ `; + } + + function renderTimeManagementDayInput( + day, + rowId = "", + dayHours = {} + ) { + const dateKey = getTimeManagementDayKey(day); + const value = cleanString(dayHours?.[dateKey]); + + return ` + + `; + } + + function getTimeManagementDayKey(day) { + const parsedDate = parseTimeEntryDate(day?.date); + + if (!Number.isNaN(parsedDate.getTime())) { + return getLocalDateKey(parsedDate); + } + + return cleanString(day?.date); + } + + function renderSelectOptions(items, selectedValue, valueKey, labelKey, emptyLabel) { + const options = []; + + if (!items.length) { + options.push(``); + } + + return options + .concat( + items.map((item) => { + const value = cleanString(item?.[valueKey]); + const label = cleanString(item?.[labelKey]) || value || emptyLabel; + + return ` + + `; + }) + ) + .join(""); + } + function buildRampForecastComparisonRows(workloads) { return (workloads || []) .flatMap((workload) => { @@ -2223,6 +3114,7 @@ appState.rampComparisonModal = null; appState.forecastUpdateConfirmModal = null; appState.timeEntriesDrawer = null; + appState.timeManagementModal = null; if (restoreFocusTarget && restoreFocusTarget.isConnected) { restoreFocusTarget.focus(); @@ -2368,6 +3260,9 @@ const pendingServiceRequests = await fetchPendingServiceRequests().catch( () => [] ); + updateLoadingProgress("Refreshing time management task types", 94); + await refreshTaskTypeCache().catch(() => null); + updateLoadingProgress("Preparing dashboard cache", 96); const finalCustomers = Array.from(customersMap.values()); const exportPayload = createExportPayload( @@ -2491,7 +3386,13 @@ }); } - function sendComcipRequestMessage({ method, url, payload, timeoutMessage }) { + async function sendComcipRequestMessage({ method, url, payload, timeoutMessage }) { + const useTimeEntryFrame = isComcipTimeEntryUrl(url); + + if (useTimeEntryFrame) { + await ensureComcipTimeEntryFrame(); + } + return new Promise((resolve, reject) => { const timeout = window.setTimeout(() => { reject(new Error(timeoutMessage || "Timed out while requesting COMCIP data.")); @@ -2503,6 +3404,7 @@ method, url, payload, + useTimeEntryFrame, headers: { "accept-language": navigator.language || "pt-BR", "x-vb-application-version": COMCIP_REQUEST.appVersion, @@ -2523,6 +3425,61 @@ }); } + function isComcipTimeEntryUrl(url) { + return String(url || "").includes("/ic/builder/rt/oalset_timeentrymobile/live"); + } + + function ensureComcipTimeEntryFrame() { + const existingFrame = document.getElementById(COMCIP_TIME_ENTRY_FRAME_ID); + + if (existingFrame instanceof HTMLIFrameElement) { + existingFrame.name = COMCIP_TIME_ENTRY_FRAME_ID; + return Promise.resolve(existingFrame); + } + + return new Promise((resolve, reject) => { + const frame = document.createElement("iframe"); + const timeout = window.setTimeout(() => { + reject(new Error("Timed out while preparing COMCIP Time Entry session frame.")); + }, 45000); + + frame.id = COMCIP_TIME_ENTRY_FRAME_ID; + frame.name = COMCIP_TIME_ENTRY_FRAME_ID; + frame.title = "COMCIP Time Entry session"; + frame.src = COMCIP_REQUEST.timeEntryAppUrl; + frame.setAttribute("aria-hidden", "true"); + frame.tabIndex = -1; + frame.style.position = "fixed"; + frame.style.width = "1px"; + frame.style.height = "1px"; + frame.style.opacity = "0"; + frame.style.pointerEvents = "none"; + frame.style.border = "0"; + frame.style.left = "-10000px"; + frame.style.top = "0"; + + frame.addEventListener( + "load", + () => { + window.clearTimeout(timeout); + resolve(frame); + }, + { once: true } + ); + frame.addEventListener( + "error", + () => { + window.clearTimeout(timeout); + frame.remove(); + reject(new Error("Unable to load COMCIP Time Entry session frame.")); + }, + { once: true } + ); + + document.documentElement.appendChild(frame); + }); + } + function createComcipServiceRequestPayload() { return { entity: "CRMServiceRequest", @@ -3640,6 +4597,38 @@ }); } + async function readCachedTaskTypePayload() { + return withCacheStore("readonly", (store, resolve, reject) => { + const request = store.get(CACHE.taskTypeKey); + + request.onsuccess = () => { + resolve(request.result?.payload || null); + }; + + request.onerror = () => { + reject(request.error || new Error("Unable to read the cached task types.")); + }; + }); + } + + async function writeCachedTaskTypePayload(payload) { + await withCacheStore("readwrite", (store, resolve, reject) => { + const request = store.put({ + key: CACHE.taskTypeKey, + updatedAt: new Date().toISOString(), + payload, + }); + + request.onsuccess = () => { + resolve(); + }; + + request.onerror = () => { + reject(request.error || new Error("Unable to write the cached task types.")); + }; + }); + } + function buildSummary(exportPayload) { const customers = exportPayload.customers || []; const allWorkloads = customers.flatMap((customer) => customer.workloads || []); @@ -4029,6 +5018,1218 @@ return response; } + async function openTimeManagementModal() { + const selectedDate = getDateInputValue(new Date()); + + appState.timeManagementModal = createTimeManagementLoadingState(selectedDate); + renderModal(); + await loadTimeManagementData(selectedDate); + } + + async function loadTimeManagementData(selectedDate) { + const normalizedDate = cleanString(selectedDate) || getDateInputValue(new Date()); + const requestId = `${Date.now()}-${++requestCounter}`; + + appState.timeManagementDatePicker = { + ...appState.timeManagementDatePicker, + isOpen: false, + }; + appState.timeManagementModal = { + ...(appState.timeManagementModal || {}), + requestId, + selectedDate: normalizedDate, + status: "loading", + errorMessage: "", + }; + renderModal(); + + try { + const resourcePartyId = getCurrentUserResourcePartyId(); + + if (!resourcePartyId) { + throw new Error("resourceCurrentUser.resourcePartyId não está disponível."); + } + + const weekInfoPayload = await fetchInfoWeekRequest(normalizedDate); + const weekDays = normalizeInfoWeekResponse(weekInfoPayload); + const weekId = cleanString(weekDays[0]?.weekId); + + if (!weekId) { + throw new Error("WeekId não encontrado no retorno de infoWeekRequest."); + } + + const timeEntryWeekUrl = buildTimeEntryWeekUrl(resourcePartyId, weekId); + const [srPayload, taskTypePayload, timeEntryWeekPayload] = await Promise.all([ + fetchActiveServiceRequests(resourcePartyId, weekId), + fetchTaskTypeRequest(), + fetchTimeEntryWeek(timeEntryWeekUrl), + ]); + const timeEntryRows = normalizeTimeManagementEntryRows( + timeEntryWeekPayload, + weekDays + ); + const serviceRequests = mergeTimeManagementServiceRequests( + normalizeTimeManagementServiceRequests(srPayload), + timeEntryRows + ); + const activitySrNumbers = getActivityRequestSrNumbers(srPayload, serviceRequests); + let activityRequestUrl = activitySrNumbers.length + ? buildActivityRequestUrl(activitySrNumbers) + : ""; + let activityPayload = activityRequestUrl + ? await fetchComcipPayload(activityRequestUrl, "activityRequest") + : null; + + if (activityRequestUrl && !hasActivityRequestItems(activityPayload)) { + activityRequestUrl = buildActivityRequestUrl( + ACTIVITY_REQUEST_REFERENCE_SR_NUMBERS + ); + activityPayload = await fetchComcipPayload( + activityRequestUrl, + "activityRequest" + ); + } + + const activityOptionsBySr = normalizeActivityRequest( + activityPayload, + serviceRequests + ); + const taskTypes = normalizeTaskTypeRequest(taskTypePayload); + const rows = timeEntryRows.length + ? timeEntryRows + : [createBlankTimeManagementEntryRow()]; + const selectedSrNumber = rows[0]?.selectedSrNumber || ""; + const normalizedRows = rows.map((row, index) => + normalizeTimeManagementEntryRowSelection( + row, + index, + activityOptionsBySr, + taskTypes + ) + ); + + if (appState.timeManagementModal?.requestId !== requestId) { + return; + } + + appState.timeManagementModal = { + requestId, + selectedDate: normalizedDate, + status: "ready", + errorMessage: "", + weekId, + weekDays, + serviceRequests, + selectedSrNumber, + activityOptionsBySr, + activityRequestUrl, + activityRequestPayload: activityPayload, + timeEntryRows: normalizedRows, + selectedActivityValue: + normalizedRows[0]?.selectedActivityValue || "", + taskTypes, + selectedTaskTypeValue: normalizedRows[0]?.selectedTaskTypeValue || "", + }; + renderModal(); + } catch (error) { + if (appState.timeManagementModal?.requestId !== requestId) { + return; + } + + appState.timeManagementModal = { + ...(appState.timeManagementModal || {}), + requestId, + selectedDate: normalizedDate, + status: "error", + errorMessage: getErrorMessage(error), + }; + renderModal(); + } + } + + function createTimeManagementLoadingState(selectedDate) { + return { + requestId: "", + selectedDate, + status: "loading", + errorMessage: "", + weekId: "", + weekDays: [], + serviceRequests: [], + selectedSrNumber: "", + activityOptionsBySr: {}, + activityRequestUrl: "", + activityRequestPayload: null, + selectedActivityValue: "", + taskTypes: [], + selectedTaskTypeValue: "", + timeEntryRows: [], + }; + } + + function getTimeManagementRows(modal) { + return modal.timeEntryRows?.length + ? modal.timeEntryRows + : [createBlankTimeManagementEntryRow()]; + } + + function createBlankTimeManagementEntryRow(srNumber = "") { + return { + id: `row-${Date.now()}-${Math.random().toString(16).slice(2)}`, + selectedSrNumber: cleanString(srNumber), + selectedActivityValue: "", + selectedTaskTypeValue: "", + dayHours: {}, + source: "blank", + }; + } + + function addTimeManagementEntryRow(type) { + if (!appState.timeManagementModal) { + return; + } + + const srNumber = type === "non-service" ? "non-service-sr" : ""; + const nextRows = [ + ...getTimeManagementRows(appState.timeManagementModal), + createBlankTimeManagementEntryRow(srNumber), + ]; + + appState.timeManagementModal = { + ...appState.timeManagementModal, + timeEntryRows: nextRows, + }; + renderModal(); + } + + function removeTimeManagementEntryRow(rowId) { + if (!appState.timeManagementModal) { + return; + } + + const normalizedRowId = cleanString(rowId); + const remainingRows = getTimeManagementRows(appState.timeManagementModal).filter( + (row) => cleanString(row?.id) !== normalizedRowId + ); + const nextRows = remainingRows.length + ? remainingRows + : [createBlankTimeManagementEntryRow()]; + + appState.timeManagementModal = { + ...appState.timeManagementModal, + timeEntryRows: nextRows, + selectedSrNumber: nextRows[0]?.selectedSrNumber || "", + selectedActivityValue: nextRows[0]?.selectedActivityValue || "", + selectedTaskTypeValue: nextRows[0]?.selectedTaskTypeValue || "", + }; + renderModal(); + } + + function getTimeManagementRowById(modal, rowId) { + const rows = getTimeManagementRows(modal); + const normalizedRowId = cleanString(rowId); + + return ( + rows.find((row) => cleanString(row?.id) === normalizedRowId) || + rows[0] || + null + ); + } + + function updateTimeManagementRows(modal, rowId, nextRow) { + const rows = getTimeManagementRows(modal); + const normalizedRowId = cleanString(rowId || nextRow?.id); + + return rows.map((row, index) => { + const isTarget = normalizedRowId + ? cleanString(row?.id) === normalizedRowId + : index === 0; + + return isTarget + ? { + ...row, + ...nextRow, + id: row?.id || nextRow?.id || `row-${index + 1}`, + } + : row; + }); + } + + function updateTimeManagementSelectedSr(srNumber, rowId = "") { + if (!appState.timeManagementModal) { + return; + } + + const targetRow = getTimeManagementRowById(appState.timeManagementModal, rowId); + const selectedServiceRequest = findTimeManagementServiceRequest( + appState.timeManagementModal, + srNumber + ); + const selectedSrNumber = selectedServiceRequest?.srNumber || cleanString(srNumber); + + const nextRow = { + ...(targetRow || createBlankTimeManagementEntryRow()), + selectedSrNumber, + selectedActivityValue: "", + selectedTaskTypeValue: "", + }; + const nextRows = updateTimeManagementRows( + appState.timeManagementModal, + rowId, + nextRow + ); + + appState.timeManagementModal = { + ...appState.timeManagementModal, + timeEntryRows: nextRows, + selectedSrNumber: nextRows[0]?.selectedSrNumber || selectedSrNumber, + selectedActivityValue: nextRows[0]?.selectedActivityValue || "", + selectedTaskTypeValue: nextRows[0]?.selectedTaskTypeValue || "", + }; + renderModal(); + } + + function updateTimeManagementField(fieldName, inputValue, rowId = "") { + if (!appState.timeManagementModal) { + return; + } + + const targetRow = getTimeManagementRowById(appState.timeManagementModal, rowId); + const normalizedInput = cleanString(inputValue); + let value = normalizedInput; + + if (fieldName === "selectedActivityValue") { + const options = getTimeManagementActivityOptions( + appState.timeManagementModal, + targetRow?.selectedSrNumber || appState.timeManagementModal.selectedSrNumber + ); + value = findOptionValue(options, normalizedInput) || normalizedInput; + } + + if (fieldName === "selectedTaskTypeValue") { + const options = getTimeManagementTaskTypeOptions( + appState.timeManagementModal, + targetRow?.selectedSrNumber || appState.timeManagementModal.selectedSrNumber + ); + value = + findOptionValue(options, normalizedInput) || normalizedInput; + } + + const nextRows = updateTimeManagementRows(appState.timeManagementModal, rowId, { + ...(targetRow || createBlankTimeManagementEntryRow()), + [fieldName]: value, + }); + + appState.timeManagementModal = { + ...appState.timeManagementModal, + timeEntryRows: nextRows, + [fieldName]: value, + }; + } + + function updateTimeManagementDayHours(rowId, dateKey, inputValue) { + if (!appState.timeManagementModal) { + return; + } + + const targetRow = getTimeManagementRowById(appState.timeManagementModal, rowId); + const nextRow = { + ...(targetRow || createBlankTimeManagementEntryRow()), + dayHours: { + ...(targetRow?.dayHours || {}), + [cleanString(dateKey)]: cleanString(inputValue), + }, + }; + + appState.timeManagementModal = { + ...appState.timeManagementModal, + timeEntryRows: updateTimeManagementRows( + appState.timeManagementModal, + rowId, + nextRow + ), + }; + } + + function selectTimeManagementComboboxOption(field, value, rowId = "") { + if (field === "sr") { + updateTimeManagementSelectedSr(value || "", rowId); + return; + } + + if (field === "activity") { + updateTimeManagementField("selectedActivityValue", value || "", rowId); + renderModal(); + return; + } + + if (field === "taskType") { + updateTimeManagementField("selectedTaskTypeValue", value || "", rowId); + renderModal(); + } + } + + function filterTimeManagementCombobox(input) { + const query = normalizeString(input.value); + const combobox = input.closest(".arch-panel-extension-combobox"); + + if (!combobox) { + return; + } + + const options = Array.from( + combobox.querySelectorAll(".arch-panel-extension-combobox-option") + ); + + for (const option of options) { + const searchText = normalizeString(option.getAttribute("data-search") || ""); + option.hidden = Boolean(query) && !searchText.includes(query); + } + + setActiveTimeManagementComboboxOption( + combobox, + getVisibleTimeManagementComboboxOptions(combobox).find((option) => + option.classList.contains("is-selected") + ) || getVisibleTimeManagementComboboxOptions(combobox)[0] + ); + } + + function handleTimeManagementComboboxKeydown(event, input) { + const combobox = input.closest(".arch-panel-extension-combobox"); + + if (!combobox) { + return; + } + + if (event.key === "Escape") { + clearActiveTimeManagementComboboxOption(combobox); + input.blur(); + return; + } + + const visibleOptions = getVisibleTimeManagementComboboxOptions(combobox); + + if (!visibleOptions.length) { + return; + } + + event.preventDefault(); + + if (event.key === "Enter") { + const activeOption = + visibleOptions.find((option) => option.classList.contains("is-active")) || + visibleOptions[0]; + activeOption.click(); + return; + } + + const activeIndex = visibleOptions.findIndex((option) => + option.classList.contains("is-active") + ); + const selectedIndex = visibleOptions.findIndex((option) => + option.classList.contains("is-selected") + ); + const baseIndex = activeIndex >= 0 ? activeIndex : selectedIndex; + let nextIndex = 0; + + if (event.key === "ArrowDown") { + nextIndex = baseIndex >= 0 ? baseIndex + 1 : 0; + } else if (event.key === "ArrowUp") { + nextIndex = baseIndex >= 0 ? baseIndex - 1 : visibleOptions.length - 1; + } else if (event.key === "Home") { + nextIndex = 0; + } else if (event.key === "End") { + nextIndex = visibleOptions.length - 1; + } + + if (nextIndex < 0) { + nextIndex = visibleOptions.length - 1; + } else if (nextIndex >= visibleOptions.length) { + nextIndex = 0; + } + + setActiveTimeManagementComboboxOption(combobox, visibleOptions[nextIndex]); + } + + function getVisibleTimeManagementComboboxOptions(combobox) { + return Array.from( + combobox.querySelectorAll(".arch-panel-extension-combobox-option") + ).filter((option) => !option.hidden); + } + + function setActiveTimeManagementComboboxOption(combobox, option) { + clearActiveTimeManagementComboboxOption(combobox); + + if (!option) { + return; + } + + option.classList.add("is-active"); + combobox + .querySelector("input[data-combobox-field]") + ?.setAttribute("aria-activedescendant", option.id || ""); + option.scrollIntoView({ block: "nearest" }); + } + + function clearActiveTimeManagementComboboxOption(combobox) { + combobox + .querySelectorAll(".arch-panel-extension-combobox-option.is-active") + .forEach((option) => option.classList.remove("is-active")); + combobox + .querySelector("input[data-combobox-field]") + ?.removeAttribute("aria-activedescendant"); + } + + async function fetchInfoWeekRequest(selectedDate) { + const dateValue = formatDateForTimeEntryRequest(selectedDate); + const url = `${COMCIP_REQUEST.timeEntryWeekDetailsUrl}?dateValue=${encodeURIComponent( + dateValue + )}`; + + return fetchComcipPayload(url, "infoWeekRequest"); + } + + async function fetchActiveServiceRequests(resourceId, weekId) { + const url = `${COMCIP_REQUEST.activeServiceRequestsUrl}?resourceId=${encodeURIComponent( + resourceId + )}&weekId=${encodeURIComponent(weekId)}`; + + return fetchComcipPayload(url, "sRRequest"); + } + + function buildTimeEntryWeekUrl(resourceId, weekId) { + return `${COMCIP_REQUEST.timeEntriesUrl}?resourceId=${encodeURIComponent( + resourceId + )}&sortBy=createdDate&sortOrder=ASC&weekId=${encodeURIComponent(weekId)}`; + } + + async function fetchTimeEntryWeek(url) { + return fetchComcipPayload(url, "timeEntryWeek"); + } + + async function fetchActivityRequest(srNumbers) { + return fetchComcipPayload(buildActivityRequestUrl(srNumbers), "activityRequest"); + } + + function buildActivityRequestUrl(srNumbers) { + const query = buildServiceRequestActivityQuery(srNumbers); + const fields = + "SrNumber,SRActivities_c,ProblemDescription,AccountPartyUniqueName,ServiceNew_c,OptyOwner_c,OptyName_c,OptyNumber_c,ReportedByPartyUniqueName,StatusCdMeaning,CountryText_c,AssigneePersonName,OptyStatus_c"; + + return [ + COMCIP_REQUEST.activityServiceRequestsUrl, + "?expand=ServiceRequest_SRToInternalSR_Tgt", + `&fields=${encodeActivityRequestParam(fields)}`, + "&limit=200", + "&onlyData=true", + `&q=${encodeActivityRequestParam(query)}`, + ].join(""); + } + + function encodeActivityRequestParam(value) { + return encodeURIComponent(String(value || "")) + .replace(/'/g, "%27") + .replace(/\(/g, "%28") + .replace(/\)/g, "%29"); + } + + async function fetchTaskTypeRequest() { + if (appState.taskTypeCache.promise) { + return appState.taskTypeCache.promise; + } + + appState.taskTypeCache.promise = (async () => { + return readCachedTaskTypePayload().catch(() => null); + })().finally(() => { + appState.taskTypeCache.promise = null; + }); + + return appState.taskTypeCache.promise; + } + + async function refreshTaskTypeCache() { + const payload = await fetchComcipPayload( + COMCIP_REQUEST.taskTypeUrl, + "taskTypeRequest" + ); + await writeCachedTaskTypePayload(payload); + + return payload; + } + + async function fetchComcipPayload(url, requestName) { + const response = await sendComcipGetMessage(url); + + if (!response?.ok) { + const payloadMessage = + typeof response?.payload === "string" + ? cleanString(response.payload) + : firstNonEmptyString([ + response?.payload?.message, + response?.payload?.error, + response?.payload?.title, + response?.payload?.detail, + ]); + + throw new Error( + `${requestName}: ${ + payloadMessage || + response?.error || + `COMCIP request failed (${response?.status || 0}).` + }` + ); + } + + return response.payload; + } + + function hasActivityRequestItems(payload) { + const count = Number(payload?.count); + + if (Number.isFinite(count) && count > 0) { + return true; + } + + return extractArray(payload, ["items"]).length > 0; + } + + function getCurrentUserResourcePartyId() { + const resourceCurrentUser = appState.dataset?.resourceCurrentUser; + const directResourcePartyId = cleanString( + getFieldValue(resourceCurrentUser, [ + "resourcePartyId", + "ResourcePartyId", + "ResourcePartyID", + "partyId", + "PartyId", + ]) + ); + + if (directResourcePartyId) { + return directResourcePartyId; + } + + const candidates = Array.isArray(resourceCurrentUser) + ? resourceCurrentUser + : extractArray(resourceCurrentUser, ["items", "data", "results", "content"]); + const source = candidates[0] || resourceCurrentUser; + + return cleanString( + getFieldValue(source, [ + "resourcePartyId", + "ResourcePartyId", + "ResourcePartyID", + "partyId", + "PartyId", + ]) + ); + } + + function normalizeInfoWeekResponse(payload) { + return extractArray(payload, ["items", "data", "results", "content"]) + .map((item) => { + const rawDate = cleanString(getFieldValue(item, ["Date", "date"])); + const parsedDate = parseTimeEntryDate(rawDate); + + return { + date: rawDate, + weekId: cleanString(getFieldValue(item, ["WeekId", "weekId"])), + dayLabel: Number.isNaN(parsedDate.getTime()) + ? rawDate + : new Intl.DateTimeFormat("en-US", { weekday: "short" }).format(parsedDate), + monthLabel: Number.isNaN(parsedDate.getTime()) + ? rawDate + : new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + }).format(parsedDate), + }; + }) + .filter((item) => item.date || item.weekId); + } + + function normalizeTimeManagementServiceRequests(payload) { + return extractArray(payload, ["items", "data", "results", "content"]) + .map((item) => { + const srNumber = cleanString( + getFieldValue(item, [ + "srNumber", + "SrNumber", + "SRNumber", + "serviceRequestNumber", + "ServiceRequestNumber", + "serviceRequest", + "ServiceRequest", + ]) + ); + const srTitle = cleanString( + getFieldValue(item, ["srTitle", "SrTitle", "SRTitle", "title", "Title"]) + ); + const labelParts = [ + srNumber, + srTitle || + cleanString( + getFieldValue(item, ["problemDescription", "ProblemDescription"]) + ), + ].filter(Boolean); + + return { + srNumber, + srTitle, + label: labelParts.join(" - ") || srNumber, + }; + }) + .filter((item) => item.srNumber); + } + + function mergeTimeManagementServiceRequests(serviceRequests, timeEntryRows) { + const merged = [...(serviceRequests || [])]; + const seen = new Set(merged.map((item) => normalizeString(item.srNumber))); + + for (const row of timeEntryRows || []) { + const srNumber = cleanString(row?.selectedSrNumber); + const key = normalizeString(srNumber); + + if (!srNumber || seen.has(key)) { + continue; + } + + merged.push({ + srNumber, + srTitle: srNumber === "non-service-sr" ? "Non-service request" : srNumber, + label: srNumber === "non-service-sr" ? "Non-service request" : srNumber, + }); + seen.add(key); + } + + return merged; + } + + function normalizeTimeManagementEntryRows(payload, weekDays) { + return extractArray(payload, ["items", "data", "results", "content"]).map( + (item, index) => { + const srNumber = getTimeEntrySrNumber(item) || "non-service-sr"; + const activityValue = getTimeEntryActivity(item); + const taskTypeValue = getTimeEntryTaskType(item); + + return { + id: `entry-${index + 1}`, + selectedSrNumber: srNumber, + selectedActivityValue: activityValue, + selectedTaskTypeValue: taskTypeValue, + dayHours: getTimeEntryWeekDayHours(item, weekDays), + source: "timeEntryWeek", + }; + } + ); + } + + function getTimeEntryWeekDayHours(item, weekDays = []) { + const dayHours = {}; + + for (const day of weekDays || []) { + const dateKey = getTimeManagementDayKey(day); + const weekdayName = getTimeManagementWeekdayName(day); + const value = getTimeEntryWeekdayTaskHours(item, weekdayName); + + if (dateKey) { + dayHours[dateKey] = formatTimeManagementHourValue(value); + } + } + + return dayHours; + } + + function getTimeManagementWeekdayName(day) { + const parsedDate = parseTimeEntryDate(day?.date); + + if (!Number.isNaN(parsedDate.getTime())) { + return new Intl.DateTimeFormat("en-US", { weekday: "long" }) + .format(parsedDate) + .toLowerCase(); + } + + const label = cleanString(day?.dayLabel || day?.date).toLowerCase(); + const aliases = { + sun: "sunday", + sunday: "sunday", + mon: "monday", + monday: "monday", + tue: "tuesday", + tuesday: "tuesday", + wed: "wednesday", + wednesday: "wednesday", + thu: "thursday", + thursday: "thursday", + fri: "friday", + friday: "friday", + sat: "saturday", + saturday: "saturday", + }; + + return aliases[label.slice(0, 3)] || aliases[label] || ""; + } + + function getTimeEntryWeekdayTaskHours(item, weekdayName) { + if (!weekdayName) { + return ""; + } + + const titleCaseWeekday = + weekdayName.charAt(0).toUpperCase() + weekdayName.slice(1); + const compactKey = `${weekdayName}TaskHours`; + const titleKey = `${titleCaseWeekday}TaskHours`; + const snakeKey = `${weekdayName}_task_hours`; + + return getTimeEntryField(item, [ + compactKey, + titleKey, + snakeKey, + `${weekdayName}.taskHours`, + `${titleCaseWeekday}.TaskHours`, + ]); + } + + function formatTimeManagementHourValue(value) { + if (value === undefined || value === null || value === "") { + return ""; + } + + return cleanString(value); + } + + function normalizeTimeManagementEntryRowSelection( + row, + index, + activityOptionsBySr, + taskTypes + ) { + const srNumber = cleanString(row?.selectedSrNumber); + const activitySrKey = Object.keys(activityOptionsBySr || {}).find( + (key) => normalizeString(key) === normalizeString(srNumber) + ); + const activityOptions = + activityOptionsBySr?.[srNumber] || activityOptionsBySr?.[activitySrKey] || []; + const taskTypeOptions = filterTaskTypesForServiceRequest(taskTypes || [], srNumber); + + return { + ...row, + id: cleanString(row?.id) || `entry-${index + 1}`, + selectedSrNumber: srNumber, + selectedActivityValue: + findOptionValue(activityOptions, row?.selectedActivityValue) || + cleanString(row?.selectedActivityValue) || + "", + selectedTaskTypeValue: + findOptionValue(taskTypeOptions, row?.selectedTaskTypeValue) || + cleanString(row?.selectedTaskTypeValue) || + "", + dayHours: row?.dayHours || {}, + }; + } + + function getActivityRequestSrNumbers(srPayload, serviceRequests) { + const normalizedSrNumbers = [ + ...(serviceRequests || []).map((item) => item.srNumber), + ...collectSrNumbers(srPayload), + ]; + const seen = new Set(); + + return normalizedSrNumbers + .map((srNumber) => cleanString(srNumber).toUpperCase()) + .filter((srNumber) => /^SR\d+$/i.test(srNumber)) + .filter((srNumber) => { + const key = normalizeString(srNumber); + + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); + } + + function collectSrNumbers(value, seen = new WeakSet()) { + if (!value) { + return []; + } + + if (typeof value === "string" || typeof value === "number") { + return Array.from(String(value).matchAll(/\bSR\d+\b/gi), (match) => match[0]); + } + + if (typeof value !== "object") { + return []; + } + + if (seen.has(value)) { + return []; + } + + seen.add(value); + + if (Array.isArray(value)) { + return value.flatMap((item) => collectSrNumbers(item, seen)); + } + + return Object.values(value).flatMap((nestedValue) => + collectSrNumbers(nestedValue, seen) + ); + } + + function normalizeActivityRequest(payload, serviceRequests) { + const activityOptionsBySr = {}; + 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); + } + } + + for (const serviceRequest of serviceRequests || []) { + const srNumber = cleanString(serviceRequest?.srNumber); + const srKey = normalizeString(srNumber); + const activityItem = activityItemsBySr.get(srKey); + const options = activityItem ? normalizeSrActivityFieldOptions(activityItem) : []; + + if (srNumber) { + activityOptionsBySr[srNumber] = options; + } + } + + return activityOptionsBySr; + } + + function normalizeSrActivityFieldOptions(item) { + const activityText = firstNonEmptyString([ + getFieldValue(item, ["SRActivities_c", "srActivities_c"]), + getFieldValue(item, [ + "ServiceRequest_SRToInternalSR_Tgt.SRActivities_c", + "ServiceRequest_SRToInternalSR_Tgt.srActivities_c", + ]), + ]); + + if (activityText) { + return parseSemicolonActivityOptions(activityText); + } + + return normalizeActivityOptions(item); + } + + function normalizeActivityOptions(value) { + const recursiveActivityTextValues = collectActivityTextValues(value); + + if (recursiveActivityTextValues.length) { + return parseSemicolonActivityOptions(recursiveActivityTextValues.join(";")); + } + + const directActivityValue = + value && typeof value === "object" && !Array.isArray(value) + ? getFieldValue(value, ["SRActivities_c", "srActivities_c", "activities"]) + : ""; + const nestedActivityValue = + value && typeof value === "object" && !Array.isArray(value) + ? getFieldValue(value, [ + "ServiceRequest_SRToInternalSR_Tgt.SRActivities_c", + "ServiceRequest_SRToInternalSR_Tgt.srActivities_c", + ]) + : ""; + const nestedActivityObject = + value && typeof value === "object" && !Array.isArray(value) + ? getFieldValue(value, ["ServiceRequest_SRToInternalSR_Tgt"]) + : ""; + const explicitActivityText = firstNonEmptyString([ + directActivityValue && typeof directActivityValue !== "object" + ? directActivityValue + : "", + nestedActivityValue && typeof nestedActivityValue !== "object" + ? nestedActivityValue + : "", + ]); + + if (explicitActivityText) { + return parseSemicolonActivityOptions(explicitActivityText); + } + + const activityValue = + directActivityValue || + nestedActivityValue || + nestedActivityObject || + value; + const rawItems = Array.isArray(value) + ? value + : activityValue && typeof activityValue === "object" + ? extractArray(activityValue, [ + "items", + "data", + "results", + "content", + "activities", + "ServiceRequest_SRToInternalSR_Tgt", + ]) + : []; + const textValue = + activityValue && typeof activityValue === "object" + ? "" + : cleanString(activityValue); + const parsedTextItems = parseActivityTextOptions(textValue); + const items = rawItems.length + ? rawItems + : parsedTextItems; + const seen = new Set(); + + return items + .map((item) => { + const optionValue = + item && typeof item === "object" + ? firstNonEmptyString([ + item.activityCode, + item.ActivityCode, + item.code, + item.value, + item.name, + item.label, + ]) + : cleanString(item); + const optionLabel = + item && typeof item === "object" + ? firstNonEmptyString([ + item.activityName, + item.ActivityName, + item.meaning, + item.label, + item.name, + optionValue, + ]) + : optionValue; + const normalizedValue = cleanString(optionValue); + const normalizedLabel = cleanString(optionLabel); + + if (!normalizedValue || seen.has(normalizedValue)) { + return null; + } + + seen.add(normalizedValue); + return { + value: normalizedValue, + label: normalizedLabel || normalizedValue, + }; + }) + .filter(Boolean); + } + + function collectActivityTextValues(value, seen = new WeakSet()) { + if (!value || typeof value !== "object") { + return []; + } + + if (seen.has(value)) { + return []; + } + + seen.add(value); + + if (Array.isArray(value)) { + return value.flatMap((item) => collectActivityTextValues(item, seen)); + } + + const values = []; + const activityFieldNames = new Set([ + normalizeString("SRActivities_c"), + normalizeString("srActivities_c"), + normalizeString("activities"), + ]); + + for (const [key, nestedValue] of Object.entries(value)) { + if ( + activityFieldNames.has(normalizeString(key)) && + typeof nestedValue !== "object" + ) { + const text = cleanString(nestedValue); + + if (text) { + values.push(text); + } + } else if (nestedValue && typeof nestedValue === "object") { + values.push(...collectActivityTextValues(nestedValue, seen)); + } + } + + return values; + } + + function parseSemicolonActivityOptions(value) { + const seen = new Set(); + + return cleanString(value) + .split(";") + .map((item) => cleanString(item)) + .filter(Boolean) + .map((item) => { + const normalizedItem = normalizeString(item); + + if (seen.has(normalizedItem)) { + return null; + } + + seen.add(normalizedItem); + return { + value: item, + label: item, + }; + }) + .filter(Boolean); + } + + function parseActivityTextOptions(value) { + if (!value) { + return []; + } + + try { + const parsed = JSON.parse(value); + + if (Array.isArray(parsed)) { + return parsed; + } + + if (parsed && typeof parsed === "object") { + return extractArray(parsed, ["items", "data", "results", "content", "activities"]); + } + } catch { + // Continue with delimiter based parsing. + } + + return value + .split(/[;|\n]/) + .map((item) => item.trim()) + .filter(Boolean); + } + + function normalizeTaskTypeRequest(payload) { + return extractArray(payload, ["items", "data", "results", "content"]) + .map((item) => { + const value = cleanString( + getFieldValue(item, ["lookUpCode_c", "LookUpCode_c", "lookupCode"]) + ); + const label = cleanString( + getFieldValue(item, [ + "lookUpMeaning_c", + "LookUpMeaning_c", + "lookupMeaning", + "meaning", + ]) + ); + const tags = cleanString( + getFieldValue(item, ["tags_c", "Tags_c", "TAGS_C", "tags", "Tags"]) + ); + + return { + value, + label: label || value, + tags, + }; + }) + .filter((item) => item.value); + } + + function getTimeManagementTaskTypeOptions(modal, srNumber) { + const options = filterTaskTypesForServiceRequest(modal.taskTypes || [], srNumber); + + return options.length + ? options + : [{ value: "", label: "No task types available", tags: "" }]; + } + + function filterTaskTypesForServiceRequest(taskTypes, srNumber) { + const expectedTag = isServiceRequestSrNumber(srNumber) ? "SERVICE" : "NON_SERVICE"; + return (taskTypes || []).filter((item) => { + const tags = normalizeString(item?.tags); + + return tags === normalizeString(expectedTag); + }); + } + + function isServiceRequestSrNumber(srNumber) { + return /^SR\d+$/i.test(cleanString(srNumber)); + } + + function isNonServiceRequestSrNumber(srNumber) { + return normalizeString(srNumber) === normalizeString("non-service-sr"); + } + + function getTimeManagementActivityOptions(modal, srNumber) { + const normalizedSrNumber = normalizeString(srNumber); + const matchedSrKey = Object.keys(modal.activityOptionsBySr || {}).find( + (key) => normalizeString(key) === normalizedSrNumber + ); + const options = + modal.activityOptionsBySr?.[srNumber] || + modal.activityOptionsBySr?.[matchedSrKey] || + []; + + return options.length + ? options + : [{ value: "", label: "No activities available" }]; + } + + function findTimeManagementServiceRequest(modal, inputValue) { + const normalizedInput = normalizeString(inputValue); + + return (modal.serviceRequests || []).find((item) => { + return ( + normalizeString(item.srNumber) === normalizedInput || + normalizeString(item.label) === normalizedInput || + normalizeString(item.srTitle) === normalizedInput + ); + }); + } + + function findTimeManagementServiceRequestByNumber(modal, srNumber) { + const normalizedSrNumber = normalizeString(srNumber); + + return (modal.serviceRequests || []).find( + (item) => normalizeString(item.srNumber) === normalizedSrNumber + ); + } + + function findOptionValue(options, inputValue) { + const normalizedInput = normalizeString(inputValue); + const option = (options || []).find((item) => { + return ( + normalizeString(item.value) === normalizedInput || + normalizeString(item.label) === normalizedInput + ); + }); + + return option?.value || ""; + } + + function findOptionLabel(options, value) { + const normalizedValue = normalizeString(value); + const option = (options || []).find( + (item) => normalizeString(item.value) === normalizedValue + ); + + return option?.label || ""; + } + + function buildServiceRequestActivityQuery(srNumbers) { + const values = [...(srNumbers || [])] + .map((srNumber) => cleanString(srNumber)) + .filter(Boolean) + .map((srNumber) => `'${srNumber.replace(/'/g, "''")}'`) + .concat("'non-service-sr'") + .join(","); + + return `SrNumber in (${values})`; + } + function closeDetailModal() { if (appState.detailModal?.scope === "calendar-day" && appState.detailModal.value) { appState.pendingFocusSelector = getCalendarDayFocusSelector( @@ -4038,6 +6239,7 @@ appState.detailModal = null; appState.timeEntriesDrawer = null; + appState.timeManagementModal = null; appState.rampComparisonModal = null; appState.forecastUpdateConfirmModal = null; } @@ -6553,6 +8755,610 @@ padding: 18px; } + .arch-panel-extension-time-management-dialog { + width: min(1760px, calc(100vw - 20px)); + max-height: calc(100vh - 88px); + display: flex; + flex-direction: column; + gap: 14px; + overflow: visible; + border: 1px solid var(--wb-border); + border-radius: 10px; + background: var(--wb-panel); + box-shadow: var(--wb-shadow); + padding: 18px; + } + + .arch-panel-extension-time-management-toolbar, + .arch-panel-extension-time-management-card { + border: 1px solid var(--wb-border); + border-radius: 8px; + background: var(--wb-panel-soft); + padding: 14px; + } + + .arch-panel-extension-time-management-card { + display: grid; + gap: 16px; + min-height: 0; + overflow: visible; + } + + .arch-panel-extension-time-management-card.is-error { + border-color: rgba(217, 79, 43, 0.42); + } + + .arch-panel-extension-date-picker { + position: relative; + display: inline-flex; + align-items: center; + gap: 5px; + width: auto; + } + + .arch-panel-extension-date-input-shell { + position: relative; + display: inline-flex; + align-items: center; + } + + .arch-panel-extension-date-picker-button { + position: absolute; + top: 50%; + right: 11px; + z-index: 1; + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border: 0; + border-radius: 5px; + background: transparent; + color: var(--wb-text-muted); + cursor: pointer; + font-size: 0; + line-height: 1; + padding: 0; + transform: translateY(-50%); + } + + .arch-panel-extension-date-step-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 32px; + border: 1px solid var(--wb-border); + border-radius: 7px; + background: var(--wb-panel); + color: var(--wb-text-muted); + cursor: pointer; + padding: 0; + } + + .arch-panel-extension-date-step-button svg { + width: 16px; + height: 16px; + fill: currentColor; + pointer-events: none; + } + + .arch-panel-extension-date-step-button:hover { + border-color: var(--wb-border-strong); + background: var(--wb-row-hover); + color: var(--wb-text); + } + + .arch-panel-extension-date-picker-button svg { + width: 16px; + height: 16px; + fill: currentColor; + pointer-events: none; + } + + .arch-panel-extension-date-picker-button:hover { + background: var(--wb-row-hover); + color: var(--wb-text); + } + + .arch-panel-extension-date-picker-icon { + display: none; + position: absolute; + left: 11px; + z-index: 1; + color: var(--wb-text-muted); + font-size: 14px; + pointer-events: none; + } + + .arch-panel-extension-date-picker input { + width: 118px; + min-height: 32px; + padding-left: 12px; + padding-right: 38px; + } + + .arch-panel-extension-form-field .arch-panel-extension-date-picker input { + min-height: 32px; + padding: 0 38px 0 12px; + } + + .arch-panel-extension-date-picker input[readonly] { + cursor: default; + } + + .arch-panel-extension-date-picker-popover { + position: absolute; + top: calc(100% + 8px); + left: 0; + z-index: 6; + width: 292px; + border: 1px solid var(--wb-border); + border-radius: 10px; + background: var(--wb-panel); + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.22); + padding: 12px; + } + + .arch-panel-extension-date-picker-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 10px; + } + + .arch-panel-extension-date-picker-head strong { + color: var(--wb-text); + font-size: 13px; + } + + .arch-panel-extension-date-picker-weekdays, + .arch-panel-extension-date-picker-grid { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 4px; + } + + .arch-panel-extension-date-picker-weekdays { + margin-bottom: 5px; + color: var(--wb-text-muted); + font-size: 10px; + font-weight: 800; + text-align: center; + } + + .arch-panel-extension-date-picker-day { + appearance: none; + min-width: 0; + height: 32px; + border: 1px solid transparent; + border-radius: 7px; + background: transparent; + color: var(--wb-text); + font: inherit; + font-size: 12px; + font-weight: 700; + cursor: pointer; + } + + .arch-panel-extension-date-picker-day:hover { + border-color: var(--wb-border-strong); + background: var(--wb-row-hover); + } + + .arch-panel-extension-date-picker-day.is-muted { + color: var(--wb-text-muted); + opacity: 0.55; + } + + .arch-panel-extension-date-picker-day.is-today { + border-color: #0b5cab; + } + + .arch-panel-extension-date-picker-day.is-selected { + border-color: #0b5cab; + background: #0b5cab; + color: #ffffff; + } + + .arch-panel-extension-time-management-select-grid { + display: grid; + grid-template-columns: minmax(240px, 1.15fr) minmax(220px, 1fr) minmax(180px, 0.8fr); + gap: 12px; + } + + .arch-panel-extension-time-management-rows { + display: grid; + gap: 10px; + max-height: min(52vh, 520px); + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + padding-right: 6px; + scrollbar-gutter: stable; + } + + .arch-panel-extension-time-management-rows:focus-within { + overflow: visible; + } + + .arch-panel-extension-time-management-header-row { + display: grid; + grid-template-columns: + minmax(420px, 3.8fr) + minmax(150px, 0.95fr) + minmax(138px, 0.82fr) + repeat(7, minmax(52px, 0.28fr)) + 34px; + align-items: end; + gap: 6px; + } + + .arch-panel-extension-time-management-column-label { + color: var(--wb-text-secondary); + font-size: 12px; + font-weight: 600; + } + + .arch-panel-extension-time-management-entry-row { + display: grid; + grid-template-columns: + minmax(420px, 3.8fr) + minmax(150px, 0.95fr) + minmax(138px, 0.82fr) + repeat(7, minmax(52px, 0.28fr)) + 34px; + align-items: start; + gap: 6px; + } + + .arch-panel-extension-time-management-entry-row.is-compact { + align-items: center; + } + + .arch-panel-extension-time-management-entry-row.is-compact .arch-panel-extension-form-field { + gap: 0; + } + + .arch-panel-extension-combobox { + position: relative; + display: block; + } + + .arch-panel-extension-combobox input { + min-height: 40px; + padding-left: 12px; + padding-right: 38px; + border-radius: 8px; + background: color-mix(in srgb, var(--wb-panel-soft) 86%, var(--wb-panel)); + color: transparent; + transition: + border-color 140ms ease, + box-shadow 140ms ease, + background 140ms ease; + } + + .arch-panel-extension-combobox input::placeholder { + color: transparent; + } + + .arch-panel-extension-combobox:focus-within input::placeholder { + color: var(--wb-text-muted); + } + + .arch-panel-extension-combobox-value { + position: absolute; + top: 50%; + right: 38px; + left: 12px; + z-index: 1; + overflow: hidden; + color: var(--wb-text); + font-size: 12px; + font-weight: 700; + line-height: 1.25; + pointer-events: none; + text-overflow: ellipsis; + transform: translateY(-50%); + white-space: nowrap; + } + + .arch-panel-extension-combobox:focus-within .arch-panel-extension-combobox-value { + display: none; + } + + .arch-panel-extension-combobox.is-disabled { + opacity: 0.72; + } + + .arch-panel-extension-combobox.is-disabled .arch-panel-extension-combobox-value, + .arch-panel-extension-combobox.is-disabled .arch-panel-extension-combobox-arrow { + color: var(--wb-text-muted); + } + + .arch-panel-extension-combobox.is-disabled input { + cursor: not-allowed; + } + + .arch-panel-extension-combobox.is-disabled:focus-within .arch-panel-extension-combobox-menu { + display: none; + } + + .arch-panel-extension-combobox input:focus { + border-color: #0b5cab; + background: var(--wb-panel); + box-shadow: 0 0 0 3px rgba(11, 92, 171, 0.16); + color: var(--wb-text); + outline: none; + } + + .arch-panel-extension-combobox-arrow { + position: absolute; + top: 50%; + right: 12px; + z-index: 1; + display: inline-flex; + width: 16px; + height: 16px; + color: var(--wb-text-muted); + pointer-events: none; + transform: translateY(-50%); + } + + .arch-panel-extension-combobox-arrow svg { + width: 100%; + height: 100%; + fill: currentColor; + } + + .arch-panel-extension-combobox-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + z-index: 10000; + display: none; + max-height: 260px; + overflow: auto; + border: 1px solid var(--wb-border-strong); + border-radius: 10px; + background: var(--wb-panel); + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.22); + padding: 6px; + } + + .arch-panel-extension-combobox:focus-within .arch-panel-extension-combobox-menu { + display: grid; + gap: 4px; + } + + .arch-panel-extension-combobox-option { + appearance: none; + display: grid; + gap: 2px; + position: relative; + width: 100%; + min-height: 38px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--wb-text); + cursor: pointer; + font: inherit; + line-height: 1.25; + padding: 8px 10px; + text-align: left; + } + + .arch-panel-extension-combobox-option:hover, + .arch-panel-extension-combobox-option:focus, + .arch-panel-extension-combobox-option.is-active { + outline: none; + background: var(--wb-row-hover); + } + + .arch-panel-extension-combobox-option.is-active { + box-shadow: inset 3px 0 0 #0b5cab; + } + + .arch-panel-extension-combobox-option.is-selected { + background: rgba(11, 92, 171, 0.12); + color: var(--wb-text); + padding-right: 32px; + } + + .arch-panel-extension-combobox-option.is-selected::after { + content: "✓"; + position: absolute; + top: 50%; + right: 11px; + color: #0b5cab; + font-size: 14px; + font-weight: 800; + transform: translateY(-50%); + } + + .arch-panel-extension-dialog[data-theme="dark"] .arch-panel-extension-combobox-option.is-selected::after { + color: #9ed4f0; + } + + .arch-panel-extension-combobox-option-main { + color: var(--wb-text); + font-size: 12px; + font-weight: 700; + } + + .arch-panel-extension-combobox-option-meta { + color: var(--wb-text-muted); + font-size: 11px; + font-weight: 600; + } + + .arch-panel-extension-combobox-option[hidden] { + display: none; + } + + .arch-panel-extension-combobox-empty { + color: var(--wb-text-muted); + font-size: 12px; + padding: 8px 9px; + } + + .arch-panel-extension-time-management-days { + display: grid; + grid-template-columns: repeat(7, minmax(110px, 1fr)); + gap: 8px; + } + + .arch-panel-extension-time-management-day { + display: grid; + gap: 6px; + padding: 7px; + border: 1px solid var(--wb-border); + border-radius: 8px; + background: var(--wb-panel); + } + + .arch-panel-extension-time-management-day-head { + display: grid; + gap: 1px; + } + + .arch-panel-extension-time-management-day-head strong { + color: var(--wb-text); + font-size: 11px; + } + + .arch-panel-extension-time-management-day-head span { + color: var(--wb-text-muted); + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + } + + .arch-panel-extension-time-management-day input { + width: 100%; + min-height: 30px; + border: 1px solid var(--wb-border); + border-radius: 6px; + background: var(--wb-panel-soft); + color: var(--wb-text); + font: inherit; + font-size: 12px; + padding: 0 6px; + text-align: right; + } + + .arch-panel-extension-time-management-delete { + appearance: none; + align-self: center; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 1px solid var(--wb-border); + border-radius: 7px; + background: var(--wb-panel); + color: var(--wb-text-muted); + cursor: pointer; + padding: 0; + } + + .arch-panel-extension-time-management-delete:hover, + .arch-panel-extension-time-management-delete:focus-visible { + border-color: rgba(217, 79, 43, 0.52); + background: rgba(217, 79, 43, 0.12); + color: #d94f2b; + outline: none; + } + + .arch-panel-extension-time-management-delete svg { + width: 16px; + height: 16px; + fill: currentColor; + pointer-events: none; + } + + .arch-panel-extension-time-management-request { + border-top: 1px solid var(--wb-border); + padding-top: 10px; + } + + .arch-panel-extension-time-management-request summary { + color: var(--wb-link); + cursor: pointer; + font-size: 12px; + font-weight: 700; + } + + .arch-panel-extension-time-management-request p { + margin: 10px 0 4px; + color: var(--wb-text-secondary); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.02em; + text-transform: uppercase; + } + + .arch-panel-extension-time-management-request pre { + max-height: 220px; + margin: 8px 0 0; + overflow: auto; + border: 1px solid var(--wb-border); + border-radius: 7px; + background: var(--wb-panel); + color: var(--wb-text-secondary); + font-size: 11px; + line-height: 1.45; + padding: 10px; + white-space: pre-wrap; + word-break: break-all; + } + + .arch-panel-extension-skeleton { + display: block; + overflow: hidden; + border-radius: 6px; + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--wb-border) 70%, transparent) 0%, + color-mix(in srgb, var(--wb-panel) 70%, #ffffff) 48%, + color-mix(in srgb, var(--wb-border) 70%, transparent) 100% + ); + background-size: 220% 100%; + animation: arch-panel-extension-skeleton 1.15s ease-in-out infinite; + } + + .arch-panel-extension-skeleton-control { + height: 38px; + } + + .arch-panel-extension-skeleton-title { + width: 62%; + height: 14px; + } + + .arch-panel-extension-skeleton-text { + width: 46%; + height: 11px; + } + + .arch-panel-extension-skeleton-input { + height: 34px; + } + + @keyframes arch-panel-extension-skeleton { + from { + background-position: 120% 0; + } + to { + background-position: -120% 0; + } + } + .arch-panel-extension-confirm-dialog { width: min(560px, calc(100vw - 32px)); max-height: calc(100vh - 120px); @@ -6599,6 +9405,7 @@ .arch-panel-extension-detail-actions { display: flex; + align-items: center; justify-content: flex-end; gap: 10px; position: sticky; @@ -6608,6 +9415,13 @@ background: linear-gradient(180deg, rgba(23, 30, 39, 0), var(--wb-panel) 35%); } + .arch-panel-extension-time-management-footer-actions { + display: flex; + align-items: center; + gap: 8px; + margin-right: auto; + } + .arch-panel-extension-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -6638,6 +9452,22 @@ font: inherit; } + .arch-panel-extension-form-field .arch-panel-extension-combobox input { + min-height: 40px; + padding: 0 38px 0 12px; + border-radius: 8px; + background: color-mix(in srgb, var(--wb-panel-soft) 86%, var(--wb-panel)); + color: transparent; + } + + .arch-panel-extension-form-field .arch-panel-extension-combobox input:focus { + border-color: #0b5cab; + background: var(--wb-panel); + box-shadow: 0 0 0 3px rgba(11, 92, 171, 0.16); + color: var(--wb-text); + outline: none; + } + .arch-panel-extension-form-error { margin: 12px 0 0; color: #d94f2b; @@ -7371,6 +10201,66 @@ }).format(date); } + function getDateInputValue(value) { + const date = parseDatePreservingDateOnly(value); + + if (Number.isNaN(date.getTime())) { + return ""; + } + + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + + return `${year}-${month}-${day}`; + } + + function formatDateForTimeEntryRequest(value) { + const date = parseDatePreservingDateOnly(value); + + if (Number.isNaN(date.getTime())) { + return ""; + } + + const day = String(date.getDate()).padStart(2, "0"); + const month = new Intl.DateTimeFormat("en-US", { month: "short" }) + .format(date) + .toUpperCase(); + const year = date.getFullYear(); + + return `${day}-${month}-${year}`; + } + + function parseTimeEntryDate(value) { + const text = cleanString(value); + const match = text.match(/^(\d{1,2})-([A-Z]{3})-(\d{4})$/i); + + if (!match) { + return parseDatePreservingDateOnly(text); + } + + const monthIndex = [ + "JAN", + "FEB", + "MAR", + "APR", + "MAY", + "JUN", + "JUL", + "AUG", + "SEP", + "OCT", + "NOV", + "DEC", + ].indexOf(match[2].toUpperCase()); + + if (monthIndex < 0) { + return new Date(Number.NaN); + } + + return new Date(Number(match[3]), monthIndex, Number(match[1])); + } + function formatDateTime(value) { if (!value) { return ""; @@ -7529,6 +10419,20 @@ return flattenedMatch ? cleanString(flattenedMatch[1]) : ""; } + function getTimeEntrySrNumber(item) { + return cleanString( + getTimeEntryField(item, [ + "srNumber", + "SrNumber", + "SRNumber", + "serviceRequestNumber", + "ServiceRequestNumber", + "serviceRequest.srNumber", + "ServiceRequest.SrNumber", + ]) + ); + } + function getTimeEntryActivity(item) { return cleanString( getTimeEntryField(item, [ @@ -7564,6 +10468,37 @@ ); } + function getTimeEntryDateKey(item, weekDays = []) { + const rawDate = cleanString( + getTimeEntryField(item, [ + "Date", + "date", + "dateValue", + "DateValue", + "timeEntryDate", + "TimeEntryDate", + "entryDate", + "EntryDate", + ]) + ); + + if (!rawDate) { + return weekDays.length === 1 ? getTimeManagementDayKey(weekDays[0]) : ""; + } + + const parsedDate = parseTimeEntryDate(rawDate); + + if (!Number.isNaN(parsedDate.getTime())) { + return getLocalDateKey(parsedDate); + } + + const matchingDay = (weekDays || []).find((day) => { + return normalizeString(day?.date) === normalizeString(rawDate); + }); + + return matchingDay ? getTimeManagementDayKey(matchingDay) : ""; + } + function getTimeEntryField(item, keys) { const directValue = getFieldValue(item, keys); @@ -7688,6 +10623,12 @@ return; } + if (appState.timeManagementModal) { + appState.timeManagementModal = null; + renderModal(); + return; + } + if (appState.forecastUpdateConfirmModal) { appState.forecastUpdateConfirmModal = null; renderModal(); diff --git a/manifest.json b/manifest.json index 5def2ab..0191420 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Arch Panel Injector", - "version": "1.0.11", + "version": "1.0.73", "description": "Insere o botao Arch panel no header do Oracle Workload Workbench.", "permissions": [ "cookies", @@ -9,7 +9,8 @@ "tabs" ], "host_permissions": [ - "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*" + "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*", + "https://spa.oracle.com/*" ], "background": { "service_worker": "background.js" @@ -48,3 +49,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +