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
+ ? `
`
+ : ""
+ }
+ `;
+ }
+
+ 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 `
+
+
+
+ ${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 || "")}
+
+
+
+
+
+
+
+ `;
+ }
+
+ 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 `
+
+ ${date.getDate()}
+
+ `;
+ });
+
+ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+