diff --git a/README.md b/README.md
index c22a4b2..bae3af1 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,156 @@
-# Arch Panel Injector
+# Arch Panel
-Extensao Chrome Manifest V3 que adiciona o botao `Arch panel` ao header da pagina Oracle:
+Extensao Chrome Manifest V3 que injeta o Arch Panel em paginas Oracle usadas pelo time de arquitetura, incluindo Workload Workbench e HCM FuseWelcome.
-`https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench`
+## Entry points
-## O que ela faz
+- `manifest.json`: fonte oficial da versao, permissoes e content scripts.
+- `src/shared/config.js`: configuracao centralizada, constantes, IDs, endpoints e chaves de cache.
+- `src/shared/utils.js`: helpers compartilhados de strings, numeros, datas, DOM, escaping e extração de payloads.
+- `src/background/config.js`: configuracao especifica do service worker e URLs usadas pelos proxies de background.
+- `src/content/styles.css`: estilos da UI injetados pelo Manifest V3.
+- `src/content/shell-view.js`: shell visual injetado na pagina, incluindo tema, cores, alvo de insercao e scroll lock.
+- `src/content/templates.js`: templates estaticos de DOM usados pelo content script.
+- `src/content/hcm-page.js`: integracao com FuseWelcome, injecao do tile HCM e captura do token Worklist via iframe.
+- `src/content/modal-events.js`: controller de eventos delegados da modal, incluindo clicks, inputs e navegacao por teclado.
+- `src/content/workbench-controller.js`: controller de Workbench, dataset, refresh, detalhes, actions, forecast e calendario.
+- `src/content/manage-time-controller.js`: controller da modal Manage Time, incluindo carregamento, diff, foco, validacao e gravacao.
+- `src/content/export-html.js`: geracao do relatorio HTML standalone baixado pelo usuario.
+- `src/repositories/workbench-repository.js`: acesso e normalizacao de dados do Workload Workbench e Worklist user API.
+- `src/repositories/comcip-repository.js`: acesso e normalizacao de dados COMCIP, Manage Time e Service Requests.
+- `src/repositories/cache-repository.js`: persistencia dos payloads de dashboard, task types e token Worklist no IndexedDB.
+- `src/domain/ramp-forecast.js`: regras puras de calculo de ramp, delta de forecast e payload de atualizacao de forecast.
+- `src/domain/workload-dataset.js`: regras puras de normalizacao de workloads, snapshots, export payloads e indicadores do dashboard.
+- `src/domain/workload-calendar.js`: regras puras para calendario mensal/semanal de inicio dos workloads.
+- `src/domain/time-management.js`: regras puras de diff, assinatura, payload, validacao e datas do Manage time.
+- `src/domain/time-management-data.js`: normalizacao de payloads, options e DTOs usados pela modal Manage time.
+- `src/services/storage-service.js`: infraestrutura generica de IndexedDB.
+- `src/services/chrome-service.js`: wrappers Promise-based para APIs do Chrome usadas pelo content script.
+- `src/services/preferences-service.js`: persistencia local de preferencias visuais e ultima data selecionada no Manage Time.
+- `src/services/transport-service.js`: camada de transporte, page bridge, iframes autenticados e requests via background.
+- `src/views/render-helpers.js`: helpers reutilizaveis de apresentacao para tabelas, links e resumo do header.
+- `content-script.js`: entrypoint principal da UI e orquestracao de estado, eventos e requests.
+- `page-bridge.js`: bridge executada no contexto da pagina SPA para capturar e reutilizar headers autorizados.
+- `background.js`: service worker responsavel por proxy de requests, descoberta de frames e execucao em frames autenticados.
+- `tools/bump-extension-version.ps1`: script obrigatorio para atualizar a versao da extensao.
-- roda apenas na pagina alvo do Oracle Workload Workbench
-- injeta o botao imediatamente a esquerda de `.oj-oal-ux-global-header-content`
-- reaproveita as cores e a tipografia do proprio header para manter compatibilidade visual
-- observa mudancas da SPA para recolocar o botao se o layout for remontado
+## Estrutura atual
-## Como instalar no Chrome
+```text
+src/
+ background/
+ config.js
+ content/
+ export-html.js
+ hcm-page.js
+ manage-time-controller.js
+ modal-events.js
+ shell-view.js
+ styles.css
+ templates.js
+ workbench-controller.js
+ domain/
+ ramp-forecast.js
+ workload-dataset.js
+ workload-calendar.js
+ time-management.js
+ time-management-data.js
+ repositories/
+ cache-repository.js
+ comcip-repository.js
+ workbench-repository.js
+ views/
+ dashboard-view.js
+ manage-time-view.js
+ render-helpers.js
+ workbench-modals-view.js
+ services/
+ chrome-service.js
+ preferences-service.js
+ storage-service.js
+ transport-service.js
+ shared/
+ config.js
+ utils.js
+background.js
+content-script.js
+page-bridge.js
+manifest.json
+```
-1. Abra `chrome://extensions`
-2. Ative `Developer mode`
-3. Clique em `Load unpacked`
-4. Selecione a pasta `C:\Codex\Projects\arch-central-extension-browser`
+O `manifest.json` carrega `src/shared/config.js`, services, repositories, domain modules e views antes de `content-script.js`. Esses arquivos expõem namespaces em `globalThis` para manter compatibilidade com content scripts classicos do Manifest V3 sem exigir bundler.
-## Arquivos principais
+O CSS principal fica em `src/content/styles.css` e e injetado diretamente pela propriedade `css` do content script no `manifest.json`.
-- `manifest.json`: define a extensao e o carregamento do content script
-- `content-script.js`: valida a URL, encontra o header e injeta o botao
+Renderizadores maiores devem ficar em `src/views/`. O dashboard principal fica em `src/views/dashboard-view.js`, os modais de Workbench ficam em `src/views/workbench-modals-view.js` e a modal Manage Time fica em `src/views/manage-time-view.js`, mantendo `content-script.js` como orquestrador.
+
+Responsabilidades de shell injetado na pagina, como tema derivado do header Oracle, posicionamento do botao e bloqueio de scroll durante modal aberta, ficam em `src/content/shell-view.js`. A integracao especifica do HCM FuseWelcome fica em `src/content/hcm-page.js`.
+
+Regras de negocio puras devem ficar em `src/domain/`. Essa camada nao deve manipular DOM, IndexedDB, Chrome APIs ou executar requests; ela recebe dependencias simples por factory e retorna funcoes deterministicas.
+
+Requests, cache especifico da extensao e normalizadores de payload de APIs externas devem ficar em `src/repositories/`. Essa camada monta URLs, centraliza contratos de endpoint e transforma payloads remotos ou persistidos em DTOs consumidos pelo orquestrador.
+
+## Fluxos principais
+
+- Workload dashboard: carrega usuario, customers, workloads, actions e service requests.
+- HCM tile: adiciona o tile `Arch Panel` dentro de `yourapps_groupNode_my_information` em `FuseWelcome`.
+- Worklist token: captura o token do iframe Worklist SaasUI e persiste em IndexedDB para resolver o usuario atual.
+- SPA Workbench iframe: cria um iframe oculto do Workload Workbench para obter contexto autenticado e executar requests da origem `spa.oracle.com`.
+- COMCIP Time Entry iframe: cria um iframe oculto do Manage Time para requests dependentes da sessao COMCIP.
+- Manage time: permite apontar horas, detectar criacao/alteracao/remocao e gravar via APIs de time entries.
+
+## IndexedDB
+
+Banco: `arch-panel-extension-db`
+
+Store: `datasets`
+
+Chaves conhecidas:
+
+- `workload-dashboard`: ultimo dataset do dashboard.
+- `time-management-task-types`: cache de task types atualizado apenas em refresh data.
+- `worklist-saasui-token`: token atual extraido do iframe Worklist.
+
+## Seguranca operacional
+
+- A extensao deve declarar apenas permissoes realmente usadas no `manifest.json`.
+- A bridge de pagina deve trocar mensagens apenas com origins explicitamente permitidos.
+- Tokens e headers de autorizacao nao devem ser logados.
+- Exports HTML devem escapar JSON inline e valores renderizados.
+
+## Validacao local
+
+```powershell
+node --check content-script.js
+node --check background.js
+node --check page-bridge.js
+node --check src/background/config.js
+node --check src/content/export-html.js
+node --check src/content/hcm-page.js
+node --check src/content/manage-time-controller.js
+node --check src/content/modal-events.js
+node --check src/content/shell-view.js
+node --check src/content/templates.js
+node --check src/content/workbench-controller.js
+node --check src/repositories/workbench-repository.js
+node --check src/repositories/comcip-repository.js
+node --check src/repositories/cache-repository.js
+node --check src/domain/ramp-forecast.js
+node --check src/domain/workload-dataset.js
+node --check src/domain/workload-calendar.js
+node --check src/domain/time-management.js
+node --check src/domain/time-management-data.js
+node --check src/views/dashboard-view.js
+node --check src/views/workbench-modals-view.js
+node --check src/views/manage-time-view.js
+node --check src/views/render-helpers.js
+node --check src/shared/config.js
+node --check src/shared/utils.js
+node --check src/services/storage-service.js
+node --check src/services/chrome-service.js
+node --check src/services/preferences-service.js
+node --check src/services/transport-service.js
+```
## Controle de versao
diff --git a/background.js b/background.js
index c46212d..43c1602 100644
--- a/background.js
+++ b/background.js
@@ -1,24 +1,20 @@
-const COMCIP_ORIGIN = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
-const COMCIP_APP_URL =
- `${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live/webApps/Dashboard/?page=shell&shell=main&main=service-requests-detailed-view`;
-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*`;
-const SPA_WORKBENCH_FRAME_URL =
- "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench";
-const SPA_WORKBENCH_FRAME_ID = "arch-panel-extension-spa-frame";
-const SPA_WORKBENCH_FRAME_HOST_ID = "arch-panel-extension-spa-frame-host";
-const SPA_FRAME_DNR_RULE_ID = 92001;
+importScripts("src/background/config.js");
+
+const {
+ COMCIP_ORIGIN,
+ COMCIP_APP_URL,
+ COMCIP_QUERY_URL,
+ COMCIP_CLIENT_ID_PROBE_URL,
+ COMCIP_TIME_ENTRY_APP_URL,
+ COMCIP_TIME_ENTRY_CLIENT_ID_PROBE_URL,
+ COMCIP_TIME_ENTRY_APP_VERSION,
+ COMCIP_TAB_URL_PATTERN,
+ COMCIP_TIME_ENTRY_TAB_URL_PATTERN,
+ SPA_WORKBENCH_FRAME_URL,
+ SPA_WORKBENCH_FRAME_ID,
+ SPA_WORKBENCH_FRAME_HOST_ID,
+ SPA_FRAME_DNR_RULE_ID,
+} = globalThis.ArchPanelBackgroundConfig;
void installSpaFrameHeaderRules();
diff --git a/content-script.js b/content-script.js
index 1eef7ec..f34f3c7 100644
--- a/content-script.js
+++ b/content-script.js
@@ -1,205 +1,150 @@
(() => {
- const TARGET_ORIGIN = "https://spa.oracle.com";
- const TARGET_PATHNAME =
- "/oalcrm/web/api/g2m-consumer-application/ui/index.html";
- const TARGET_PARAM = "ojr";
- const TARGET_PARAM_VALUE = "workload_workbench";
- const TARGET_APP_URL =
- "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench";
- const HCM_ORIGIN = "https://eeho.fa.us2.oraclecloud.com";
- const HCM_WELCOME_PATHNAME = "/hcmUI/faces/FuseWelcome";
- const HCM_MY_INFORMATION_APPS_GROUP_ID = "yourapps_groupNode_my_information";
- const WORKLIST_ORIGIN = "https://worklist.oracle.com";
- const WORKLIST_SAASUI_PATHNAME = "/oalapp/pub/worklist/saasui/index.html";
- const USER_BUTTON_LABEL = "[[displayUser().fullName]]";
- const ALLOWED_STAGE_TYPES = ["SQL", "Pipeline", "Upside", "Forecast"];
+ const {
+ TARGET_ORIGIN,
+ TARGET_PATHNAME,
+ TARGET_PARAM,
+ TARGET_PARAM_VALUE,
+ TARGET_APP_URL,
+ HCM_ORIGIN,
+ HCM_WELCOME_PATHNAME,
+ HCM_MY_INFORMATION_APPS_GROUP_ID,
+ WORKLIST_ORIGIN,
+ WORKLIST_SAASUI_PATHNAME,
+ USER_BUTTON_LABEL,
+ ALLOWED_STAGE_TYPES,
+ SELECTORS,
+ IDS,
+ CLASSES,
+ STORAGE_KEYS,
+ CACHE,
+ DETAIL_SORT_KEYS,
+ BRIDGE,
+ API_ENDPOINTS,
+ COMCIP_REQUEST,
+ COMCIP_TIME_ENTRY_FRAME_ID,
+ ACTION_TEAM_OPTIONS,
+ ACTION_COMPLEXITY_OPTIONS,
+ } = globalThis.ArchPanelConfig;
- const SELECTORS = {
- header: ".oj-oal-ux-global-header.oj-oal-ux-global-header-pillar-cx",
- functions: ".oj-flex.oj-oal-ux-global-header-functions",
- };
-
- const IDS = {
- mount: "arch-panel-extension-mount",
- button: "arch-panel-extension-button",
- style: "arch-panel-extension-style",
- overlay: "arch-panel-extension-overlay",
- dialog: "arch-panel-extension-dialog",
- hcmTile: "arch-panel-extension-hcm-tile",
- spaFrame: "arch-panel-extension-spa-frame",
- spaFrameHost: "arch-panel-extension-spa-frame-host",
- };
-
- const CLASSES = {
- modalOpen: "arch-panel-extension-modal-open",
- overlayOpen: "is-open",
- };
-
- const STORAGE_KEYS = {
- theme: "arch-panel-extension-theme",
- sidebarCollapsed: "arch-panel-extension-sidebar-collapsed",
- calendarView: "arch-panel-extension-calendar-view",
- timeManagementSelectedDate: "arch-panel-extension-time-management-selected-date",
- };
-
- const CACHE = {
- dbName: "arch-panel-extension-db",
- dbVersion: 1,
- storeName: "datasets",
- datasetKey: "workload-dashboard",
- taskTypeKey: "time-management-task-types",
- worklistTokenKey: "worklist-saasui-token",
- };
-
- const DETAIL_SORT_KEYS = {
- opportunityNumber: "opportunityNumber",
- customerName: "customerName",
- workload: "workload",
- rampMonths: "rampMonths",
- adjustedACR: "adjustedACR",
- opportunityForecastTypeGroup: "opportunityForecastTypeGroup",
- hasSR: "hasSR",
- hasAction: "hasAction",
- };
-
- const BRIDGE = {
- source: "arch-panel-extension",
- fetchType: "ARCH_PANEL_FETCH",
- resultType: "ARCH_PANEL_FETCH_RESULT",
- headersType: "ARCH_PANEL_HEADERS",
- headersResultType: "ARCH_PANEL_HEADERS_RESULT",
- workbenchAuthHeadersType: "ARCH_PANEL_WORKBENCH_AUTH_HEADERS",
- readyType: "ARCH_PANEL_BRIDGE_READY",
- scriptId: "arch-panel-extension-page-bridge",
- };
-
- const API_ENDPOINTS = {
- currentUser:
- "/oalcrm/web/api/identity-management/users/current",
- worklistCurrentUser:
- "https://worklist.oracle.com/oalapp/pub/wlp/svc/user-service/user",
- customerSummary:
- "/oalcrm/web/api/provider-proxy/wwb-provider/service/elastic/customers/listSummary",
- customerWorkloads:
- "/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/customers",
- workloadActions:
- "/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/actions",
- workloadServiceRequests:
- "/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/workloads",
- };
-
- const COMCIP_REQUEST = {
- url:
- "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/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",
- timeEntriesSummaryUrl:
- "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 = [
- {
- value: "DM_AUTONOMOUS",
- label: "Data Platform - Autonomous Database",
- },
- {
- value: "CI_COMPUTE",
- label: "Cloud Infrastructure - Compute including HPC",
- },
- {
- value: "APP_DEV_AI_CLOUD_SERVICES",
- label: "App Dev - AI Cloud Services",
- },
- {
- value: "DEV_CLOUD_NATIVE_APP",
- label: "App Dev - Cloud Native",
- },
- {
- value: "CI_BIG_COMPUTE_HPC",
- label: "Cloud Infrastructure - AI Infra/GPU",
- },
- {
- value: "CI_MULTI_CLOUD",
- label: "Cloud Infrastructure - Multi-Cloud",
- },
- {
- value: "CI_STORAGE",
- label: "Cloud Infrastructure - Storage",
- },
- {
- value: "CI_VMWARE",
- label: "Cloud Infrastructure - VMware",
- },
- {
- value: "DALH_DESIGN",
- label: "Data Platform - Analytical Data Platform/Lakehouse",
- },
- {
- value: "DATA_INTEGRATION",
- label: "Data Platform - Data Integration",
- },
- {
- value: "DM_UPGRADE",
- label: "Data Platform - Modernise",
- },
- ];
-
- const ACTION_COMPLEXITY_OPTIONS = ["LOW", "MEDIUM", "HIGH", "VERY_HIGH"];
+ const storageService = globalThis.ArchPanelStorageService;
+ const chromeService = globalThis.ArchPanelChromeService;
+ const {
+ normalizeText,
+ cleanString,
+ firstNonEmptyString,
+ normalizeString,
+ toNumber,
+ toIsoDateString,
+ extractArray,
+ mapWithConcurrency,
+ getFieldValue,
+ flattenDisplayRecord,
+ isVisible,
+ formatWholeNumber,
+ formatCurrency,
+ formatHours,
+ formatWeekNumber,
+ formatOperationalWeekLabel,
+ formatPercent,
+ formatProgressPercent,
+ formatDate,
+ getDateInputValue,
+ formatDateInputDisplay,
+ formatDateForTimeEntryRequest,
+ parseTimeEntryDate,
+ formatDateTime,
+ getLocalDateKey,
+ getOperationalWeekInfo,
+ getCalendarRowWeekDate,
+ parseDatePreservingDateOnly,
+ getErrorMessage,
+ escapeHtml,
+ cssEscape,
+ } = globalThis.ArchPanelUtils;
+ const {
+ getStoredTheme,
+ setStoredTheme,
+ getStoredSidebarCollapsed,
+ setStoredSidebarCollapsed,
+ getStoredCalendarView,
+ setStoredCalendarView,
+ getStoredTimeManagementSelectedDate,
+ setStoredTimeManagementSelectedDate,
+ } = globalThis.ArchPanelPreferencesService.create({
+ STORAGE_KEYS,
+ getDateInputValue,
+ parseDatePreservingDateOnly,
+ });
+ const {
+ applyDialogTheme,
+ ensureStyles,
+ applyTheme,
+ findUserAnchor,
+ resolveInsertionTarget,
+ lockScroll,
+ unlockScroll,
+ } = globalThis.ArchPanelShellView.create({
+ IDS,
+ CLASSES,
+ USER_BUTTON_LABEL,
+ normalizeText,
+ isVisible,
+ });
+ const {
+ normalizeWorkload,
+ isWonWorkload,
+ normalizeTeamValue,
+ teamContainsUser,
+ stripTransientFields,
+ createExportPayload,
+ createDatasetSnapshot,
+ normalizeCachedExportPayload,
+ } = globalThis.ArchPanelWorkloadDatasetDomain.create({
+ ALLOWED_STAGE_TYPES,
+ cleanString,
+ firstNonEmptyString,
+ normalizeString,
+ toNumber,
+ extractArray,
+ formatDateTime,
+ normalizePendingServiceRequests: (...args) =>
+ comcipRepository.normalizePendingServiceRequests(...args),
+ });
+ const {
+ buildWorkloadStartCalendar,
+ getWeekStartDate,
+ getCalendarAnchorDate,
+ } = globalThis.ArchPanelWorkloadCalendarDomain.create({
+ getLocalDateKey,
+ getOperationalWeekInfo,
+ getCalendarRowWeekDate,
+ formatOperationalWeekLabel,
+ formatDate,
+ parseDatePreservingDateOnly,
+ });
+ const {
+ readCachedDataset,
+ writeCachedDataset,
+ readCachedTaskTypePayload,
+ writeCachedTaskTypePayload,
+ writeCachedWorklistToken,
+ readCachedWorklistToken,
+ } = globalThis.ArchPanelCacheRepository.create({
+ CACHE,
+ storageService,
+ normalizeCachedExportPayload,
+ cleanString,
+ });
+ const hcmPage = globalThis.ArchPanelHcmPage.create({
+ HCM_MY_INFORMATION_APPS_GROUP_ID,
+ WORKLIST_ORIGIN,
+ WORKLIST_SAASUI_PATHNAME,
+ IDS,
+ cleanString,
+ hcmTileTemplate: globalThis.ArchPanelTemplates.hcmTile,
+ onOpenArchPanel: openModal,
+ writeCachedWorklistToken,
+ });
const appState = {
theme: getStoredTheme(),
@@ -231,16 +176,61 @@
month: new Date().getMonth(),
},
pendingFocusSelector: "",
+ spaFrameLifecycleIntervalId: null,
+ lastSpaFrameLifecycleError: "",
};
let syncFrame = 0;
let restoreFocusTarget = null;
let closeTimer = 0;
- let requestCounter = 0;
- let bridgeReadyPromise = null;
- let cacheHydrationPromise = null;
- let lastPersistedWorklistIframeUrl = "";
- const pendingBridgeRequests = new Map();
+ const {
+ ensurePageBridge,
+ fetchJson,
+ sendComcipRequestMessage,
+ ensureSpaFrame,
+ handleBridgeMessage,
+ } = globalThis.ArchPanelTransportService.create({
+ TARGET_ORIGIN,
+ TARGET_APP_URL,
+ IDS,
+ BRIDGE,
+ COMCIP_REQUEST,
+ COMCIP_TIME_ENTRY_FRAME_ID,
+ chromeService,
+ cleanString,
+ firstNonEmptyString,
+ isHcmWelcomePage,
+ getSessionAuthHeaders: () => appState.sessionAuthHeaders,
+ setSessionAuthHeaders: (headers) => {
+ appState.sessionAuthHeaders = headers;
+ },
+ });
+ const createExportHtml = globalThis.ArchPanelExportHtml.createExportHtml;
+ const {
+ renderServiceRequestDetailTable,
+ renderSortButton,
+ renderOpportunityLink,
+ renderWorkloadLink,
+ getHeaderSummary,
+ } = globalThis.ArchPanelRenderHelpers.create({
+ appState,
+ escapeHtml,
+ cleanString,
+ formatWholeNumber,
+ formatCurrency,
+ formatDate,
+ formatHours,
+ });
+ let workbenchController = null;
+ let modalEvents = null;
+
+ function getSortedDetailItems() {
+ return workbenchController?.getSortedDetailItems() || [];
+ }
+
+ function findWorkloadById(workloadId) {
+ return workbenchController?.findWorkloadById(workloadId) || null;
+ }
function isTargetPage() {
const currentUrl = new URL(window.location.href);
@@ -280,9 +270,9 @@
mount?.remove();
ensureStyles();
ensureModal();
- syncHcmTile();
+ hcmPage.syncHcmTile();
ensureSpaFrame();
- syncWorklistIframeToken();
+ hcmPage.syncWorklistIframeToken();
return;
}
@@ -334,107 +324,6 @@
}
}
- function syncHcmTile() {
- const appsGroup = document.getElementById(HCM_MY_INFORMATION_APPS_GROUP_ID);
- const addTile = appsGroup?.querySelector(".flat-grid-cell.flat-grid-cell-addicon");
-
- if (!addTile?.parentElement) {
- document.getElementById(IDS.hcmTile)?.remove();
- return;
- }
-
- const tile = document.getElementById(IDS.hcmTile) || createHcmTile();
-
- if (tile.parentElement !== addTile.parentElement || tile.nextElementSibling !== addTile) {
- addTile.parentElement.insertBefore(tile, addTile);
- }
- }
-
- function syncWorklistIframeToken(options = {}) {
- if (!isHcmWelcomePage()) {
- return null;
- }
-
- for (const frame of document.querySelectorAll("iframe")) {
- const tokenPayload = extractWorklistIframeToken(frame);
-
- if (tokenPayload?.token) {
- persistWorklistIframeToken(tokenPayload, options);
- return tokenPayload;
- }
- }
-
- return null;
- }
-
- function extractWorklistIframeToken(frame) {
- const rawSrc = cleanString(frame?.getAttribute?.("src") || frame?.src);
-
- if (!rawSrc) {
- return null;
- }
-
- try {
- const url = new URL(rawSrc, window.location.href);
- const token = cleanString(url.searchParams.get("token"));
-
- if (
- url.origin !== WORKLIST_ORIGIN ||
- url.pathname !== WORKLIST_SAASUI_PATHNAME ||
- !token
- ) {
- return null;
- }
-
- return {
- token,
- url: url.toString(),
- };
- } catch {
- return null;
- }
- }
-
- function persistWorklistIframeToken(tokenPayload, options = {}) {
- if (!options.force && tokenPayload.url === lastPersistedWorklistIframeUrl) {
- return;
- }
-
- lastPersistedWorklistIframeUrl = tokenPayload.url;
- void writeCachedWorklistToken(tokenPayload);
- }
-
- function createHcmTile() {
- const template = document.createElement("template");
- template.innerHTML = `
-
- `;
- const tile = template.content.firstElementChild;
-
- tile.addEventListener("click", openHcmArchPanelTile);
- tile.addEventListener("keydown", (event) => {
- if (event.key === "Enter" || event.key === " ") {
- openHcmArchPanelTile(event);
- }
- });
- tile.tabIndex = 0;
- tile.setAttribute("role", "button");
- tile.setAttribute("aria-label", "Open Arch Panel");
-
- return tile;
- }
-
- function openHcmArchPanelTile(event) {
- event.preventDefault();
- event.stopPropagation();
- openModal();
- }
-
function createMount() {
const mount = document.createElement("div");
mount.id = IDS.mount;
@@ -453,37 +342,6 @@
return mount;
}
- function ensurePageBridge() {
- if (bridgeReadyPromise) {
- return bridgeReadyPromise;
- }
-
- bridgeReadyPromise = new Promise((resolve) => {
- const existingScript = document.getElementById(BRIDGE.scriptId);
-
- if (existingScript) {
- resolve();
- return;
- }
-
- const script = document.createElement("script");
- script.id = BRIDGE.scriptId;
- script.src = chrome.runtime.getURL("page-bridge.js");
- script.async = false;
- script.onload = () => {
- script.remove();
- resolve();
- };
- script.onerror = () => {
- resolve();
- };
-
- (document.head || document.documentElement).appendChild(script);
- });
-
- return bridgeReadyPromise;
- }
-
function ensureModal() {
const existingOverlay = document.getElementById(IDS.overlay);
@@ -496,10 +354,10 @@
overlay.className = "arch-panel-extension-overlay";
overlay.hidden = true;
overlay.setAttribute("aria-hidden", "true");
- overlay.addEventListener("click", handleOverlayClick);
- overlay.addEventListener("change", handleOverlayChange);
- overlay.addEventListener("input", handleOverlayInput);
- overlay.addEventListener("keydown", handleOverlayKeydown);
+ overlay.addEventListener("click", modalEvents.handleOverlayClick);
+ overlay.addEventListener("change", modalEvents.handleOverlayChange);
+ overlay.addEventListener("input", modalEvents.handleOverlayInput);
+ overlay.addEventListener("keydown", modalEvents.handleOverlayKeydown);
document.body.appendChild(overlay);
renderModal();
@@ -507,535 +365,6 @@
return overlay;
}
- function handleOverlayClick(event) {
- const overlay = document.getElementById(IDS.overlay);
-
- if (!overlay) {
- return;
- }
-
- if (event.target === overlay) {
- if (appState.actionFormModal) {
- appState.actionFormModal = null;
- renderModal();
- } else if (appState.timeManagementModal) {
- appState.timeManagementModal = null;
- renderModal();
- } else if (appState.forecastUpdateConfirmModal) {
- appState.forecastUpdateConfirmModal = null;
- renderModal();
- } else if (appState.rampComparisonModal) {
- appState.rampComparisonModal = null;
- renderModal();
- } else if (appState.detailModal) {
- closeDetailModal();
- renderModal();
- } else {
- closeModal();
- }
-
- return;
- }
-
- const actionElement = event.target.closest("[data-action]");
-
- if (!actionElement) {
- return;
- }
-
- 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") {
- closeModal();
- return;
- }
-
- if (action === "toggle-theme") {
- toggleTheme();
- return;
- }
-
- if (action === "toggle-sidebar") {
- toggleSidebar();
- return;
- }
-
- if (action === "refresh") {
- void refreshData();
- return;
- }
-
- if (action === "calendar-prev") {
- shiftCalendarPeriod(-1);
- return;
- }
-
- if (action === "calendar-next") {
- shiftCalendarPeriod(1);
- return;
- }
-
- if (action === "calendar-today") {
- goToCurrentCalendarPeriod();
- return;
- }
-
- if (action === "calendar-view") {
- const calendarView = actionElement.getAttribute("data-calendar-view");
-
- if (calendarView) {
- setCalendarView(calendarView);
- }
- return;
- }
-
- if (action === "download-json") {
- if (appState.dataset) {
- downloadFile(
- "arch-panel-data.json",
- JSON.stringify(appState.dataset.exportPayload, null, 2),
- "application/json"
- );
- }
-
- return;
- }
-
- if (action === "download-html") {
- if (appState.dataset) {
- downloadFile(
- "arch-panel-dashboard.html",
- createExportHtml(appState.dataset),
- "text/html"
- );
- }
-
- return;
- }
-
- if (action === "open-detail") {
- const detailScope = actionElement.getAttribute("data-detail-scope");
- const detailValue = actionElement.getAttribute("data-detail-value");
-
- openDetailModal(detailScope, detailValue);
- return;
- }
-
- if (action === "open-action-form") {
- const workloadId = actionElement.getAttribute("data-workload-id");
-
- if (workloadId) {
- openActionFormModal(workloadId);
- }
-
- return;
- }
-
- if (action === "open-ramp-comparison") {
- const workloadId = actionElement.getAttribute("data-workload-id");
-
- if (workloadId) {
- openRampComparisonModal(workloadId);
- }
-
- return;
- }
-
- if (action === "close-ramp-comparison") {
- appState.forecastUpdateConfirmModal = null;
- appState.rampComparisonModal = null;
- renderModal();
- return;
- }
-
- if (action === "open-forecast-update-confirm") {
- openForecastUpdateConfirmModal();
- return;
- }
-
- if (action === "close-forecast-update-confirm") {
- appState.forecastUpdateConfirmModal = null;
- renderModal();
- return;
- }
-
- if (action === "submit-forecast-update") {
- void submitForecastUpdate();
- return;
- }
-
- if (action === "close-action-form") {
- appState.actionFormModal = null;
- renderModal();
- return;
- }
-
- if (action === "open-time-entries") {
- const srNumber = actionElement.getAttribute("data-sr-number");
-
- if (srNumber) {
- void openTimeEntriesDrawer(srNumber);
- }
-
- return;
- }
-
- if (action === "close-time-entries") {
- appState.timeEntriesDrawer = null;
- renderModal();
- return;
- }
-
- if (action === "retry-time-entries") {
- const srNumber = appState.timeEntriesDrawer?.srNumber;
-
- if (srNumber) {
- void openTimeEntriesDrawer(srNumber);
- }
-
- 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-save") {
- void saveTimeManagementChanges();
- 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");
-
- if (form instanceof HTMLFormElement) {
- void submitActionForm(form);
- }
-
- return;
- }
-
- if (action === "close-detail") {
- closeDetailModal();
- renderModal();
- return;
- }
-
- if (action === "sort-detail") {
- const sortKey = actionElement.getAttribute("data-sort-key");
-
- if (sortKey) {
- toggleDetailSort(sortKey);
- }
- }
- }
-
- 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);
@@ -1048,7 +377,7 @@
const requestedFocusSelector = appState.pendingFocusSelector || activeFocusSelector;
overlay.innerHTML = createModalMarkup();
- applyDialogTheme(overlay);
+ applyDialogTheme(overlay, appState.theme);
restoreModalScrollState(overlay, previousScrollState);
@@ -1264,2161 +593,311 @@
`;
}
- function renderMainContent() {
- if (appState.status === "loading") {
- return `
-
-
- Loading data
- Building the workload dataset
- ${escapeHtml(appState.loadingMessage)}
-
-
- Overall progress
-
-
-
-
- ${formatProgressPercent(appState.loadingProgress || 0)}
-
-
-
-
-
- `;
- }
-
- if (appState.status === "error") {
- return `
-
-
- Request failed
- Unable to assemble the dashboard
- ${escapeHtml(appState.errorMessage)}
-
-
- Retry
-
-
-
-
- `;
- }
-
- if (!appState.dataset) {
- return `
-
-
- Ready
- Start the live data extraction
-
- No cached snapshot is available yet. Use Refresh data to fetch the current user, customers, workloads, actions and service requests.
-
-
-
- Refresh data
-
-
-
-
- `;
- }
-
- const { user, summary } = appState.dataset;
-
- return `
-
-
-
-
- ${renderMetricCard("Total clients", formatWholeNumber(summary.totalCustomers), "Loaded from listSummary")}
- ${renderMetricCard("Total workloads", formatWholeNumber(summary.totalWorkloads), "Excluding Won workloads")}
- ${renderMetricCard("Total (ACR)", formatCurrency(summary.totalEligibleAcr), "Across eligible workloads")}
- ${renderMetricCard("Pending SRs", formatWholeNumber(summary.pendingServiceRequests.length), "SVC_PENDING_DELIVERY")}
-
-
-
- ${summary.stageIndicators.map((indicator) => renderStageIndicator(indicator)).join("")}
-
-
-
- ${renderBooleanIndicator(
- "Status de SRs",
- "Workloads com SR",
- summary.srStatus.trueCount,
- "Workloads sem SR",
- summary.srStatus.falseCount,
- "sr"
- )}
- ${renderBooleanIndicator(
- "Status de Consumption Plan/action",
- "Workloads com action",
- summary.actionStatus.trueCount,
- "Workloads sem action",
- summary.actionStatus.falseCount,
- "action"
- )}
-
-
- ${renderSrHoursAndCalendarSection(summary)}
-
-
- `;
- }
-
- function renderCurrentUserSidebar(user) {
- const displayName = cleanString(user?.name || user?.userEmail);
- const userEmail = cleanString(user?.userEmail);
- const avatar = cleanString(user?.avatar);
- const initials = getUserInitials(displayName || userEmail);
-
- return `
-
- ${
- avatar
- ? `
`
- : `
${escapeHtml(initials)} `
- }
-
-
- `;
- }
-
- function getUserInitials(value) {
- const parts = cleanString(value)
- .split(/\s+/)
- .filter(Boolean)
- .slice(0, 2);
-
- if (!parts.length) {
- return "U";
- }
-
- return parts.map((part) => part.charAt(0).toUpperCase()).join("");
- }
-
- function getExtensionVersion() {
- try {
- return chrome.runtime.getManifest()?.version || "0.0.0";
- } catch {
- return "0.0.0";
- }
- }
-
- function renderMetricCard(label, value, detail) {
- return `
-
- ${escapeHtml(label)}
- ${escapeHtml(value)}
- ${escapeHtml(detail)}
-
- `;
- }
-
- function renderStageIndicator(indicator) {
- return `
-
- ${escapeHtml(indicator.label)}
- ${formatWholeNumber(indicator.count)}
- Total (ACR): ${formatCurrency(indicator.totalAcr)}
-
- `;
- }
-
- function renderBooleanIndicator(
- title,
- trueLabel,
- trueCount,
- falseLabel,
- falseCount,
- scope
- ) {
- const total = trueCount + falseCount;
- const truePercent = total > 0 ? (trueCount / total) * 100 : 0;
- const falsePercent = total > 0 ? (falseCount / total) * 100 : 0;
-
- return `
-
-
-
-
-
-
-
-
- ${escapeHtml(trueLabel)}: ${formatPercent(truePercent)}
- ${escapeHtml(falseLabel)}: ${formatPercent(falsePercent)}
-
-
-
-
- ${escapeHtml(trueLabel)}
- ${formatWholeNumber(trueCount)}
- ${formatPercent(truePercent)}
-
-
- ${escapeHtml(falseLabel)}
- ${formatWholeNumber(falseCount)}
- ${formatPercent(falsePercent)}
-
-
-
- `;
- }
-
- function renderSrHoursAndCalendarSection(summary) {
- const calendar = buildWorkloadStartCalendar(
- summary.allWorkloads,
- appState.calendarYear,
- appState.calendarMonth,
- appState.calendarView,
- appState.calendarAnchorDateKey
- );
-
- return `
-
- ${renderSrHoursIndicatorCard(summary)}
- ${renderWorkloadStartCalendar(calendar)}
-
- `;
- }
-
- function renderSrHoursIndicatorCard(summary) {
- const zeroToFourCount = summary.srHours.betweenZeroAndFour.length;
- const aboveFourCount = summary.srHours.aboveFour.length;
- const total = zeroToFourCount + aboveFourCount;
- const zeroToFourPercent = total > 0 ? (zeroToFourCount / total) * 100 : 0;
- const aboveFourPercent = total > 0 ? (aboveFourCount / total) * 100 : 0;
-
- return `
-
-
-
-
-
-
-
-
- SRs 0-4h: ${formatPercent(zeroToFourPercent)}
- SRs >4h: ${formatPercent(aboveFourPercent)}
-
-
-
- ${renderSrHoursIndicator(
- "SRs 0-4h",
- formatPercent(zeroToFourPercent),
- zeroToFourCount,
- "0-4"
- )}
- ${renderSrHoursIndicator(
- "SRs >4h",
- formatPercent(aboveFourPercent),
- aboveFourCount,
- "above-4"
- )}
-
-
- `;
- }
-
- function renderSrHoursIndicator(label, detail, count, value) {
- return `
-
- ${escapeHtml(label)}
- ${formatWholeNumber(count)}
- ${escapeHtml(detail)}
-
- `;
- }
-
- function renderWorkloadStartCalendar(calendar) {
- return `
-
-
-
- ${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
- .map((day) => `${day} `)
- .join("")}
-
-
- ${calendar.days
- .map((day) => renderCalendarDay(day, calendar.view))
- .join("")}
-
-
- `;
- }
-
- function renderCalendarDay(day, view = "month") {
- if (view === "week") {
- return renderCalendarWeekDay(day);
- }
-
- const firstWorkload = day.workloads[0];
- const calendarWorkloadLabel =
- firstWorkload?.description || firstWorkload?.name || "Workload";
- const className = [
- "arch-panel-extension-calendar-day",
- day.isCurrentMonth ? "" : "is-muted",
- day.isToday ? "is-today" : "",
- day.workloads.length ? "has-workload" : "",
- ]
- .filter(Boolean)
- .join(" ");
-
- return `
-
- ${day.dayNumber}
- ${
- day.isWeekStart
- ? `${escapeHtml(
- day.operationalWeekLabel
- )} `
- : ""
- }
- ${
- day.workloads.length
- ? `${escapeHtml(
- calendarWorkloadLabel
- )} ${formatWholeNumber(
- day.workloads.length
- )} `
- : ""
- }
-
- `;
- }
-
- function renderCalendarWeekDay(day) {
- const className = [
- "arch-panel-extension-calendar-day",
- "is-week-day",
- day.isToday ? "is-today" : "",
- day.workloads.length ? "has-workload" : "",
- ]
- .filter(Boolean)
- .join(" ");
- const title = day.workloads
- .map((workload) => workload.description || workload.name)
- .filter(Boolean)
- .join(" | ");
-
- return `
-
-
- ${day.dayNumber}
- ${
- day.isWeekStart
- ? `${escapeHtml(
- day.operationalWeekLabel
- )} `
- : ""
- }
-
-
- ${
- day.workloads.length
- ? day.workloads.map((workload) => renderCalendarWeekWorkload(workload)).join("")
- : `No starts `
- }
-
-
- `;
- }
-
- function renderCalendarWeekWorkload(workload) {
- const label = workload.description || workload.name || "Workload";
- const customer = workload.customerName || "";
-
- return `
-
- ${escapeHtml(label)}
- ${
- customer
- ? `${escapeHtml(
- customer
- )} `
- : ""
- }
-
- `;
- }
-
- function renderWorkloadTable(workloads) {
- return `
-
-
-
-
-
-
-
-
-
-
-
-
-
- Opportunity
- Customer
- Workload
- ACR
- Type
- Tem SR?
- Tem Action?
-
-
-
- ${workloads.map((workload) => renderWorkloadTableRow(workload)).join("")}
-
-
-
- `;
- }
-
- function renderWorkloadTableRow(
- workload,
- includeConsumptionStartDate = false,
- includeRampMonths = false
- ) {
- const hasRampMismatch =
- includeRampMonths && hasRampForecastMismatch(workload);
-
- return `
-
- ${renderOpportunityLink(workload)}
- ${escapeHtml(workload.customerName || "")}
- ${renderWorkloadLink(workload, hasRampMismatch)}
- ${
- includeConsumptionStartDate
- ? `${escapeHtml(formatDate(workload.consumptionStartDate) || "-")} `
- : ""
- }
- ${
- includeRampMonths
- ? `${renderRampMonthsButton(workload)} `
- : ""
- }
- ${formatCurrency(workload.adjustedACR)}
- ${escapeHtml(workload.opportunityForecastTypeGroup)}
- ${renderBooleanBadge(workload.hasSR)}
- ${renderHasActionCell(workload)}
-
- `;
- }
-
- function renderRampMonthsButton(workload) {
- return `
-
- ${formatWholeNumber(workload.rampMonths || 0)}
-
- `;
- }
-
- function renderBooleanBadge(value) {
- const isTrue = Boolean(value);
- const variantClass = isTrue ? "is-success" : "is-danger";
- const label = isTrue ? "Sim" : "Não";
-
- return `
-
- ${label}
-
- `;
- }
-
- function renderHasActionCell(workload) {
- if (workload.hasAction) {
- return renderBooleanBadge(true);
- }
-
- return `
-
- ${renderBooleanBadge(false)}
-
- ➕
-
-
- `;
- }
-
- function renderInsightRow(label, value, detail) {
- return `
-
-
-
${escapeHtml(label)}
-
${escapeHtml(detail)}
-
-
${escapeHtml(value)}
-
- `;
- }
-
- function renderDetailModal() {
- if (!appState.detailModal) {
- return "";
- }
-
- const isServiceRequestDetail = appState.detailModal.type === "serviceRequests";
- const detailItems = isServiceRequestDetail
- ? appState.detailModal.items || []
- : getSortedDetailItems();
-
- return `
-
-
-
-
- ${
- isServiceRequestDetail
- ? `${formatWholeNumber(detailItems.length)} SRs `
- : `${formatWholeNumber(detailItems.length)} workloads
-
- Total (ACR): ${formatCurrency(appState.detailModal.totalAcr)} `
- }
-
- ${
- isServiceRequestDetail
- ? renderServiceRequestDetailTable(detailItems)
- : renderDetailTable(
- detailItems,
- appState.detailModal.scope === "calendar-day"
- )
- }
-
- ${
- isServiceRequestDetail
- ? `
- ⏱️ Manage time
- `
- : ""
- }
-
- Fechar
-
-
-
-
- `;
- }
-
- function renderActionFormModal() {
- if (!appState.actionFormModal || !appState.dataset) {
- return "";
- }
-
- const workload = findWorkloadById(appState.actionFormModal.workloadId);
- const userEmail = appState.dataset.user?.userEmail || "";
-
- if (!workload) {
- return "";
- }
-
- const modalError = cleanString(appState.actionFormModal.errorMessage);
- const isSubmitting = Boolean(appState.actionFormModal.isSubmitting);
-
- return `
-
-
-
-
-
Create workload action
-
Criar action
-
-
- Fechar
-
-
-
-
- ${renderOpportunityLink(workload)}
-
- ${escapeHtml(workload.customerName || "")}
-
- ${escapeHtml(workload.name || "Unnamed workload")}
-
-
- ${modalError ? `
${escapeHtml(modalError)}
` : ""}
-
-
-
-
- `;
- }
-
- function renderRampComparisonModal() {
- const modal = appState.rampComparisonModal;
-
- if (!modal) {
- return "";
- }
-
- const rows = modal.rows || [];
- const title = `${modal.workload.customerName || "Unknown customer"} | ${
- modal.workload.name || "Unnamed workload"
- }${
- modal.workload.description ? ` | ${modal.workload.description}` : ""
- }`;
-
- return `
-
-
-
-
-
Ramp forecast comparison
-
${escapeHtml(title)}
-
-
-
- Ramp months: ${formatWholeNumber(modal.workload.rampMonths || 0)}
-
- Adjusted ACR: ${formatCurrency(modal.workload.adjustedACR)}
-
- Consumption start: ${escapeHtml(
- formatDate(modal.workload.consumptionStartDate) || "-"
- )}
-
- ${renderRampComparisonPivotTable(rows)}
-
-
- Atualizar pelo Calculated ramp
-
-
- Fechar
-
-
-
-
- `;
- }
-
- function renderRampComparisonPivotTable(rows) {
- if (!rows.length) {
- return `Nenhum forecast encontrado para comparar.
`;
- }
-
- return `
-
-
-
-
- Period
- ${rows.map((row) => `${escapeHtml(row.periodLabel)} `).join("")}
-
-
- Current adjusted
- ${rows
- .map(
- (row) =>
- `${formatCurrency(row.currentValue)} `
- )
- .join("")}
-
-
- Calculated ramp
- ${rows
- .map(
- (row) =>
- `${formatCurrency(row.calculatedValue)} `
- )
- .join("")}
-
-
- Delta
- ${rows
- .map(
- (row) =>
- `${formatCurrency(row.delta)} `
- )
- .join("")}
-
-
-
-
- `;
- }
-
- function renderForecastUpdateConfirmModal() {
- const modal = appState.forecastUpdateConfirmModal;
-
- if (!modal) {
- return "";
- }
-
- return `
-
-
-
-
-
Confirmar atualização
-
Atualizar forecast pelo Calculated ramp?
-
-
-
- Esta ação enviará ${formatWholeNumber(
- modal.payload.length
- )} meses calculados para o workload atual. Os valores atuais do forecast serão substituídos pelos valores da linha Calculated ramp.
-
- ${
- modal.errorMessage
- ? `
${escapeHtml(
- modal.errorMessage
- )}
`
- : ""
- }
-
-
- Cancelar
-
-
- ${modal.isSubmitting ? "Atualizando..." : "Confirmar atualização"}
-
-
-
-
- `;
- }
-
- function renderTimeEntriesDrawer() {
- const drawer = appState.timeEntriesDrawer;
-
- if (!drawer) {
- return "";
- }
-
- const items = Array.isArray(drawer.items) ? drawer.items : [];
-
- return `
-
-
-
-
Horas apontadas
-
SR ${escapeHtml(drawer.srNumber || "-")}
-
-
- ${renderTimeEntriesDrawerBody(drawer, items)}
-
-
- `;
- }
-
- function renderTimeEntriesDrawerBody(drawer, items) {
- if (drawer.status === "loading") {
- return `
-
-
Carregando
-
Buscando dados
-
Consultando o resumo de time entries na origem COMCIP.
-
- `;
- }
-
- if (drawer.status === "error") {
- return `
-
-
Erro na requisição
-
${escapeHtml(drawer.errorMessage || "Não foi possível carregar os apontamentos.")}
-
- Tentar novamente
-
-
- `;
- }
-
- const groupedItems = groupTimeEntriesByResourceName(items);
-
- return `
-
- ${formatWholeNumber(items.length)} registros
- ${formatWholeNumber(groupedItems.length)} resources
-
- ${
- items.length
- ? groupedItems.map((group) => renderTimeEntryGroup(group)).join("")
- : `Nenhum registro retornado para esta SR.
`
- }
- `;
- }
-
- function renderTimeEntryGroup(group) {
- const totalTimeSpent = group.items.reduce((sum, item) => {
- return sum + getTimeEntryTimeSpent(item);
- }, 0);
-
- return `
-
-
-
-
resourceName
-
${escapeHtml(group.resourceName)}
-
- ${formatHours(totalTimeSpent)} total
-
-
-
-
-
- Activity
- Task type
- Time spent
-
-
-
- ${group.items.map((item) => renderTimeEntryRow(item)).join("")}
-
-
-
- Total
- ${formatHours(totalTimeSpent)}
-
-
-
-
-
- `;
- }
-
- function renderTimeEntryRow(item) {
- return `
-
- ${escapeHtml(getTimeEntryActivity(item) || "-")}
- ${escapeHtml(getTimeEntryTaskType(item) || "-")}
- ${formatHours(getTimeEntryTimeSpent(item))}
-
- `;
- }
-
- function renderDetailTable(items, includeConsumptionStartDate = false) {
- return `
-
-
-
-
- ${renderSortButton("Opportunity", DETAIL_SORT_KEYS.opportunityNumber)}
- ${renderSortButton("Customer", DETAIL_SORT_KEYS.customerName)}
- ${renderSortButton("Workload", DETAIL_SORT_KEYS.workload)}
- ${includeConsumptionStartDate ? "consumptionStartDate " : ""}
- ${renderSortButton("Ramp months", DETAIL_SORT_KEYS.rampMonths, true)}
- ${renderSortButton("ACR", DETAIL_SORT_KEYS.adjustedACR, true)}
- ${renderSortButton("Type", DETAIL_SORT_KEYS.opportunityForecastTypeGroup)}
- ${renderSortButton("Tem SR?", DETAIL_SORT_KEYS.hasSR)}
- ${renderSortButton("Tem Action?", DETAIL_SORT_KEYS.hasAction)}
-
-
-
- ${items
- .map((workload) =>
- renderWorkloadTableRow(workload, includeConsumptionStartDate, true)
- )
- .join("")}
-
-
-
- `;
- }
-
- function renderTimeManagementModal() {
- const modal = appState.timeManagementModal;
-
- if (!modal) {
- return "";
- }
-
- const selectedDate = modal.selectedDate || getDateInputValue(new Date());
- const selectedDateLabel = formatDateInputDisplay(selectedDate);
- const canSaveTimeManagement = isTimeManagementSaveEnabled(modal);
- const isSavingTimeManagement = Boolean(modal.isSaving);
-
- return `
-
-
-
-
-
Horas apontadas
-
Manage time
-
-
-
-
-
- ${renderTimeManagementBody(modal)}
-
-
-
- ${
- modal.saveErrorMessage
- ? `
${escapeHtml(modal.saveErrorMessage)}
`
- : ""
- }
-
- Fechar
-
-
-
-
- `;
- }
-
- 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.length
- ? rows.map((row, index) => renderTimeManagementEntryRow(modal, row, index)).join("")
- : `
- Nenhum apontamento de hora neste período
-
`
- }
-
-
- `;
- }
-
- 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 isHighlighted =
- cleanString(modal?.highlightRowId) &&
- cleanString(modal?.highlightRowId) === rowId;
- const selectedSr = cleanString(row?.selectedSrNumber);
- const activityOptions = getTimeManagementActivityOptions(modal, selectedSr);
- const selectedSrOption = findTimeManagementServiceRequestByNumber(
- modal,
- selectedSr
- );
- const isNonServiceRequest = isNonServiceRequestSrNumber(selectedSr);
- const selectedSrLabel =
- selectedSrOption?.label ||
- formatTimeManagementServiceRequestLabel(selectedSrOption) ||
- 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 `
-
-
-
- ${renderSearchableCombobox({
- field: "sr",
- rowId,
- selectedValue: selectedSr,
- value: selectedSrLabel,
- placeholder: "Search SR",
- options: modal.serviceRequests || [],
- valueKey: "srNumber",
- labelKey: "label",
- fallbackLabelKey: "srTitle",
- disabled: isNonServiceRequest,
- })}
-
-
- ${renderSearchableCombobox({
- field: "activity",
- rowId,
- selectedValue: selectedActivityValue,
- value: selectedActivityLabel,
- placeholder: "Search activity",
- options: activityOptions,
- valueKey: "value",
- labelKey: "label",
- metaKey: "value",
- disabled: isNonServiceRequest,
- })}
-
-
- ${renderSearchableCombobox({
- field: "taskType",
- rowId,
- selectedValue: selectedTaskTypeValue,
- value: selectedTaskTypeLabel,
- placeholder: "Search task type",
- options: taskTypeOptions,
- valueKey: "value",
- labelKey: "label",
- })}
-
- ${(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 renderTimeManagementHourIndicators(modal) {
- if (modal?.status === "loading") {
- return `
-
- ${Array.from({ length: 4 }, () => `
-
-
-
-
- `).join("")}
-
- `;
- }
-
- return `
-
- ${buildTimeManagementHourIndicators(modal)
- .map(
- (indicator) => `
-
- ${escapeHtml(indicator.label)}
- ${escapeHtml(formatHours(indicator.value))}
-
- `
- )
- .join("")}
-
- `;
- }
-
- function buildTimeManagementHourIndicators(modal) {
- const totals = getTimeManagementRows(modal).reduce(
- (accumulator, row) => {
- const rowTotal = getTimeManagementRowTotalHours(row);
- const category = getTimeManagementHourCategory(modal, row);
-
- accumulator[category] += rowTotal;
- accumulator.total += rowTotal;
-
- return accumulator;
- },
- {
- service: 0,
- nonService: 0,
- timeOff: 0,
- total: 0,
- }
- );
-
- return [
- { label: "Service", value: totals.service },
- { label: "Non Service", value: totals.nonService },
- { label: "Time Off", value: totals.timeOff },
- { label: "Total hours", value: totals.total },
- ];
- }
-
- function getTimeManagementRowTotalHours(row) {
- return Object.values(row?.dayHours || {}).reduce(
- (sum, value) => sum + formatTimeManagementHourPayloadValue(value),
- 0
- );
- }
-
- function getTimeManagementHourCategory(modal, row) {
- if (isTimeManagementTimeOffRow(modal, row)) {
- return "timeOff";
- }
-
- return isNonServiceRequestSrNumber(row?.selectedSrNumber)
- ? "nonService"
- : "service";
- }
-
- function isTimeManagementTimeOffRow(modal, row) {
- const taskTypeValue = cleanString(row?.selectedTaskTypeValue);
- const taskType = findTimeManagementTaskTypeByValue(modal, taskTypeValue);
- const candidates = [
- taskTypeValue,
- taskType?.label,
- taskType?.tags,
- ].map((value) => normalizeString(value));
-
- return candidates.some(
- (value) =>
- value === "TIME_OFF" ||
- value === "TIMEOFF" ||
- value.includes("TIME_OFF") ||
- value.includes("TIME OFF")
- );
- }
-
- function findTimeManagementTaskTypeByValue(modal, value) {
- const normalizedValue = normalizeString(value);
-
- if (!normalizedValue) {
- return null;
- }
-
- return (modal?.taskTypes || []).find(
- (item) => normalizeString(item?.value) === normalizedValue
- ) || null;
- }
-
- 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(`${escapeHtml(emptyLabel)} `);
- }
-
- return options
- .concat(
- items.map((item) => {
- const value = cleanString(item?.[valueKey]);
- const label = cleanString(item?.[labelKey]) || value || emptyLabel;
-
- return `
- ${escapeHtml(label)}
- `;
- })
- )
- .join("");
- }
-
- function buildRampForecastComparisonRows(workloads) {
- return (workloads || [])
- .flatMap((workload) => {
- const startDate = parseDatePreservingDateOnly(workload.consumptionStartDate);
-
- if (Number.isNaN(startDate.getTime())) {
- return [];
- }
-
- const startMonth = startDate.getMonth() + 1;
- const startYear = startDate.getFullYear();
- const workloadLabel = `${workload.name || "Unnamed workload"}${
- workload.description ? ` | ${workload.description}` : ""
- }`;
-
- return (workload.forecast || [])
- .map((forecastItem) => {
- const forecastMonth = parseForecastMonthNumber(forecastItem.month);
- const forecastYear = toNumber(forecastItem.year);
-
- if (!forecastMonth || !forecastYear) {
- return null;
- }
-
- const monthIndex =
- (forecastYear - startYear) * 12 + (forecastMonth - startMonth) + 1;
-
- const currentValue = toNumber(forecastItem.adjustedConsumptionAmount);
- const calculatedValue =
- monthIndex >= 1 && monthIndex <= 12
- ? calculateRampForecastAmount({
- adjustedACR: workload.adjustedACR,
- rampMonths: workload.rampMonths,
- monthIndex,
- consumptionStartDate: startDate,
- })
- : 0;
-
- return {
- workloadLabel,
- periodLabel: formatForecastPeriodLabel(forecastYear, forecastMonth),
- currentValue,
- calculatedValue,
- delta: currentValue - calculatedValue,
- monthIndex,
- year: forecastYear,
- month: forecastMonth,
- sortKey: `${workloadLabel}|${String(forecastYear).padStart(
- 4,
- "0"
- )}-${String(forecastMonth).padStart(2, "0")}`,
- };
- })
- .filter(Boolean);
- })
- .sort((left, right) => left.sortKey.localeCompare(right.sortKey));
- }
-
- function hasRampForecastMismatch(workload) {
- return buildRampForecastComparisonRows([workload]).some((row) =>
- isNonZeroAmount(row.delta)
- );
- }
-
- function calculateRampForecastAmount({
- adjustedACR,
- rampMonths,
- monthIndex,
- consumptionStartDate,
- }) {
- const annualValue = toNumber(adjustedACR);
- const ramp = Math.max(0, toNumber(rampMonths));
- const month = Math.max(1, toNumber(monthIndex));
-
- if (!annualValue) {
- return 0;
- }
-
- if (ramp <= 0) {
- return Math.round(
- (annualValue / 12) *
- getStartMonthProrationFactor(month, consumptionStartDate)
- );
- }
-
- const steadyStateMonthlyAmount = (2 * annualValue) / (24 - ramp);
- const rampStepCount = ramp + 1;
- const calculatedAmount =
- month <= rampStepCount
- ? steadyStateMonthlyAmount * (month / rampStepCount)
- : steadyStateMonthlyAmount;
-
- return Math.round(
- calculatedAmount * getStartMonthProrationFactor(month, consumptionStartDate)
- );
- }
-
- function getStartMonthProrationFactor(monthIndex, consumptionStartDate) {
- if (toNumber(monthIndex) !== 1) {
- return 1;
- }
-
- const startDate = parseDatePreservingDateOnly(consumptionStartDate);
-
- if (Number.isNaN(startDate.getTime())) {
- return 1;
- }
-
- const daysInMonth = new Date(
- startDate.getFullYear(),
- startDate.getMonth() + 1,
- 0
- ).getDate();
- const remainingDays = daysInMonth - startDate.getDate() + 1;
-
- return Math.max(0, Math.min(1, remainingDays / daysInMonth));
- }
-
- function isNonZeroAmount(value) {
- return Math.abs(toNumber(value)) > 0.000001;
- }
-
- function parseForecastMonthNumber(value) {
- if (typeof value === "number") {
- return value >= 1 && value <= 12 ? value : 0;
- }
-
- const normalized = normalizeString(value);
- const numericMonth = Number.parseInt(normalized, 10);
-
- if (numericMonth >= 1 && numericMonth <= 12) {
- return numericMonth;
- }
-
- const monthMap = {
- JAN: 1,
- JANUARY: 1,
- FEB: 2,
- FEBRUARY: 2,
- MAR: 3,
- MARCH: 3,
- APR: 4,
- APRIL: 4,
- MAY: 5,
- JUN: 6,
- JUNE: 6,
- JUL: 7,
- JULY: 7,
- AUG: 8,
- AUGUST: 8,
- SEP: 9,
- SEPT: 9,
- SEPTEMBER: 9,
- OCT: 10,
- OCTOBER: 10,
- NOV: 11,
- NOVEMBER: 11,
- DEC: 12,
- DECEMBER: 12,
- };
-
- return monthMap[normalized] || 0;
- }
-
- function formatForecastPeriodLabel(year, month) {
- const fiscalYear = month >= 6 ? year + 1 : year;
- const fiscalYearLabel = formatWeekNumber(fiscalYear % 100);
- const monthLabel = new Intl.DateTimeFormat("en-US", {
- month: "short",
- }).format(new Date(year, month - 1, 1));
-
- return `FY${fiscalYearLabel} ${monthLabel}`;
- }
-
- function renderServiceRequestDetailTable(items) {
- return `
-
-
-
-
- Customer
- SR Number
- Queue
- Service
- Pillar
- Oppty value
- Horas apontadas
- Customer deadline
- Oppty Status
-
-
-
- ${items
- .map(
- (serviceRequest) => `
-
- ${escapeHtml(serviceRequest.customerName || "-")}
- ${renderServiceRequestLink(serviceRequest)}
- ${escapeHtml(serviceRequest.queueName || "-")}
- ${escapeHtml(serviceRequest.serviceName || "-")}
- ${escapeHtml(serviceRequest.pillarLabel || "-")}
- ${formatCurrency(serviceRequest.opportunityValue)}
- ${renderReportedHoursLink(serviceRequest)}
- ${escapeHtml(formatDate(serviceRequest.customerDeadline) || "-")}
- ${escapeHtml(serviceRequest.opportunityStatusLabel || "-")}
-
- `
- )
- .join("")}
-
-
-
- `;
- }
-
- function renderSortButton(label, sortKey, isNumeric = false) {
- const currentKey = appState.detailModal?.sortKey || "";
- const currentDirection = appState.detailModal?.sortDirection || "asc";
- const indicator =
- currentKey === sortKey ? (currentDirection === "asc" ? " ↑" : " ↓") : "";
-
- return `
-
- ${escapeHtml(label)}${indicator}
-
- `;
- }
-
- function renderOpportunityLink(workload) {
- const label = workload.opportunityNumber || "Opportunity";
-
- if (!workload.opportunityId || !workload.opportunityNumber) {
- return escapeHtml(label);
- }
-
- const href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?id=${encodeURIComponent(
- workload.opportunityId
- )}&puid=${encodeURIComponent(workload.opportunityNumber)}`;
-
- return `${escapeHtml(label)} `;
- }
-
- function renderServiceRequestLink(serviceRequest) {
- const srNumber = cleanString(serviceRequest?.srNumber);
-
- if (!srNumber) {
- return "-";
- }
-
- const href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=${encodeURIComponent(
- srNumber
- )}`;
-
- return `${escapeHtml(srNumber)} `;
- }
-
- function renderReportedHoursLink(serviceRequest) {
- const srNumber = cleanString(serviceRequest?.srNumber);
- const label = formatHours(serviceRequest?.totalHoursWorked);
-
- if (!srNumber) {
- return escapeHtml(label);
- }
-
- return `
-
- ${escapeHtml(label)}
-
- `;
- }
-
- function renderWorkloadLink(workload, showRampAlert = false) {
- const description = workload.description ? ` | ${workload.description}` : "";
- const label = `${workload.name || "Unnamed workload"}${description}`;
-
- if (!workload.workloadId) {
- return `${escapeHtml(label)}${renderRampAlertButton(workload, showRampAlert)}`;
- }
-
- const href = `https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench/workload_details;workloadId=${encodeURIComponent(
- workload.workloadId
- )}`;
-
- return `
-
- ${escapeHtml(label)}
- ${renderRampAlertButton(workload, showRampAlert)}
-
- `;
- }
-
- function renderRampAlertButton(workload, showRampAlert) {
- if (!showRampAlert || !workload.workloadId) {
- return "";
- }
-
- return `
-
- !
-
- `;
- }
-
- function getHeaderNote() {
- if (appState.status === "loading") {
- return appState.loadingMessage || "Loading";
- }
-
- if (appState.status === "error") {
- return "Extraction failed";
- }
-
- if (appState.dataset) {
- return `${appState.dataset.user.userEmail} · ${formatWholeNumber(
- appState.dataset.summary.eligibleWorkloads.length
- )} eligible workloads`;
- }
-
- return "Ready";
- }
-
- function getHeaderSummary() {
- if (appState.status === "loading") {
- return appState.loadingMessage || "Loading";
- }
-
- if (appState.status === "error") {
- return "Extraction failed";
- }
-
- if (appState.dataset) {
- return `${appState.dataset.user.userEmail} | ${formatWholeNumber(
- appState.dataset.summary.eligibleWorkloads.length
- )} eligible workloads`;
- }
-
- return "Ready";
- }
+ const {
+ buildRampForecastComparisonRows,
+ hasRampForecastMismatch,
+ isNonZeroAmount,
+ parseForecastMonthNumber,
+ createForecastUpdatePayload,
+ } = globalThis.ArchPanelRampForecastDomain.create({
+ toNumber,
+ normalizeString,
+ parseDatePreservingDateOnly,
+ formatWeekNumber,
+ });
+
+ const {
+ getTimeManagementRows,
+ isTimeManagementSaveEnabled,
+ getTimeManagementPendingChanges,
+ createTimeManagementRowSignature,
+ createTimeManagementSavePayload,
+ formatTimeManagementHourPayloadValue,
+ getTimeManagementDayKey,
+ getTimeManagementWeekdayName,
+ isNonServiceRequestSrNumber,
+ } = globalThis.ArchPanelTimeManagementDomain.create({
+ cleanString,
+ normalizeString,
+ toNumber,
+ parseTimeEntryDate,
+ getLocalDateKey,
+ });
+
+ const {
+ normalizeInfoWeekResponse,
+ normalizeTimeManagementServiceRequests,
+ formatTimeManagementServiceRequestLabel,
+ mergeTimeManagementServiceRequests,
+ normalizeTimeManagementEntryRows,
+ normalizeTimeManagementEntryRowSelection,
+ getActivityRequestSrNumbers,
+ normalizeActivityRequest,
+ normalizeTaskTypeRequest,
+ getTimeManagementTaskTypeOptions,
+ getTimeManagementActivityOptions,
+ findTimeManagementServiceRequest,
+ findTimeManagementServiceRequestByNumber,
+ findOptionValue,
+ findOptionLabel,
+ normalizeTimeEntriesSummary,
+ groupTimeEntriesByResourceName,
+ getTimeEntryTimeSpent,
+ getTimeEntryActivity,
+ getTimeEntryTaskType,
+ } = globalThis.ArchPanelTimeManagementData.create({
+ cleanString,
+ normalizeString,
+ firstNonEmptyString,
+ extractArray,
+ getFieldValue,
+ flattenDisplayRecord,
+ toNumber,
+ parseTimeEntryDate,
+ getLocalDateKey,
+ getTimeManagementDayKey,
+ getTimeManagementWeekdayName,
+ createTimeManagementRowSignature,
+ });
+
+ const workbenchRepository = globalThis.ArchPanelWorkbenchRepository.create({
+ TARGET_ORIGIN,
+ API_ENDPOINTS,
+ fetchJson,
+ extractArray,
+ cleanString,
+ firstNonEmptyString,
+ getFieldValue,
+ toNumber,
+ normalizeWorkload,
+ isWonWorkload,
+ normalizeTeamValue,
+ });
+
+ const comcipRepository = globalThis.ArchPanelComcipRepository.create({
+ COMCIP_REQUEST,
+ sendComcipRequest: sendComcipRequestMessage,
+ cleanString,
+ firstNonEmptyString,
+ extractArray,
+ getFieldValue,
+ toNumber,
+ formatDateForTimeEntryRequest,
+ });
+
+ const {
+ renderMainContent,
+ renderWorkloadTableRow,
+ } = globalThis.ArchPanelDashboardView.create({
+ appState,
+ buildWorkloadStartCalendar,
+ hasRampForecastMismatch,
+ renderOpportunityLink,
+ renderWorkloadLink,
+ escapeHtml,
+ cleanString,
+ formatWholeNumber,
+ formatCurrency,
+ formatPercent,
+ formatProgressPercent,
+ formatDate,
+ });
+
+ const {
+ renderDetailModal,
+ renderActionFormModal,
+ renderRampComparisonModal,
+ renderForecastUpdateConfirmModal,
+ renderTimeEntriesDrawer,
+ } = globalThis.ArchPanelWorkbenchModalsView.create({
+ appState,
+ DETAIL_SORT_KEYS,
+ ACTION_TEAM_OPTIONS,
+ ACTION_COMPLEXITY_OPTIONS,
+ getSortedDetailItems,
+ renderServiceRequestDetailTable,
+ renderSortButton,
+ renderWorkloadTableRow,
+ findWorkloadById,
+ renderOpportunityLink,
+ groupTimeEntriesByResourceName,
+ getTimeEntryTimeSpent,
+ getTimeEntryActivity,
+ getTimeEntryTaskType,
+ isNonZeroAmount,
+ escapeHtml,
+ cleanString,
+ formatWholeNumber,
+ formatCurrency,
+ formatDate,
+ formatHours,
+ });
+
+ const {
+ renderTimeManagementModal,
+ } = globalThis.ArchPanelManageTimeView.create({
+ appState,
+ escapeHtml,
+ cleanString,
+ normalizeString,
+ formatDateInputDisplay,
+ getDateInputValue,
+ formatHours,
+ getWeekStartDate,
+ getLocalDateKey,
+ getTimeManagementDayKey,
+ isTimeManagementSaveEnabled,
+ getTimeManagementRows,
+ getTimeManagementActivityOptions,
+ findTimeManagementServiceRequestByNumber,
+ formatTimeManagementServiceRequestLabel,
+ isNonServiceRequestSrNumber,
+ findOptionValue,
+ findOptionLabel,
+ getTimeManagementTaskTypeOptions,
+ formatTimeManagementHourPayloadValue,
+ });
+
+ const manageTimeController = globalThis.ArchPanelManageTimeController.create({
+ appState,
+ COMCIP_REQUEST,
+ comcipRepository,
+ readCachedTaskTypePayload,
+ writeCachedTaskTypePayload,
+ getStoredTimeManagementSelectedDate,
+ setStoredTimeManagementSelectedDate,
+ renderModal,
+ cleanString,
+ normalizeString,
+ getDateInputValue,
+ parseDatePreservingDateOnly,
+ getFieldValue,
+ extractArray,
+ getErrorMessage,
+ cssEscape,
+ getTimeManagementRows,
+ isTimeManagementSaveEnabled,
+ getTimeManagementPendingChanges,
+ createTimeManagementSavePayload,
+ isNonServiceRequestSrNumber,
+ normalizeInfoWeekResponse,
+ normalizeTimeManagementServiceRequests,
+ mergeTimeManagementServiceRequests,
+ normalizeTimeManagementEntryRows,
+ normalizeTimeManagementEntryRowSelection,
+ getActivityRequestSrNumbers,
+ normalizeActivityRequest,
+ normalizeTaskTypeRequest,
+ getTimeManagementTaskTypeOptions,
+ getTimeManagementActivityOptions,
+ findTimeManagementServiceRequest,
+ findOptionValue,
+ });
+ const {
+ toggleTimeManagementDatePicker,
+ shiftTimeManagementDatePickerMonth,
+ selectTimeManagementDate,
+ shiftTimeManagementDateByWeeks,
+ openTimeManagementModal,
+ loadTimeManagementData,
+ addTimeManagementEntryRow,
+ removeTimeManagementEntryRow,
+ updateTimeManagementSelectedSr,
+ updateTimeManagementField,
+ updateTimeManagementDayHours,
+ saveTimeManagementChanges,
+ selectTimeManagementComboboxOption,
+ filterTimeManagementCombobox,
+ handleTimeManagementComboboxKeydown,
+ refreshTaskTypeCache,
+ } = manageTimeController;
+
+ workbenchController = globalThis.ArchPanelWorkbenchController.create({
+ appState,
+ IDS,
+ DETAIL_SORT_KEYS,
+ hcmPage,
+ workbenchRepository,
+ comcipRepository,
+ readCachedDataset,
+ writeCachedDataset,
+ readCachedWorklistToken,
+ setStoredCalendarView,
+ refreshTaskTypeCache,
+ renderModal,
+ cleanString,
+ normalizeString,
+ toNumber,
+ toIsoDateString,
+ mapWithConcurrency,
+ getLocalDateKey,
+ getErrorMessage,
+ formatWholeNumber,
+ formatProgressPercent,
+ formatDate,
+ teamContainsUser,
+ stripTransientFields,
+ createExportPayload,
+ createDatasetSnapshot,
+ buildRampForecastComparisonRows,
+ parseForecastMonthNumber,
+ createForecastUpdatePayload,
+ normalizeTimeEntriesSummary,
+ getCalendarAnchorDate,
+ });
+ const {
+ refreshData,
+ hydrateDatasetFromCache,
+ openDetailModal,
+ openActionFormModal,
+ openRampComparisonModal,
+ openForecastUpdateConfirmModal,
+ submitForecastUpdate,
+ openTimeEntriesDrawer,
+ shiftCalendarPeriod,
+ goToCurrentCalendarPeriod,
+ setCalendarView,
+ submitActionForm,
+ toggleDetailSort,
+ } = workbenchController;
+
+ modalEvents = globalThis.ArchPanelModalEvents.create({
+ appState,
+ renderModal,
+ closeModal,
+ closeDetailModal,
+ toggleTheme,
+ toggleSidebar,
+ refreshData,
+ shiftCalendarPeriod,
+ goToCurrentCalendarPeriod,
+ setCalendarView,
+ downloadFile,
+ createExportHtml,
+ openDetailModal,
+ openActionFormModal,
+ openRampComparisonModal,
+ openForecastUpdateConfirmModal,
+ submitForecastUpdate,
+ openTimeEntriesDrawer,
+ openTimeManagementModal,
+ addTimeManagementEntryRow,
+ saveTimeManagementChanges,
+ removeTimeManagementEntryRow,
+ toggleTimeManagementDatePicker,
+ shiftTimeManagementDateByWeeks,
+ shiftTimeManagementDatePickerMonth,
+ selectTimeManagementDate,
+ selectTimeManagementComboboxOption,
+ submitActionForm,
+ toggleDetailSort,
+ loadTimeManagementData,
+ updateTimeManagementSelectedSr,
+ updateTimeManagementField,
+ updateTimeManagementDayHours,
+ filterTimeManagementCombobox,
+ handleTimeManagementComboboxKeydown,
+ });
function openModal() {
const overlay = ensureModal();
@@ -3475,3996 +954,6 @@
}, 160);
}
- async function refreshData() {
- if (appState.status === "loading") {
- return;
- }
-
- try {
- appState.status = "loading";
- appState.errorMessage = "";
- updateLoadingProgress("Requesting current user", 4);
-
- const user = await fetchCurrentUser();
-
- updateLoadingProgress("Loading current resource user", 8);
-
- const resourceCurrentUser = await fetchResourceCurrentUser(user.userEmail).catch(
- () => null
- );
-
- updateLoadingProgress("Loading customer summary pages", 12);
-
- const customers = await fetchAllCustomers(user.userEmail);
-
- updateLoadingProgress(
- `Loading workloads for ${formatWholeNumber(customers.length)} customers`,
- 24
- );
-
- let completedCustomers = 0;
- const customersWithWorkloads = await mapWithConcurrency(
- customers,
- 4,
- async (customer, index) => {
- updateLoadingProgress(
- `Loading workloads ${index + 1}/${customers.length}: ${customer.name}`,
- getSegmentProgress(24, 52, completedCustomers, customers.length)
- );
-
- const workloads = await fetchCustomerWorkloads(customer);
- completedCustomers += 1;
- updateLoadingProgress(
- `Loaded workloads ${completedCustomers}/${customers.length}: ${customer.name}`,
- getSegmentProgress(24, 52, completedCustomers, customers.length)
- );
-
- return {
- customerId: customer.customerId,
- name: customer.name,
- workloads,
- };
- }
- );
-
- const allWorkloads = customersWithWorkloads.flatMap((customer) => {
- return customer.workloads.map((workload) => ({
- ...workload,
- customerId: customer.customerId,
- customerName: customer.name,
- }));
- });
-
- updateLoadingProgress(
- `Loading actions and service requests for ${formatWholeNumber(
- allWorkloads.length
- )} workloads`,
- 56
- );
-
- let completedWorkloads = 0;
- const enrichedWorkloads = await mapWithConcurrency(
- allWorkloads,
- 6,
- async (workload, index) => {
- updateLoadingProgress(
- `Loading actions and SRs ${index + 1}/${allWorkloads.length}: ${workload.name}`,
- getSegmentProgress(56, 86, completedWorkloads, allWorkloads.length)
- );
-
- const [actions, serviceRequests] = await Promise.all([
- fetchWorkloadActions(workload.workloadId),
- fetchWorkloadServiceRequests(workload.workloadId),
- ]);
- completedWorkloads += 1;
- updateLoadingProgress(
- `Loaded actions and SRs ${completedWorkloads}/${allWorkloads.length}: ${workload.name}`,
- getSegmentProgress(56, 86, completedWorkloads, allWorkloads.length)
- );
-
- const hasSR = serviceRequests.some((serviceRequest) => {
- return (
- normalizeString(serviceRequest.status) === "SVC_PENDING_DELIVERY" &&
- teamContainsUser(serviceRequest.team, user.userEmail)
- );
- });
-
- const hasAction = actions.some((action) => {
- return normalizeString(action.owner) === normalizeString(user.userEmail);
- });
-
- return {
- ...workload,
- actions,
- serviceRequests,
- hasSR,
- hasAction,
- };
- }
- );
-
- const customersMap = new Map(
- customersWithWorkloads.map((customer) => [
- customer.customerId,
- {
- customerId: customer.customerId,
- name: customer.name,
- workloads: [],
- },
- ])
- );
-
- for (const workload of enrichedWorkloads) {
- const customer = customersMap.get(workload.customerId);
-
- if (customer) {
- customer.workloads.push(stripTransientFields(workload));
- }
- }
-
- updateLoadingProgress("Loading pending delivery service requests", 90);
-
- 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(
- user,
- finalCustomers,
- pendingServiceRequests,
- resourceCurrentUser
- );
- const dataset = createDatasetSnapshot(exportPayload);
-
- await writeCachedDataset(exportPayload);
- updateLoadingProgress("Dashboard ready", 100);
-
- appState.dataset = dataset;
- appState.status = "ready";
- appState.loadingMessage = "";
- appState.loadingProgress = 0;
- appState.detailModal = null;
- renderModal();
- } catch (error) {
- appState.status = "error";
- appState.errorMessage = getErrorMessage(error);
- appState.loadingMessage = "";
- appState.loadingProgress = 0;
- renderModal();
- }
- }
-
- function updateLoadingProgress(message, progress) {
- appState.loadingMessage = message;
- appState.loadingProgress = Math.max(0, Math.min(Number(progress) || 0, 100));
-
- if (!updateLoadingProgressDom()) {
- renderModal();
- window.requestAnimationFrame(() => {
- updateLoadingProgressDom();
- });
- }
- }
-
- function updateLoadingProgressDom() {
- const overlay = document.getElementById(IDS.overlay);
- const progress = Math.max(0, Math.min(appState.loadingProgress || 0, 100));
- const stateCopy = overlay?.querySelector(".arch-panel-extension-state-copy");
- const progressRoot = overlay?.querySelector(
- ".arch-panel-extension-loading-progress"
- );
- const progressBar = overlay?.querySelector(
- ".arch-panel-extension-loading-progress-fill"
- );
- const progressTrackValue = overlay?.querySelector(
- ".arch-panel-extension-loading-progress-value"
- );
- const shellNote = overlay?.querySelector(".arch-panel-extension-shell-note");
-
- if (!stateCopy || !progressRoot || !progressBar) {
- return false;
- }
-
- stateCopy.textContent = appState.loadingMessage;
- progressRoot.setAttribute("aria-valuenow", String(Math.round(progress)));
- progressBar.style.width = `${progress}%`;
-
- if (progressTrackValue) {
- progressTrackValue.textContent = formatProgressPercent(progress);
- }
-
- if (shellNote) {
- shellNote.textContent = appState.loadingMessage || "Loading";
- }
-
- return true;
- }
-
- function getSegmentProgress(start, end, current, total) {
- const denominator = Math.max(Number(total) || 0, 1);
- const ratio = Math.max(0, Math.min((Number(current) || 0) / denominator, 1));
-
- return start + (end - start) * ratio;
- }
-
- async function fetchPendingServiceRequests() {
- const response = await sendComcipPostMessage();
-
- 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(
- payloadMessage ||
- response?.error ||
- `COMCIP request failed (${response?.status || 0}).`
- );
- }
-
- return normalizePendingServiceRequests(response.payload);
- }
-
- function sendComcipPostMessage() {
- return sendComcipRequestMessage({
- method: "POST",
- url: COMCIP_REQUEST.url,
- payload: createComcipServiceRequestPayload(),
- timeoutMessage: "Timed out while requesting COMCIP data.",
- });
- }
-
- function sendComcipGetMessage(url) {
- return sendComcipRequestMessage({
- method: "GET",
- url,
- payload: null,
- timeoutMessage: "Timed out while requesting COMCIP data.",
- });
- }
-
- 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."));
- }, 130000);
-
- chrome.runtime.sendMessage(
- {
- type: "ARCH_PANEL_COMCIP_REQUEST",
- method,
- url,
- payload,
- useTimeEntryFrame,
- headers: {
- "accept-language": navigator.language || "pt-BR",
- "x-vb-application-version": COMCIP_REQUEST.appVersion,
- },
- },
- (response) => {
- window.clearTimeout(timeout);
- const runtimeError = chrome.runtime.lastError;
-
- if (runtimeError) {
- reject(new Error(runtimeError.message));
- return;
- }
-
- resolve(response || null);
- }
- );
- });
- }
-
- 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",
- offset: 0,
- limit: 100,
- onlyData: true,
- language: "US",
- fields: [
- "SrNumber",
- "Title",
- "LastUpdateDate",
- "CreationDate",
- "PrimaryPillarNew_c",
- "PrimaryPillarNew_c_localizedValue",
- "CountryText_c",
- "OpportunityName_Id_c.OptyId",
- "OpportunityName_Id_c.Name",
- "OpportunityName_Id_c.OptyNumber",
- "OpportunityName_Id_c.OpportunityType_c",
- "OpportunityName_Id_c.StatusCode",
- "OpportunityName_Id_c.EffectiveDate",
- "OpportunityName_Id_c.StatusCode_localizedValue",
- "OpportunityName_Id_c.PrimaryRevenue.WinProb",
- "OpportunityName_Id_c.PrimaryRevenue.RevnAmount",
- "OpportunityName_Id_c.PrimaryRevenue.CrmConversionRate",
- "OpportunityName_Id_c.WorkloadAmountUSDWorkspace_c",
- "CustomerAccount.PartyId",
- "CustomerAccount.PartyUniqueName",
- "CustomerAccount.PartyNumber",
- "CustomerAccount.NamedFlag",
- "StatusCd",
- "StatusCd_localizedValue",
- "ServiceNew_Id_c",
- "CustomerDeadline_c",
- "Owner.PartyId",
- "Owner.PartyName",
- "Owner.PrimaryEmail.EmailAddress",
- "Owner.ResourceEmailAddress",
- "ReportedByPartyId.PrimaryEmail.EmailAddress",
- "Queue.QueueId",
- "Queue.QueueName",
- "ServiceRequest_Id_SRToInternalSR",
- "ReportedHours_c",
- "ProblemDescription",
- "AccountRegistryId_c",
- ],
- sort: [
- {
- attribute: "LastUpdateDate",
- direction: "descending",
- },
- ],
- q: {
- op: "$and",
- criteria: [
- {
- op: "$eq",
- attribute: "RecordSet",
- value: "ORA_SVC_MY_SUBORD_SR_TEAM_SRS",
- },
- {
- op: "$in",
- attribute: "StatusCd",
- values: ["SVC_PENDING_DELIVERY"],
- },
- ],
- },
- aggregations: {
- StatusCd: {
- ignore: true,
- terms: {
- attribute: "StatusCd",
- includeTerms: [
- "SVC_PENDING_ACCEPTANCE",
- "SVC_PENDING_DELIVERY",
- "SVC_PENDING_RESOURCING",
- "SVC_SE_MGR_CLOSED",
- "SVC_REJECT",
- ],
- excludeTerms: ["SVC_PENDING_DISPATCHER", "ORA_SVC_NEW", "SVC_REJECT"],
- minBucketCount: 1,
- maxNumberOfBuckets: 5,
- other: false,
- missing: false,
- localize: true,
- },
- },
- Queue: createTermsAggregation("Queue", 10),
- CustomerAccount: createTermsAggregation("CustomerAccount", 5),
- OpportunityStatus: createTermsAggregation(
- "OpportunityName_Id_c.StatusCode",
- 6
- ),
- Service: createTermsAggregation("ServiceNew_Id_c", 5),
- Country: createTermsAggregation("CountryText_c", 5),
- Pillar: createTermsAggregation("PrimaryPillarNew_c", 5),
- WinProb: {
- ignore: true,
- range: {
- attribute: "OpportunityName_Id_c.PrimaryRevenue.WinProb",
- ranges: [
- { key: "0-10 % SQL", from: 0, to: 11 },
- { key: "20-30 % Pipeline", from: 20, to: 31 },
- { key: "40-50 % Upside", from: 40, to: 51 },
- { key: "60-90 % Forecast", from: 60, to: 91 },
- { key: "100 % Won", from: 100, to: 101 },
- ],
- },
- },
- Amount: {
- ignore: true,
- range: {
- attribute: "OpportunityName_Id_c.PrimaryRevenue.RevnAmount",
- ranges: [
- { key: "0-10,000", from: 0, to: 10000 },
- { key: "10,000-50,000", from: 10000, to: 50000 },
- { key: "50,000-100,000", from: 50000, to: 100000 },
- { key: ">=100,000", from: 100000 },
- ],
- },
- },
- OpportunityName_Id_c: createTermsAggregation("OpportunityName_Id_c", 5),
- },
- };
- }
-
- function createTermsAggregation(attribute, maxNumberOfBuckets) {
- return {
- ignore: true,
- terms: {
- attribute,
- includeTerms: [],
- minBucketCount: 1,
- maxNumberOfBuckets,
- other: false,
- missing: false,
- localize: true,
- },
- };
- }
-
- function normalizePendingServiceRequests(payload) {
- const items = Array.isArray(payload)
- ? payload
- : extractArray(payload, ["items", "data", "results", "content"]);
-
- return items
- .map((item) => normalizePendingServiceRequest(item))
- .filter((item) => item.srNumber || item.customerName || item.lastUpdateDate)
- .sort((left, right) => {
- return (
- new Date(right.lastUpdateDate || 0).getTime() -
- new Date(left.lastUpdateDate || 0).getTime()
- );
- });
- }
-
- function normalizePendingServiceRequest(item) {
- return {
- customerName: cleanString(
- getFieldValue(item, [
- "CustomerAccount.PartyUniqueName",
- "CustomerAccount.PartyName",
- "CustomerAccount.Name",
- "customerName",
- ])
- ),
- lastUpdateDate: cleanString(getFieldValue(item, ["LastUpdateDate", "lastUpdateDate"])),
- srNumber: cleanString(getFieldValue(item, ["SrNumber", "srNumber"])),
- queueName: cleanString(
- getFieldValue(item, ["Queue.QueueName", "QueueName", "queueName"])
- ),
- requestedBy: cleanString(
- getFieldValue(item, [
- "ReportedByPartyId.PrimaryEmail.EmailAddress",
- "ReportedByPartyId.EmailAddress",
- "requestedBy",
- ])
- ),
- creationDate: cleanString(getFieldValue(item, ["CreationDate", "creationDate"])),
- opportunityNumber: cleanString(
- getFieldValue(item, [
- "OpportunityName_Id_c.OptyNumber",
- "OptyNumber",
- "opportunityNumber",
- ])
- ),
- srStatusLabel: cleanString(
- getFieldValue(item, ["StatusCd_localizedValue", "StatusCd", "srStatusLabel"])
- ),
- opportunityStatusLabel: cleanString(
- getFieldValue(item, [
- "OpportunityName_Id_c.StatusCode_localizedValue",
- "OpportunityName_Id_c.StatusCode",
- "opportunityStatusLabel",
- ])
- ),
- serviceName: cleanString(
- getFieldValue(item, [
- "ServiceNew_Id_c.RecordName",
- "ServiceNew_Id_c",
- "serviceName",
- ])
- ),
- country: cleanString(getFieldValue(item, ["CountryText_c", "country"])),
- totalHoursWorked: toNumber(
- getFieldValue(item, ["ReportedHours_c", "totalHoursWorked"])
- ),
- customerDeadline: cleanString(
- getFieldValue(item, ["CustomerDeadline_c", "customerDeadline"])
- ),
- opportunityValue: toNumber(
- getFieldValue(item, [
- "WorkloadAmountUSDWorkspace_c",
- "OpportunityName_Id_c.WorkloadAmountUSDWorkspace_c",
- "OpportunityName_Id_c.PrimaryRevenue.RevnAmount",
- "opportunityValue",
- ])
- ),
- pillarLabel: cleanString(
- getFieldValue(item, [
- "PrimaryPillarNew_c_localizedValue",
- "PrimaryPillarNew_c",
- "pillarLabel",
- ])
- ),
- };
- }
-
- function getFieldValue(source, paths) {
- if (!source || typeof source !== "object") {
- return "";
- }
-
- for (const path of paths) {
- if (Object.prototype.hasOwnProperty.call(source, path)) {
- return source[path];
- }
-
- const parts = String(path).split(".");
- let current = source;
-
- for (const part of parts) {
- if (!current || typeof current !== "object") {
- current = "";
- break;
- }
-
- current = current[part];
- }
-
- if (current !== undefined && current !== null && current !== "") {
- return current;
- }
- }
-
- return "";
- }
-
- async function hydrateDatasetFromCache() {
- if (appState.dataset || cacheHydrationPromise) {
- return cacheHydrationPromise;
- }
-
- appState.status = "loading";
- appState.errorMessage = "";
- appState.loadingMessage = "Loading cached snapshot";
- appState.loadingProgress = 18;
- renderModal();
-
- cacheHydrationPromise = (async () => {
- try {
- const exportPayload = await readCachedDataset();
- appState.loadingProgress = exportPayload ? 100 : 0;
-
- if (exportPayload) {
- appState.dataset = createDatasetSnapshot(exportPayload);
- appState.status = "ready";
- } else {
- appState.status = "idle";
- }
- } catch (error) {
- appState.status = "error";
- appState.errorMessage = getErrorMessage(error);
- } finally {
- appState.loadingMessage = "";
- appState.loadingProgress = 0;
- cacheHydrationPromise = null;
- renderModal();
- }
- })();
-
- return cacheHydrationPromise;
- }
-
- async function fetchCurrentUser() {
- const currentFrameToken = syncWorklistIframeToken({ force: true })?.token;
-
- const worklistToken = currentFrameToken || (await readCachedWorklistToken());
-
- if (!worklistToken) {
- throw new Error(
- "Unable to resolve Worklist token. Reload the FuseWelcome page so the Worklist iframe token can be captured before refreshing data."
- );
- }
-
- const url = new URL(API_ENDPOINTS.worklistCurrentUser);
- url.search = new URLSearchParams({
- source: "saasui",
- _: String(Date.now()),
- token: worklistToken,
- }).toString();
-
- appState.sessionAuthHeaders = {};
-
- const payload = await fetchJson(url.toString(), { forceExtension: true });
- const userPayload = normalizeWorklistUserPayload(payload);
- const firstName = cleanString(
- getFieldValue(userPayload, ["FirstName", "firstName", "User.FirstName"])
- );
- const lastName = cleanString(
- getFieldValue(userPayload, ["LastName", "lastName", "User.LastName"])
- );
- const userEmail = cleanString(
- getFieldValue(userPayload, [
- "Email",
- "email",
- "EmailAddress",
- "emailAddress",
- "User.Email",
- "User.email",
- "User.EmailAddress",
- ])
- );
- const displayName = firstNonEmptyString([
- `${firstName} ${lastName}`.trim(),
- userPayload?.DisplayName,
- userPayload?.displayName,
- userEmail,
- ]);
-
- if (!userEmail) {
- const userKeys =
- userPayload && typeof userPayload === "object"
- ? Object.keys(userPayload).join(", ")
- : "";
- throw new Error(
- `Unable to resolve userEmail from Worklist user response. Available top-level keys: ${Object.keys(
- payload || {}
- ).join(", ")}${userKeys ? ` | User keys: ${userKeys}` : ""}`
- );
- }
-
- appState.sessionAuthHeaders = {};
-
- return {
- userEmail,
- name: displayName || userEmail,
- avatar: cleanString(getFieldValue(userPayload, ["ImageURL", "imageUrl", "User.ImageURL"])),
- administrator: Boolean(
- getFieldValue(userPayload, ["Administrator", "administrator", "User.Administrator"])
- ),
- };
- }
-
- function normalizeWorklistUserPayload(payload) {
- const candidate = payload?.User ?? payload?.user ?? payload;
-
- if (Array.isArray(candidate)) {
- return candidate[0] || {};
- }
-
- if (candidate?.User) {
- return normalizeWorklistUserPayload(candidate);
- }
-
- return candidate || {};
- }
-
- async function fetchResourceCurrentUser(userEmail) {
- const normalizedUserEmail = cleanString(userEmail);
-
- if (!normalizedUserEmail) {
- throw new Error("Unable to request resource current user without userEmail.");
- }
-
- const url = `${COMCIP_REQUEST.resourceCurrentUserUrl}?emailAddress=${encodeURIComponent(
- normalizedUserEmail
- )}`;
- 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(
- payloadMessage ||
- response?.error ||
- `COMCIP resource user request failed (${response?.status || 0}).`
- );
- }
-
- return response.payload;
- }
-
-
- async function fetchAllCustomers(userEmail) {
- const limit = 49;
- let offset = 0;
- const customers = [];
- const seen = new Set();
-
- while (true) {
- const url = new URL(API_ENDPOINTS.customerSummary, TARGET_ORIGIN);
- url.search = new URLSearchParams({
- emailAddress: userEmail,
- projectionPeriod: "ALL",
- recordSet: "IAM_ON_SR_TEAM",
- growth: "All",
- projectedGrowth: "All",
- attainmentBand: "All",
- projectedConsumption: "All",
- customerStatus: "All",
- workloadStatus: "All",
- lastProjectionUpdate: "All",
- revenueForecastType: "All",
- customerProgramCode: "All",
- planType: "All",
- workLoadLivePeriod: "All",
- activeConsumptionOnly: "false",
- favorite: "false",
- workloadDeleted: "false",
- awsFlag: "N",
- googleFlag: "N",
- azureFlag: "N",
- excludePayAsYouGoOnly: "false",
- projectionReview: "N",
- pillarFilter: "dpOci",
- sortBy: "CUSTOMER_NAME",
- sortOrder: "ASC",
- limit: String(limit),
- offset: String(offset),
- }).toString();
-
- const payload = await fetchJson(url.toString());
- const items = extractArray(payload, [
- "items",
- "customers",
- "content",
- "results",
- "records",
- "data",
- ]);
-
- const mapped = items
- .map((item) => ({
- customerId: cleanString(item?.id),
- name: item?.name || "",
- }))
- .filter((item) => item.customerId);
-
- for (const item of mapped) {
- if (!seen.has(item.customerId)) {
- seen.add(item.customerId);
- customers.push(item);
- }
- }
-
- if (mapped.length < limit) {
- break;
- }
-
- offset += limit;
- }
-
- return customers;
- }
-
- async function fetchCustomerWorkloads(customer) {
- const url = new URL(
- `${API_ENDPOINTS.customerWorkloads}/${encodeURIComponent(
- customer.customerId
- )}/workloads`,
- TARGET_ORIGIN
- );
- url.search = new URLSearchParams({
- workloadStatus: "Pipeline,Implementation,Live",
- opportunityForecastType: "All",
- includeForEstimates: "ALL",
- financialYear: "ROLLING_4_QTRS",
- }).toString();
-
- const payload = await fetchJson(url.toString());
- const items = extractArray(payload, [
- "items",
- "workloads",
- "content",
- "results",
- "data",
- ]);
-
- return items
- .map((item) => normalizeWorkload(item))
- .filter(Boolean)
- .filter((workload) => !isWonWorkload(workload));
- }
-
- async function fetchWorkloadActions(workloadId) {
- const url = new URL(API_ENDPOINTS.workloadActions, TARGET_ORIGIN);
- url.search = new URLSearchParams({
- parentType: "WORKLOAD",
- parentId: workloadId,
- }).toString();
-
- const payload = await fetchJson(url.toString());
- const items = extractArray(payload, ["items", "actions", "content", "results", "data"]);
-
- return items.map((item) => ({
- owner: item?.owner || "",
- role: item?.role || "",
- name: item?.name || "",
- team: normalizeTeamValue(item?.team),
- startDate: item?.startDate || "",
- endDate: item?.endDate || "",
- complexity: item?.complexity || "",
- }));
- }
-
- async function fetchWorkloadServiceRequests(workloadId) {
- const url = new URL(
- `${API_ENDPOINTS.workloadServiceRequests}/${encodeURIComponent(
- workloadId
- )}/serviceRequests`,
- TARGET_ORIGIN
- );
-
- const payload = await fetchJson(url.toString());
- const items = extractArray(payload, [
- "items",
- "serviceRequests",
- "content",
- "results",
- "data",
- ]);
-
- return items.map((item) => ({
- srNumber: item?.srNumber || "",
- status: item?.status || "",
- team: normalizeTeamValue(item?.team),
- }));
- }
-
- async function fetchJson(url, options = {}) {
- if (options.forceExtension) {
- return fetchJsonViaExtension(url, options);
- }
-
- if (shouldUseSpaFrameFetch(url) && !options.forceExtension) {
- return fetchJsonViaSpaFrame(url, options);
- }
-
- if (shouldUseExtensionFetch(url) && !options.forcePageBridge) {
- return fetchJsonViaExtension(url, options);
- }
-
- await ensurePageBridge();
-
- const requestId = `${Date.now()}-${++requestCounter}`;
- const requestUrl = toBridgeRequestUrl(url);
-
- const response = await new Promise((resolve, reject) => {
- const timeout = window.setTimeout(() => {
- pendingBridgeRequests.delete(requestId);
- reject(new Error(`Timed out while requesting ${requestUrl}`));
- }, 60000);
-
- pendingBridgeRequests.set(requestId, {
- resolve,
- reject,
- timeout,
- });
-
- window.postMessage(
- {
- source: BRIDGE.source,
- type: BRIDGE.fetchType,
- requestId,
- url: requestUrl,
- options: {
- method: options.method || "GET",
- transport: options.transport || "fetch",
- referrer: options.referrer,
- headers: {
- accept: "application/json, text/plain, */*",
- ...(options.headers || {}),
- },
- body: options.body,
- },
- },
- window.location.origin
- );
- });
-
- 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,
- ]);
- const debugHeaderKeys = Array.isArray(response.debug?.headerKeys)
- ? response.debug.headerKeys.join(", ")
- : "";
- const debugScopeKeys = Array.isArray(response.debug?.scopeKeys)
- ? response.debug.scopeKeys.join(", ")
- : "";
- const debugParts = [
- debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
- debugScopeKeys ? `scopes: ${debugScopeKeys}` : "",
- ].filter(Boolean);
- const debugSuffix = debugParts.length > 0
- ? ` | ${debugParts.join(" | ")}`
- : "";
-
- throw new Error(
- `${payloadMessage || response.error || `Request failed (${response.status}) for ${requestUrl}`}${debugSuffix}`
- );
- }
-
- return response.payload;
- }
-
- function shouldUseExtensionFetch(url) {
- const normalized = resolveRequestUrl(url);
-
- if (!normalized) {
- return false;
- }
-
- try {
- const parsedUrl = new URL(normalized, window.location.origin);
- return parsedUrl.origin !== window.location.origin;
- } catch {
- return false;
- }
- }
-
- function shouldUseSpaFrameFetch(url) {
- if (!isHcmWelcomePage()) {
- return false;
- }
-
- const normalized = resolveRequestUrl(url);
-
- if (!normalized) {
- return false;
- }
-
- try {
- return new URL(normalized).origin === TARGET_ORIGIN;
- } catch {
- return false;
- }
- }
-
- async function fetchJsonViaSpaFrame(url, options = {}) {
- const requestUrl = resolveRequestUrl(url);
- const bridgeHeaders = await requestBridgeHeaders({
- url: requestUrl,
- headers: options.headers,
- body: options.body,
- includeAllAuth: true,
- }).catch(() => ({}));
-
- await ensureSpaFrame();
-
- const response = await new Promise((resolve, reject) => {
- chrome.runtime.sendMessage(
- {
- type: "ARCH_PANEL_SPA_FRAME_FETCH",
- url: requestUrl,
- options: {
- method: options.method || "GET",
- referrer: options.referrer,
- headers: buildExtensionFetchHeaders(
- requestUrl,
- options,
- bridgeHeaders || {}
- ),
- body: options.body,
- },
- },
- (result) => {
- const runtimeError = chrome.runtime.lastError;
-
- if (runtimeError) {
- reject(new Error(runtimeError.message));
- return;
- }
-
- resolve(result || null);
- }
- );
- });
-
- if (!response || typeof response !== "object") {
- throw new Error(`No response received for ${requestUrl}`);
- }
-
- if (
- !response.ok &&
- response.status === 0 &&
- /SPA Workbench frame did not return a result/i.test(cleanString(response.error))
- ) {
- throw new Error(response.error || "SPA Workbench iframe did not return a result.");
- }
-
- 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,
- ]);
- const debugHeaderKeys = Array.isArray(response.debug?.headerKeys)
- ? response.debug.headerKeys.join(", ")
- : "";
- const debugFrameUrl = cleanString(response.debug?.frameUrl);
- const debugUrl = cleanString(response.debug?.url);
- const debugParts = [
- debugUrl ? `url: ${debugUrl}` : "",
- debugFrameUrl ? `frame: ${debugFrameUrl}` : "",
- debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
- ].filter(Boolean);
- const debugSuffix = debugParts.length > 0
- ? ` | ${debugParts.join(" | ")}`
- : "";
-
- throw new Error(
- `${
- payloadMessage ||
- response.error ||
- `Request failed (${response.status}) for ${requestUrl}`
- }${debugSuffix}`
- );
- }
-
- return response.payload;
- }
-
- function ensureSpaFrame() {
- const frameHost = ensureSpaFrameHost();
- const existingFrame = document.getElementById(IDS.spaFrame);
- const frameUrl = TARGET_APP_URL;
-
- if (existingFrame instanceof HTMLIFrameElement) {
- if (existingFrame.parentElement !== frameHost) {
- frameHost.appendChild(existingFrame);
- }
-
- if (shouldNavigateSpaFrame(existingFrame, frameUrl)) {
- navigateSpaFrame(existingFrame, frameUrl);
- }
-
- return waitForFrameLoad(existingFrame);
- }
-
- const frame = document.createElement("iframe");
- frame.id = IDS.spaFrame;
- frame.name = IDS.spaFrame;
- frame.dataset.archPanelFrame = "spa-workbench";
- frame.setAttribute("aria-hidden", "true");
- frame.setAttribute("referrerpolicy", "no-referrer-when-downgrade");
- frame.tabIndex = -1;
- Object.assign(frame.style, {
- position: "absolute",
- width: "1440px",
- height: "1000px",
- opacity: "0",
- pointerEvents: "none",
- border: "0",
- left: "0",
- top: "0",
- });
-
- frameHost.appendChild(frame);
- navigateSpaFrame(frame, frameUrl);
-
- return waitForFrameLoad(frame);
- }
-
- function ensureSpaFrameHost() {
- const existingHost = document.getElementById(IDS.spaFrameHost);
-
- if (existingHost) {
- const desiredParent = document.body || document.documentElement;
-
- if (desiredParent && existingHost.parentElement !== desiredParent) {
- desiredParent.appendChild(existingHost);
- }
-
- Object.assign(existingHost.style, {
- position: "fixed",
- width: "1440px",
- height: "1000px",
- overflow: "hidden",
- opacity: "0",
- pointerEvents: "none",
- border: "0",
- left: "-20000px",
- top: "0",
- zIndex: "0",
- });
-
- return existingHost;
- }
-
- const host = document.createElement("div");
- host.id = IDS.spaFrameHost;
- host.setAttribute("aria-hidden", "true");
- Object.assign(host.style, {
- position: "fixed",
- width: "1440px",
- height: "1000px",
- overflow: "hidden",
- opacity: "0",
- pointerEvents: "none",
- border: "0",
- left: "-20000px",
- top: "0",
- zIndex: "0",
- });
-
- (document.body || document.documentElement).appendChild(host);
-
- return host;
- }
-
- function shouldNavigateSpaFrame(frame, frameUrl) {
- const currentSrc = cleanString(frame.getAttribute("src") || frame.src);
-
- if (!currentSrc || currentSrc === "about:blank") {
- return true;
- }
-
- try {
- return new URL(currentSrc, window.location.href).toString() !== frameUrl;
- } catch {
- return currentSrc !== frameUrl;
- }
- }
-
- function navigateSpaFrame(frame, frameUrl) {
- frame.dataset.archPanelLoaded = "false";
- frame.src = frameUrl;
- }
-
- function waitForFrameLoad(frame) {
- if (frame.dataset.archPanelLoaded === "true") {
- return Promise.resolve();
- }
-
- return new Promise((resolve) => {
- const timeout = window.setTimeout(() => {
- resolve();
- }, 12000);
-
- frame.addEventListener(
- "load",
- () => {
- frame.dataset.archPanelLoaded = "true";
- window.clearTimeout(timeout);
- window.setTimeout(resolve, 1200);
- },
- { once: true }
- );
- });
- }
-
- async function fetchJsonViaExtension(url, options = {}) {
- const requestUrl = resolveRequestUrl(url);
- const bridgeHeaders = await requestBridgeHeaders({
- url: requestUrl,
- headers: options.headers,
- body: options.body,
- includeAllAuth: true,
- });
- const response = await new Promise((resolve, reject) => {
- chrome.runtime.sendMessage(
- {
- type: "ARCH_PANEL_EXTENSION_FETCH",
- url: requestUrl,
- options: {
- method: options.method || "GET",
- referrer: options.referrer,
- headers: buildExtensionFetchHeaders(
- requestUrl,
- options,
- bridgeHeaders || {}
- ),
- body: options.body,
- },
- },
- (result) => {
- const runtimeError = chrome.runtime.lastError;
-
- if (runtimeError) {
- reject(new Error(runtimeError.message));
- return;
- }
-
- resolve(result || null);
- }
- );
- });
-
- if (!response || typeof response !== "object") {
- throw new Error(`No response received for ${requestUrl}`);
- }
-
- 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,
- ]);
- const debugHeaderKeys = Array.isArray(response.debug?.headerKeys)
- ? response.debug.headerKeys.join(", ")
- : "";
- const debugUrl = cleanString(response.debug?.url);
- const debugParts = [
- debugUrl ? `url: ${debugUrl}` : "",
- debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
- ].filter(Boolean);
- const debugSuffix = debugParts.length > 0
- ? ` | ${debugParts.join(" | ")}`
- : "";
-
- throw new Error(
- `${
- payloadMessage ||
- response.error ||
- `Request failed (${response.status}) for ${requestUrl}`
- }${debugSuffix}`
- );
- }
-
- return response.payload;
- }
-
- function buildExtensionFetchHeaders(requestUrl, options = {}, bridgeHeaders = {}) {
- const combinedHeaders = {
- ...(bridgeHeaders || {}),
- ...(appState.sessionAuthHeaders || {}),
- accept: "application/json, text/plain, */*",
- ...(options.headers || {}),
- };
-
- return options.omitAuthHeaders ? stripAuthHeaders(combinedHeaders) : combinedHeaders;
- }
-
- function stripAuthHeaders(headers = {}) {
- const sanitizedHeaders = {};
-
- for (const [key, value] of Object.entries(headers || {})) {
- const normalizedKey = cleanString(key).toLowerCase();
-
- if (
- normalizedKey === "authorization" ||
- normalizedKey === "x-id-token" ||
- normalizedKey === "spa-ts-authorization" ||
- normalizedKey.endsWith("-authorization")
- ) {
- continue;
- }
-
- sanitizedHeaders[key] = value;
- }
-
- return sanitizedHeaders;
- }
-
- async function requestBridgeHeaders(options = {}) {
- await ensurePageBridge();
-
- const requestId = `${Date.now()}-${++requestCounter}`;
-
- return new Promise((resolve, reject) => {
- const timeout = window.setTimeout(() => {
- pendingBridgeRequests.delete(requestId);
- reject(new Error("Timed out while requesting bridge headers"));
- }, 30000);
-
- pendingBridgeRequests.set(requestId, {
- resolve,
- reject,
- timeout,
- });
-
- window.postMessage(
- {
- source: BRIDGE.source,
- type: BRIDGE.headersType,
- requestId,
- options,
- },
- window.location.origin
- );
- });
- }
-
- function normalizeWorkload(item) {
- const workloadId = cleanString(item?.workloadId || item?.id);
-
- if (!workloadId) {
- return null;
- }
-
- const opportunityForecastType = item?.opportunityForecastType || "";
- const opportunityForecastTypeGroup = normalizeForecastType(
- opportunityForecastType
- );
-
- return {
- workloadId,
- name: item?.name || "",
- description: item?.description || "",
- consumptionStartDate: item?.consumptionStartDate || "",
- workloadStatus: item?.workloadStatus || "",
- opportunityForecastType,
- opportunityForecastTypeGroup,
- opportunityId: cleanString(item?.opportunityId),
- opportunityNumber: item?.opportunityNumber || "",
- adjustedACR: toNumber(item?.adjustedACR),
- rampMonths: toNumber(item?.rampMonths),
- forecast: extractArray(item?.forecast, ["items", "content", "results", "data"]).map(
- (forecastItem) => ({
- month: forecastItem?.month || "",
- year: toNumber(forecastItem?.year),
- projectedConsumptionAmount: toNumber(
- forecastItem?.projectedConsumptionAmount
- ),
- adjustedConsumptionAmount: toNumber(
- forecastItem?.adjustedConsumptionAmount
- ),
- createdBy: forecastItem?.createdBy || "",
- createdDate: forecastItem?.createdDate || "",
- updatedBy: forecastItem?.updatedBy || "",
- updatedDate: forecastItem?.updatedDate || "",
- })
- ),
- };
- }
-
- function isWonWorkload(workload) {
- const forecastType = normalizeString(workload.opportunityForecastTypeGroup);
- const workloadStatus = normalizeString(workload.workloadStatus);
-
- return forecastType === "WON" || workloadStatus === "WON";
- }
-
- function normalizeForecastType(value) {
- const text = String(value || "").trim();
-
- if (!text) {
- return "Unknown";
- }
-
- const [head] = text.split(" -");
- return head.trim() || "Unknown";
- }
-
- function normalizeTeamValue(value) {
- if (Array.isArray(value)) {
- return value.map((item) => normalizeTeamValue(item)).join(" | ");
- }
-
- if (value && typeof value === "object") {
- return Object.values(value).map((item) => normalizeTeamValue(item)).join(" | ");
- }
-
- return String(value || "");
- }
-
- function teamContainsUser(team, userEmail) {
- return normalizeString(team).includes(normalizeString(userEmail));
- }
-
- function stripTransientFields(workload) {
- return {
- workloadId: workload.workloadId,
- name: workload.name,
- description: workload.description,
- consumptionStartDate: workload.consumptionStartDate,
- workloadStatus: workload.workloadStatus,
- opportunityForecastType: workload.opportunityForecastType,
- opportunityForecastTypeGroup: workload.opportunityForecastTypeGroup,
- opportunityId: workload.opportunityId,
- opportunityNumber: workload.opportunityNumber,
- adjustedACR: workload.adjustedACR,
- rampMonths: workload.rampMonths,
- forecast: workload.forecast,
- customerId: workload.customerId,
- customerName: workload.customerName,
- actions: workload.actions,
- serviceRequests: workload.serviceRequests,
- hasSR: workload.hasSR,
- hasAction: workload.hasAction,
- };
- }
-
- function createExportPayload(
- user,
- customers,
- pendingServiceRequests = [],
- resourceCurrentUser = null
- ) {
- return {
- generatedAt: new Date().toISOString(),
- user,
- resourceCurrentUser,
- customers,
- pendingServiceRequests,
- };
- }
-
- function createDatasetSnapshot(exportPayload) {
- const normalizedPayload = normalizeCachedExportPayload(exportPayload);
-
- return {
- user: normalizedPayload.user,
- resourceCurrentUser: normalizedPayload.resourceCurrentUser,
- customers: normalizedPayload.customers,
- exportPayload: normalizedPayload,
- summary: buildSummary(normalizedPayload),
- };
- }
-
- function findWorkloadById(workloadId) {
- if (!appState.dataset || !workloadId) {
- return null;
- }
-
- for (const customer of appState.dataset.customers || []) {
- for (const workload of customer.workloads || []) {
- if (workload.workloadId === workloadId) {
- return {
- ...workload,
- customerId: workload.customerId || customer.customerId,
- customerName: workload.customerName || customer.name,
- };
- }
- }
- }
-
- return null;
- }
-
- function applyForecastUpdateToDataset(workloadId, payload) {
- if (!appState.dataset || !workloadId) {
- return;
- }
-
- const previousDetailModal = appState.detailModal
- ? {
- scope: appState.detailModal.scope,
- value: appState.detailModal.value,
- sortKey: appState.detailModal.sortKey,
- sortDirection: appState.detailModal.sortDirection,
- type: appState.detailModal.type,
- }
- : null;
- const updatesByPeriod = new Map(
- (payload || []).map((item) => [
- `${toNumber(item.year)}-${toNumber(item.month)}`,
- toNumber(item.amount),
- ])
- );
-
- function updateForecast(workload) {
- if (!workload || workload.workloadId !== workloadId) {
- return workload;
- }
-
- workload.forecast = (workload.forecast || []).map((forecastItem) => {
- const month = parseForecastMonthNumber(forecastItem.month);
- const year = toNumber(forecastItem.year);
- const key = `${year}-${month}`;
-
- if (!updatesByPeriod.has(key)) {
- return forecastItem;
- }
-
- return {
- ...forecastItem,
- adjustedConsumptionAmount: updatesByPeriod.get(key),
- updatedBy: appState.dataset.user?.userEmail || forecastItem.updatedBy,
- updatedDate: new Date().toISOString(),
- };
- });
-
- return workload;
- }
-
- for (const customer of appState.dataset.customers || []) {
- customer.workloads = (customer.workloads || []).map((workload) =>
- updateForecast(workload)
- );
- }
-
- for (const customer of appState.dataset.exportPayload?.customers || []) {
- customer.workloads = (customer.workloads || []).map((workload) =>
- updateForecast(workload)
- );
- }
-
- appState.dataset = createDatasetSnapshot(appState.dataset.exportPayload);
-
- if (previousDetailModal?.scope && previousDetailModal.type !== "serviceRequests") {
- restoreDetailModal(previousDetailModal);
- }
-
- void writeCachedDataset(appState.dataset.exportPayload);
- }
-
- function normalizeCachedExportPayload(exportPayload) {
- const normalizedUser = {
- userEmail: cleanString(exportPayload?.user?.userEmail),
- name:
- firstNonEmptyString([
- exportPayload?.user?.name,
- exportPayload?.user?.displayName,
- exportPayload?.user?.fullName,
- exportPayload?.user?.profile?.name,
- exportPayload?.user?.userEmail,
- ]) || "",
- avatar: cleanString(exportPayload?.user?.avatar || exportPayload?.user?.imageUrl),
- administrator: Boolean(exportPayload?.user?.administrator),
- };
- const resourceCurrentUser = exportPayload?.resourceCurrentUser ?? null;
- const normalizedCustomers = extractArray(exportPayload?.customers, ["customers"])
- .map((customer) => ({
- customerId: cleanString(customer?.customerId || customer?.id),
- name: cleanString(customer?.name),
- workloads: extractArray(customer?.workloads, ["workloads"])
- .map((workload) => stripTransientFields(normalizeWorkloadForCache(workload)))
- .filter((workload) => workload.workloadId),
- }))
- .filter((customer) => customer.customerId);
- const pendingServiceRequests = normalizePendingServiceRequests(
- extractArray(exportPayload?.pendingServiceRequests, [
- "pendingServiceRequests",
- "items",
- "data",
- "results",
- ])
- );
-
- return {
- generatedAt:
- firstNonEmptyString([exportPayload?.generatedAt]) || new Date().toISOString(),
- user: normalizedUser,
- resourceCurrentUser,
- customers: normalizedCustomers,
- pendingServiceRequests,
- };
- }
-
- function normalizeWorkloadForCache(workload) {
- const normalized = {
- workloadId: cleanString(workload?.workloadId),
- name: cleanString(workload?.name),
- description: cleanString(workload?.description),
- consumptionStartDate: workload?.consumptionStartDate || "",
- workloadStatus: cleanString(workload?.workloadStatus),
- opportunityForecastType: cleanString(workload?.opportunityForecastType),
- opportunityForecastTypeGroup:
- cleanString(workload?.opportunityForecastTypeGroup) ||
- normalizeOpportunityForecastType(workload?.opportunityForecastType),
- opportunityId: cleanString(workload?.opportunityId),
- opportunityNumber: cleanString(workload?.opportunityNumber),
- adjustedACR: toNumber(workload?.adjustedACR),
- rampMonths: toNumber(workload?.rampMonths),
- forecast: extractArray(workload?.forecast, ["forecast"]).map((entry) => ({
- month: entry?.month || "",
- year: entry?.year || "",
- projectedConsumptionAmount: toNumber(entry?.projectedConsumptionAmount),
- adjustedConsumptionAmount: toNumber(entry?.adjustedConsumptionAmount),
- createdBy: entry?.createdBy || "",
- createdDate: entry?.createdDate || "",
- updatedBy: entry?.updatedBy || "",
- updatedDate: entry?.updatedDate || "",
- })),
- customerId: cleanString(workload?.customerId),
- customerName: cleanString(workload?.customerName),
- actions: extractArray(workload?.actions, ["actions"]).map((action) => ({
- owner: cleanString(action?.owner),
- role: cleanString(action?.role),
- name: cleanString(action?.name),
- team: normalizeTeamValue(action?.team),
- startDate: action?.startDate || "",
- endDate: action?.endDate || "",
- complexity: cleanString(action?.complexity),
- })),
- serviceRequests: extractArray(workload?.serviceRequests, [
- "serviceRequests",
- ]).map((serviceRequest) => ({
- srNumber: cleanString(serviceRequest?.srNumber),
- status: cleanString(serviceRequest?.status),
- team: normalizeTeamValue(serviceRequest?.team),
- })),
- hasSR: Boolean(workload?.hasSR),
- hasAction: Boolean(workload?.hasAction),
- };
-
- return normalized;
- }
-
- function openCacheDatabase() {
- return new Promise((resolve, reject) => {
- if (!window.indexedDB) {
- reject(new Error("IndexedDB is not available in this browser context."));
- return;
- }
-
- const request = window.indexedDB.open(CACHE.dbName, CACHE.dbVersion);
-
- request.onupgradeneeded = () => {
- const database = request.result;
-
- if (!database.objectStoreNames.contains(CACHE.storeName)) {
- database.createObjectStore(CACHE.storeName, { keyPath: "key" });
- }
- };
-
- request.onsuccess = () => {
- resolve(request.result);
- };
-
- request.onerror = () => {
- reject(request.error || new Error("Unable to open IndexedDB cache."));
- };
- });
- }
-
- async function withCacheStore(mode, callback) {
- const database = await openCacheDatabase();
-
- return new Promise((resolve, reject) => {
- const transaction = database.transaction(CACHE.storeName, mode);
- const store = transaction.objectStore(CACHE.storeName);
-
- transaction.oncomplete = () => {
- database.close();
- };
-
- transaction.onerror = () => {
- const error =
- transaction.error || new Error("IndexedDB transaction failed.");
- database.close();
- reject(error);
- };
-
- transaction.onabort = () => {
- const error =
- transaction.error || new Error("IndexedDB transaction was aborted.");
- database.close();
- reject(error);
- };
-
- callback(store, resolve, reject);
- });
- }
-
- async function readCachedDataset() {
- const record = await withCacheStore("readonly", (store, resolve, reject) => {
- const request = store.get(CACHE.datasetKey);
-
- request.onsuccess = () => {
- const payload =
- request.result?.exportPayload ||
- request.result?.payload ||
- request.result?.dataset?.exportPayload ||
- request.result?.dataset ||
- null;
-
- resolve(payload);
- };
-
- request.onerror = () => {
- reject(request.error || new Error("Unable to read the cached dataset."));
- };
- });
-
- return record ? normalizeCachedExportPayload(record) : null;
- }
-
- async function writeCachedDataset(exportPayload) {
- const normalizedPayload = normalizeCachedExportPayload(exportPayload);
-
- await withCacheStore("readwrite", (store, resolve, reject) => {
- const request = store.put({
- key: CACHE.datasetKey,
- updatedAt: new Date().toISOString(),
- exportPayload: normalizedPayload,
- });
-
- request.onsuccess = () => {
- resolve();
- };
-
- request.onerror = () => {
- reject(request.error || new Error("Unable to write the cached dataset."));
- };
- });
- }
-
- 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."));
- };
- });
- }
-
- async function writeCachedWorklistToken(tokenPayload) {
- const token = cleanString(tokenPayload?.token);
-
- if (!token) {
- return;
- }
-
- await withCacheStore("readwrite", (store, resolve, reject) => {
- const request = store.put({
- key: CACHE.worklistTokenKey,
- updatedAt: new Date().toISOString(),
- token,
- sourceUrl: cleanString(tokenPayload?.url),
- });
-
- request.onsuccess = () => {
- resolve();
- };
-
- request.onerror = () => {
- reject(request.error || new Error("Unable to write the cached Worklist token."));
- };
- });
- }
-
- async function readCachedWorklistToken() {
- const record = await withCacheStore("readonly", (store, resolve, reject) => {
- const request = store.get(CACHE.worklistTokenKey);
-
- request.onsuccess = () => {
- resolve(request.result || null);
- };
-
- request.onerror = () => {
- reject(request.error || new Error("Unable to read the cached Worklist token."));
- };
- });
-
- return cleanString(record?.token);
- }
-
- function buildSummary(exportPayload) {
- const customers = exportPayload.customers || [];
- const allWorkloads = customers.flatMap((customer) => customer.workloads || []);
- const eligibleWorkloads = allWorkloads
- .filter((workload) =>
- ALLOWED_STAGE_TYPES.includes(workload.opportunityForecastTypeGroup)
- )
- .sort((left, right) => right.adjustedACR - left.adjustedACR);
- const totalEligibleAcr = eligibleWorkloads.reduce((sum, workload) => {
- return sum + workload.adjustedACR;
- }, 0);
- const stageIndicators = ALLOWED_STAGE_TYPES.map((label) => {
- const items = eligibleWorkloads.filter(
- (workload) => workload.opportunityForecastTypeGroup === label
- );
-
- return {
- label,
- count: items.length,
- totalAcr: items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
- };
- });
- const srStatus = {
- trueCount: eligibleWorkloads.filter((workload) => workload.hasSR).length,
- falseCount: eligibleWorkloads.filter((workload) => !workload.hasSR).length,
- };
- const actionStatus = {
- trueCount: eligibleWorkloads.filter((workload) => workload.hasAction).length,
- falseCount: eligibleWorkloads.filter((workload) => !workload.hasAction).length,
- };
- const pendingServiceRequests = exportPayload.pendingServiceRequests || [];
- const srHours = {
- betweenZeroAndFour: pendingServiceRequests.filter((serviceRequest) => {
- const hours = toNumber(serviceRequest.totalHoursWorked);
- return hours >= 0 && hours <= 4;
- }),
- aboveFour: pendingServiceRequests.filter((serviceRequest) => {
- return toNumber(serviceRequest.totalHoursWorked) > 4;
- }),
- };
-
- return {
- generatedAtLabel: formatDateTime(exportPayload.generatedAt),
- totalCustomers: customers.length,
- totalWorkloads: allWorkloads.length,
- allWorkloads,
- eligibleWorkloads,
- totalEligibleAcr,
- stageIndicators,
- srStatus,
- actionStatus,
- pendingServiceRequests,
- srHours,
- };
- }
-
- function buildWorkloadStartCalendar(
- workloads,
- year,
- month,
- view = "month",
- anchorDateKey = ""
- ) {
- const today = new Date();
- const normalizedView = view === "week" ? "week" : "month";
- const monthStart = new Date(year, month, 1);
- const anchorDate = getCalendarAnchorDate(year, month, anchorDateKey);
- const calendarStart =
- normalizedView === "week"
- ? getWeekStartDate(anchorDate)
- : getWeekStartDate(monthStart);
- const dayCount = normalizedView === "week" ? 7 : 42;
- const workloadMap = new Map();
-
- for (const workload of workloads || []) {
- const dateKey = getLocalDateKey(workload.consumptionStartDate);
-
- if (!dateKey) {
- continue;
- }
-
- const bucket = workloadMap.get(dateKey) || [];
- bucket.push(workload);
- workloadMap.set(dateKey, bucket);
- }
-
- const days = Array.from({ length: dayCount }, (_, index) => {
- const date = new Date(calendarStart);
- date.setDate(calendarStart.getDate() + index);
- const dateKey = getLocalDateKey(date);
- const workloadsForDay = workloadMap.get(dateKey) || [];
-
- return {
- dateKey,
- dayNumber: date.getDate(),
- isWeekStart: index % 7 === 0,
- operationalWeekLabel: formatOperationalWeekLabel(
- getOperationalWeekInfo(getCalendarRowWeekDate(date))
- ),
- isCurrentMonth: normalizedView === "week" || date.getMonth() === month,
- isToday: dateKey === getLocalDateKey(today),
- workloads: workloadsForDay,
- };
- });
- const weekEnd = new Date(calendarStart);
- weekEnd.setDate(calendarStart.getDate() + 6);
- const monthLabel = new Intl.DateTimeFormat("en-US", {
- month: "long",
- year: "numeric",
- }).format(monthStart);
- const weekLabel = `${formatDate(calendarStart)} - ${formatDate(weekEnd)}`;
-
- return {
- view: normalizedView,
- periodLabel: normalizedView === "week" ? weekLabel : monthLabel,
- days,
- totalVisibleWorkloads: days.reduce(
- (sum, day) => sum + day.workloads.length,
- 0
- ),
- };
- }
-
- function getCalendarAnchorDate(year, month, anchorDateKey) {
- const anchorDate = parseDatePreservingDateOnly(anchorDateKey);
-
- if (!Number.isNaN(anchorDate.getTime())) {
- return anchorDate;
- }
-
- return new Date(year, month, 1);
- }
-
- function getWeekStartDate(value) {
- const date = parseDatePreservingDateOnly(value);
- const weekStart = Number.isNaN(date.getTime()) ? new Date() : new Date(date);
-
- weekStart.setDate(weekStart.getDate() - weekStart.getDay());
- return weekStart;
- }
-
- function openDetailModal(scope, value) {
- if (!appState.dataset) {
- return;
- }
-
- const eligibleWorkloads = appState.dataset.summary.eligibleWorkloads;
- let items = [];
- let label = "";
- let title = "";
-
- if (scope === "stage") {
- items = eligibleWorkloads.filter(
- (workload) => workload.opportunityForecastTypeGroup === value
- );
- label = `Opportunity type: ${value}`;
- title = `${value} workloads`;
- } else if (scope === "sr") {
- const flag = value === "true";
- items = eligibleWorkloads.filter((workload) => workload.hasSR === flag);
- label = "Status de SRs";
- title = flag ? "Workloads com SR" : "Workloads sem SR";
- } else if (scope === "action") {
- const flag = value === "true";
- items = eligibleWorkloads.filter((workload) => workload.hasAction === flag);
- label = "Status de Consumption Plan/action";
- title = flag ? "Workloads com action" : "Workloads sem action";
- } else if (scope === "sr-hours") {
- const pendingServiceRequests = appState.dataset.summary.pendingServiceRequests || [];
- const isAboveFour = value === "above-4";
- items = pendingServiceRequests.filter((serviceRequest) => {
- const hours = toNumber(serviceRequest.totalHoursWorked);
- return isAboveFour ? hours > 4 : hours >= 0 && hours <= 4;
- });
- items.sort((left, right) => {
- return toNumber(left.totalHoursWorked) - toNumber(right.totalHoursWorked);
- });
- label = "SRs por horas reportadas";
- title = isAboveFour ? "SRs acima de 4h" : "SRs entre 0 e 4h";
- } else if (scope === "calendar-day") {
- const allWorkloads = appState.dataset.summary.allWorkloads || [];
- items = allWorkloads.filter((workload) => {
- return getLocalDateKey(workload.consumptionStartDate) === value;
- });
- label = "Workload start calendar";
- title = `Workloads starting ${formatDate(value) || value}`;
- }
-
- appState.detailModal = {
- label,
- title,
- items,
- totalAcr:
- scope === "sr-hours"
- ? 0
- : items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
- sortKey: DETAIL_SORT_KEYS.adjustedACR,
- sortDirection: "desc",
- scope,
- value,
- type: scope === "sr-hours" ? "serviceRequests" : "workloads",
- };
-
- renderModal();
- }
-
- function openActionFormModal(workloadId) {
- appState.actionFormModal = {
- workloadId,
- errorMessage: "",
- isSubmitting: false,
- };
- renderModal();
- }
-
- function openRampComparisonModal(workloadId) {
- const workload = findWorkloadById(workloadId);
-
- if (!workload) {
- return;
- }
-
- appState.rampComparisonModal = {
- workload,
- rows: buildRampForecastComparisonRows([workload]),
- };
- renderModal();
- }
-
- function openForecastUpdateConfirmModal() {
- if (!appState.rampComparisonModal || !appState.dataset) {
- return;
- }
-
- const payload = createForecastUpdatePayload(
- appState.rampComparisonModal.rows || []
- );
-
- appState.forecastUpdateConfirmModal = {
- workloadId: appState.rampComparisonModal.workload.workloadId,
- payload,
- isSubmitting: false,
- errorMessage: "",
- };
- renderModal();
- }
-
- async function submitForecastUpdate() {
- const modal = appState.forecastUpdateConfirmModal;
-
- if (!modal || modal.isSubmitting) {
- return;
- }
-
- appState.forecastUpdateConfirmModal = {
- ...modal,
- isSubmitting: true,
- errorMessage: "",
- };
- renderModal();
-
- try {
- const workloadId = cleanString(modal.workloadId);
- const url = `${API_ENDPOINTS.workloadServiceRequests}/${encodeURIComponent(
- workloadId
- )}/forecast`;
- const headers = {
- accept: "*/*",
- "content-type": "application/json",
- };
-
- await fetchJson(url, {
- method: "PATCH",
- omitAuthHeaders: true,
- headers,
- body: JSON.stringify(modal.payload),
- });
-
- applyForecastUpdateToDataset(workloadId, modal.payload);
- const updatedWorkload = findWorkloadById(workloadId);
-
- appState.forecastUpdateConfirmModal = null;
-
- if (updatedWorkload) {
- appState.rampComparisonModal = {
- workload: updatedWorkload,
- rows: buildRampForecastComparisonRows([updatedWorkload]),
- };
- }
-
- renderModal();
- } catch (error) {
- appState.forecastUpdateConfirmModal = {
- ...modal,
- isSubmitting: false,
- errorMessage: getErrorMessage(error),
- };
- renderModal();
- }
- }
-
- function createForecastUpdatePayload(rows) {
- const userEmail = appState.dataset?.user?.userEmail || "";
-
- return (rows || [])
- .filter((row) => row.year && row.month)
- .map((row) => ({
- year: toNumber(row.year),
- month: toNumber(row.month),
- amount: toNumber(row.calculatedValue),
- updatedBy: userEmail,
- }));
- }
-
- async function openTimeEntriesDrawer(srNumber) {
- const normalizedSrNumber = cleanString(srNumber);
-
- if (!normalizedSrNumber) {
- return;
- }
-
- appState.timeEntriesDrawer = {
- srNumber: normalizedSrNumber,
- status: "loading",
- errorMessage: "",
- payload: null,
- items: [],
- };
- renderModal();
-
- try {
- const response = await fetchTimeEntriesSummary(normalizedSrNumber);
-
- appState.timeEntriesDrawer = {
- srNumber: normalizedSrNumber,
- status: "ready",
- errorMessage: "",
- payload: response.payload,
- items: normalizeTimeEntriesSummary(response.payload),
- };
- } catch (error) {
- appState.timeEntriesDrawer = {
- srNumber: normalizedSrNumber,
- status: "error",
- errorMessage: getErrorMessage(error),
- payload: null,
- items: [],
- };
- }
-
- renderModal();
- }
-
- async function fetchTimeEntriesSummary(srNumber) {
- const url = `${COMCIP_REQUEST.timeEntriesSummaryUrl}?srNumber=${encodeURIComponent(
- srNumber
- )}`;
- 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(
- payloadMessage ||
- response?.error ||
- `COMCIP request failed (${response?.status || 0}).`
- );
- }
-
- return response;
- }
-
- async function openTimeManagementModal() {
- const selectedDate = getStoredTimeManagementSelectedDate();
-
- appState.timeManagementModal = createTimeManagementLoadingState(selectedDate);
- renderModal();
- await loadTimeManagementData(selectedDate);
- }
-
- async function loadTimeManagementData(selectedDate) {
- const normalizedDate = cleanString(selectedDate) || getDateInputValue(new Date());
- const requestId = `${Date.now()}-${++requestCounter}`;
-
- setStoredTimeManagementSelectedDate(normalizedDate);
- appState.timeManagementDatePicker = {
- ...appState.timeManagementDatePicker,
- isOpen: false,
- };
- appState.timeManagementModal = {
- ...(appState.timeManagementModal || {}),
- requestId,
- selectedDate: normalizedDate,
- status: "loading",
- errorMessage: "",
- };
- renderModal();
-
- try {
- const resourcePartyId = getCurrentUserResourcePartyId();
- const taskTypePromise = fetchTaskTypeRequest();
-
- 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),
- taskTypePromise,
- fetchTimeEntryWeek(timeEntryWeekUrl),
- ]);
- const timeEntryRows = normalizeTimeManagementEntryRows(
- timeEntryWeekPayload,
- weekDays
- );
- const serviceRequests = mergeTimeManagementServiceRequests(
- normalizeTimeManagementServiceRequests(srPayload),
- timeEntryRows
- );
- const activitySrNumbers = getActivityRequestSrNumbers(srPayload, serviceRequests);
- const activityRequest = await fetchCachedActivityRequest(activitySrNumbers);
- const activityRequestUrl = activityRequest.url;
- const activityPayload = activityRequest.payload;
-
- const activityOptionsBySr = normalizeActivityRequest(
- activityPayload,
- serviceRequests
- );
- const taskTypes = normalizeTaskTypeRequest(taskTypePayload);
- const rows = timeEntryRows;
- 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 || "",
- highlightRowId: "",
- deletedTimeEntryIds: [],
- isSaving: false,
- saveErrorMessage: "",
- };
- 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: [],
- highlightRowId: "",
- deletedTimeEntryIds: [],
- isSaving: false,
- saveErrorMessage: "",
- };
- }
-
- function getTimeManagementRows(modal) {
- return modal.timeEntryRows || [];
- }
-
- function isTimeManagementSaveEnabled(modal) {
- const pendingChanges = getTimeManagementPendingChanges(modal);
- const changedRows = [
- ...pendingChanges.newRows,
- ...pendingChanges.updatedRows,
- ];
-
- if (
- !pendingChanges.newRows.length &&
- !pendingChanges.updatedRows.length &&
- !pendingChanges.deletedIds.length
- ) {
- return false;
- }
-
- return changedRows.every((row) => isTimeManagementRowReadyToSave(row));
- }
-
- function isTimeManagementRowReadyToSave(row) {
- const selectedSrNumber = cleanString(row?.selectedSrNumber);
-
- if (!selectedSrNumber) {
- return false;
- }
-
- if (!hasPositiveTimeManagementHours(row)) {
- return false;
- }
-
- if (isNonServiceRequestSrNumber(selectedSrNumber)) {
- return Boolean(cleanString(row?.selectedTaskTypeValue));
- }
-
- return Boolean(
- cleanString(row?.selectedActivityValue) &&
- cleanString(row?.selectedTaskTypeValue)
- );
- }
-
- function hasPositiveTimeManagementHours(row) {
- return Object.values(row?.dayHours || {}).some((value) => {
- const normalizedValue = cleanString(value);
-
- if (!normalizedValue) {
- return false;
- }
-
- const numericValue = Number(normalizedValue.replace(",", "."));
-
- return Number.isFinite(numericValue) && numericValue > 0;
- });
- }
-
- function createBlankTimeManagementEntryRow(srNumber = "", source = "blank") {
- return {
- id: `row-${Date.now()}-${Math.random().toString(16).slice(2)}`,
- timeEntryId: "",
- selectedSrNumber: cleanString(srNumber),
- selectedActivityValue: "",
- selectedTaskTypeValue: "",
- dayHours: {},
- source,
- originalSignature: "",
- };
- }
-
- function addTimeManagementEntryRow(type) {
- if (!appState.timeManagementModal) {
- return;
- }
-
- const existingDuplicateRow = findExistingTimeManagementDuplicateRow(
- appState.timeManagementModal,
- type
- );
-
- if (existingDuplicateRow) {
- highlightTimeManagementDuplicateRow(existingDuplicateRow.id);
- return;
- }
-
- const srNumber = type === "non-service" ? "non-service-sr" : "";
- const nextRow = createBlankTimeManagementEntryRow(srNumber, "manual");
- const nextRows = [
- ...getTimeManagementRows(appState.timeManagementModal),
- nextRow,
- ];
-
- appState.timeManagementModal = {
- ...appState.timeManagementModal,
- timeEntryRows: nextRows,
- highlightRowId: "",
- saveErrorMessage: "",
- };
- appState.pendingFocusSelector = getTimeManagementComboboxFocusSelector(
- nextRow.id,
- type === "non-service" ? "taskType" : "sr"
- );
- renderModal();
- }
-
- function removeTimeManagementEntryRow(rowId) {
- if (!appState.timeManagementModal) {
- return;
- }
-
- const normalizedRowId = cleanString(rowId);
- const rows = getTimeManagementRows(appState.timeManagementModal);
- const removedRow = rows.find((row) => cleanString(row?.id) === normalizedRowId);
- const removedTimeEntryId = cleanString(removedRow?.timeEntryId);
- const remainingRows = rows.filter(
- (row) => cleanString(row?.id) !== normalizedRowId
- );
- const nextRows = remainingRows;
- const deletedTimeEntryIds = removedTimeEntryId
- ? Array.from(
- new Set([
- ...(appState.timeManagementModal.deletedTimeEntryIds || []),
- removedTimeEntryId,
- ])
- )
- : appState.timeManagementModal.deletedTimeEntryIds || [];
-
- appState.timeManagementModal = {
- ...appState.timeManagementModal,
- timeEntryRows: nextRows,
- selectedSrNumber: nextRows[0]?.selectedSrNumber || "",
- selectedActivityValue: nextRows[0]?.selectedActivityValue || "",
- selectedTaskTypeValue: nextRows[0]?.selectedTaskTypeValue || "",
- highlightRowId: "",
- deletedTimeEntryIds,
- saveErrorMessage: "",
- };
- appState.pendingFocusSelector = getTimeManagementPostDeleteFocusSelector(
- nextRows,
- rows,
- normalizedRowId
- );
- renderModal();
- }
-
- function getTimeManagementComboboxFocusSelector(rowId, field) {
- return `.arch-panel-extension-combobox input[data-row-id="${cssEscape(rowId)}"][data-combobox-field="${cssEscape(field)}"]`;
- }
-
- function getTimeManagementPostDeleteFocusSelector(nextRows, previousRows, removedRowId) {
- if (!nextRows.length) {
- return "";
- }
-
- const removedIndex = previousRows.findIndex(
- (row) => cleanString(row?.id) === cleanString(removedRowId)
- );
- const focusIndex = Math.min(Math.max(removedIndex, 0), nextRows.length - 1);
- const focusRow = nextRows[focusIndex];
-
- return focusRow?.id
- ? `.arch-panel-extension-time-management-delete[data-row-id="${cssEscape(focusRow.id)}"]`
- : "";
- }
-
- 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 duplicateRow = findDuplicateTimeManagementSrRow(
- appState.timeManagementModal,
- selectedSrNumber,
- rowId
- );
-
- if (duplicateRow) {
- highlightTimeManagementDuplicateRow(duplicateRow.id);
- return;
- }
-
- 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 || "",
- highlightRowId: "",
- saveErrorMessage: "",
- };
- 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 duplicateRow = findDuplicateTimeManagementNonServiceTaskTypeRow(
- appState.timeManagementModal,
- value,
- rowId
- );
-
- if (duplicateRow) {
- highlightTimeManagementDuplicateRow(duplicateRow.id);
- return;
- }
- }
-
- const nextRows = updateTimeManagementRows(appState.timeManagementModal, rowId, {
- ...(targetRow || createBlankTimeManagementEntryRow()),
- [fieldName]: value,
- });
-
- appState.timeManagementModal = {
- ...appState.timeManagementModal,
- timeEntryRows: nextRows,
- [fieldName]: value,
- highlightRowId: "",
- saveErrorMessage: "",
- };
- }
-
- function findDuplicateTimeManagementSrRow(modal, srNumber, currentRowId = "") {
- const normalizedSrNumber = normalizeString(srNumber);
- const normalizedCurrentRowId = cleanString(currentRowId);
-
- if (!normalizedSrNumber || isNonServiceRequestSrNumber(srNumber)) {
- return null;
- }
-
- return getTimeManagementRows(modal).find((row) => {
- return (
- cleanString(row?.id) !== normalizedCurrentRowId &&
- normalizeString(row?.selectedSrNumber) === normalizedSrNumber
- );
- }) || null;
- }
-
- function findExistingTimeManagementDuplicateRow(modal, type) {
- const seen = new Map();
-
- for (const row of getTimeManagementRows(modal)) {
- const rowId = cleanString(row?.id);
- let key = "";
-
- if (type === "non-service") {
- if (!isNonServiceRequestSrNumber(row?.selectedSrNumber)) {
- continue;
- }
-
- key = normalizeString(row?.selectedTaskTypeValue);
- } else {
- if (isNonServiceRequestSrNumber(row?.selectedSrNumber)) {
- continue;
- }
-
- key = normalizeString(row?.selectedSrNumber);
- }
-
- if (!key) {
- continue;
- }
-
- if (seen.has(key)) {
- return getTimeManagementRows(modal).find(
- (candidate) => cleanString(candidate?.id) === seen.get(key)
- ) || row;
- }
-
- seen.set(key, rowId);
- }
-
- return null;
- }
-
- function findDuplicateTimeManagementNonServiceTaskTypeRow(
- modal,
- taskTypeValue,
- currentRowId = ""
- ) {
- const normalizedTaskTypeValue = normalizeString(taskTypeValue);
- const normalizedCurrentRowId = cleanString(currentRowId);
-
- if (!normalizedTaskTypeValue) {
- return null;
- }
-
- const currentRow = getTimeManagementRowById(modal, currentRowId);
-
- if (!isNonServiceRequestSrNumber(currentRow?.selectedSrNumber)) {
- return null;
- }
-
- return getTimeManagementRows(modal).find((row) => {
- return (
- cleanString(row?.id) !== normalizedCurrentRowId &&
- isNonServiceRequestSrNumber(row?.selectedSrNumber) &&
- normalizeString(row?.selectedTaskTypeValue) === normalizedTaskTypeValue
- );
- }) || null;
- }
-
- function highlightTimeManagementDuplicateRow(rowId) {
- const normalizedRowId = cleanString(rowId);
-
- if (!appState.timeManagementModal || !normalizedRowId) {
- return;
- }
-
- appState.pendingTimeManagementHighlightRowId = normalizedRowId;
- appState.timeManagementModal = {
- ...appState.timeManagementModal,
- highlightRowId: normalizedRowId,
- };
- renderModal();
- }
-
- 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
- ),
- highlightRowId: "",
- saveErrorMessage: "",
- };
- renderModal();
- }
-
- async function saveTimeManagementChanges() {
- const modal = appState.timeManagementModal;
-
- if (!modal || modal.isSaving || !isTimeManagementSaveEnabled(modal)) {
- return;
- }
-
- appState.timeManagementModal = {
- ...modal,
- isSaving: true,
- saveErrorMessage: "",
- };
- renderModal();
-
- try {
- const resourceId = getCurrentUserResourcePartyId();
- const weekId = cleanString(modal.weekId);
-
- if (!resourceId) {
- throw new Error("resourceCurrentUser.resourcePartyId não está disponÃvel.");
- }
-
- if (!weekId) {
- throw new Error("WeekId não encontrado para gravação do apontamento.");
- }
-
- const changes = getTimeManagementPendingChanges(modal);
- const baseUrl = COMCIP_REQUEST.timeEntriesUrl;
- const deletedIds = changes.deletedIds;
- const updatedPayload = changes.updatedRows.map((row) =>
- createTimeManagementSavePayload(row, modal.weekDays, weekId, resourceId, {
- includeId: true,
- })
- );
- const newPayload = changes.newRows.map((row) =>
- createTimeManagementSavePayload(row, modal.weekDays, weekId, resourceId)
- );
-
- for (const timeEntryId of deletedIds) {
- await mutateComcipPayload(
- "DELETE",
- `${baseUrl}/${encodeURIComponent(timeEntryId)}`,
- null,
- "timeEntryDelete"
- );
- }
-
- if (updatedPayload.length) {
- await mutateComcipPayload(
- "PUT",
- baseUrl,
- updatedPayload,
- "timeEntryUpdate"
- );
- }
-
- if (newPayload.length) {
- await mutateComcipPayload("POST", baseUrl, newPayload, "timeEntryCreate");
- }
-
- await loadTimeManagementData(modal.selectedDate || getDateInputValue(new Date()));
- } catch (error) {
- appState.timeManagementModal = {
- ...(appState.timeManagementModal || modal),
- isSaving: false,
- saveErrorMessage: getErrorMessage(error),
- };
- renderModal();
- }
- }
-
- function getTimeManagementPendingChanges(modal) {
- const rows = getTimeManagementRows(modal);
-
- return {
- newRows: rows.filter((row) => isNewTimeManagementRow(row)),
- updatedRows: rows.filter((row) => isUpdatedTimeManagementRow(row)),
- deletedIds: (modal?.deletedTimeEntryIds || [])
- .map((id) => cleanString(id))
- .filter(Boolean),
- };
- }
-
- function isNewTimeManagementRow(row) {
- return cleanString(row?.source) === "manual" && !cleanString(row?.timeEntryId);
- }
-
- function isUpdatedTimeManagementRow(row) {
- const timeEntryId = cleanString(row?.timeEntryId);
- const originalSignature = cleanString(row?.originalSignature);
-
- return Boolean(
- timeEntryId &&
- originalSignature &&
- originalSignature !== createTimeManagementRowSignature(row)
- );
- }
-
- function createTimeManagementRowSignature(row) {
- return JSON.stringify({
- srNumber: cleanString(row?.selectedSrNumber),
- activityCode: cleanString(row?.selectedActivityValue),
- taskType: cleanString(row?.selectedTaskTypeValue),
- dayHours: normalizeTimeManagementDayHours(row?.dayHours || {}),
- });
- }
-
- function normalizeTimeManagementDayHours(dayHours) {
- return Object.keys(dayHours || {})
- .sort()
- .reduce((normalized, key) => {
- normalized[key] = formatTimeManagementHourPayloadValue(dayHours[key]);
- return normalized;
- }, {});
- }
-
- function createTimeManagementSavePayload(
- row,
- weekDays,
- weekId,
- resourceId,
- options = {}
- ) {
- const selectedSrNumber = cleanString(row?.selectedSrNumber);
- const payload = {
- srNumber: selectedSrNumber,
- activityCode: isNonServiceRequestSrNumber(selectedSrNumber)
- ? ""
- : cleanString(row?.selectedActivityValue),
- taskType: cleanString(row?.selectedTaskTypeValue),
- deliveryType: "",
- saturdayTaskHours: 0,
- sundayTaskHours: 0,
- mondayTaskHours: 0,
- tuesdayTaskHours: 0,
- wednesdayTaskHours: 0,
- thursdayTaskHours: 0,
- fridayTaskHours: 0,
- notes: "",
- weekId: toNumber(weekId) || weekId,
- resourceId: cleanString(resourceId),
- tag: isNonServiceRequestSrNumber(selectedSrNumber)
- ? "NON_SERVICE"
- : "SERVICE",
- };
-
- if (options.includeId) {
- const id = cleanString(row?.timeEntryId);
- payload.id = toNumber(id) || id;
- }
-
- for (const day of weekDays || []) {
- const weekdayName = getTimeManagementWeekdayName(day);
- const dateKey = getTimeManagementDayKey(day);
- const payloadKey = `${weekdayName}TaskHours`;
-
- if (payloadKey in payload) {
- payload[payloadKey] = formatTimeManagementHourPayloadValue(
- row?.dayHours?.[dateKey]
- );
- }
- }
-
- return payload;
- }
-
- function formatTimeManagementHourPayloadValue(value) {
- const normalizedValue = cleanString(value);
-
- if (!normalizedValue) {
- return 0;
- }
-
- const numericValue = Number(normalizedValue.replace(",", "."));
-
- return Number.isFinite(numericValue) ? numericValue : 0;
- }
-
- 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");
- }
-
- async function fetchCachedActivityRequest(srNumbers) {
- const cacheKey = getActivityRequestCacheKey(srNumbers);
-
- if (!cacheKey) {
- return {
- url: "",
- payload: null,
- };
- }
-
- const cachedRequest = appState.timeManagementActivityCache.get(cacheKey);
-
- if (cachedRequest?.payload) {
- return cachedRequest;
- }
-
- if (cachedRequest?.promise) {
- return cachedRequest.promise;
- }
-
- const requestPromise = fetchActivityRequestWithFallback(srNumbers).then((result) => {
- appState.timeManagementActivityCache.set(cacheKey, {
- url: result.url,
- payload: result.payload,
- });
-
- return result;
- }).catch((error) => {
- appState.timeManagementActivityCache.delete(cacheKey);
- throw error;
- });
-
- appState.timeManagementActivityCache.set(cacheKey, {
- promise: requestPromise,
- });
-
- return requestPromise;
- }
-
- async function fetchActivityRequestWithFallback(srNumbers) {
- let activityRequestUrl = buildActivityRequestUrl(srNumbers);
- let activityPayload = await fetchComcipPayload(
- activityRequestUrl,
- "activityRequest"
- );
-
- if (activityRequestUrl && !hasActivityRequestItems(activityPayload)) {
- activityRequestUrl = buildActivityRequestUrl(
- ACTIVITY_REQUEST_REFERENCE_SR_NUMBERS
- );
- activityPayload = await fetchComcipPayload(
- activityRequestUrl,
- "activityRequest"
- );
- }
-
- return {
- url: activityRequestUrl,
- payload: activityPayload,
- };
- }
-
- function getActivityRequestCacheKey(srNumbers) {
- const normalizedSrNumbers = (srNumbers || [])
- .map((srNumber) => cleanString(srNumber).toUpperCase())
- .filter((srNumber) => /^SR\d+$/i.test(srNumber))
- .sort();
-
- return normalizedSrNumbers.join("|");
- }
-
- 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;
- }
-
- async function mutateComcipPayload(method, url, payload, requestName) {
- const response = await sendComcipRequestMessage({
- method,
- url,
- payload,
- timeoutMessage: `Timed out while executing ${requestName}.`,
- });
-
- 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 fallbackTitle = cleanString(
- getFieldValue(item, ["problemDescription", "ProblemDescription"])
- );
-
- return {
- srNumber,
- srTitle,
- label: formatTimeManagementServiceRequestLabel({
- srNumber,
- srTitle: srTitle || fallbackTitle,
- }),
- };
- })
- .filter((item) => item.srNumber);
- }
-
- function formatTimeManagementServiceRequestLabel(serviceRequest) {
- const srNumber = cleanString(serviceRequest?.srNumber);
- const srTitle = cleanString(serviceRequest?.srTitle || serviceRequest?.label);
-
- if (srTitle && srNumber) {
- return `${srTitle} (${srNumber})`;
- }
-
- return srTitle || 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: formatTimeManagementServiceRequestLabel({
- srNumber,
- srTitle: 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 timeEntryId = getTimeEntryId(item);
- const srNumber = getTimeEntrySrNumber(item) || "non-service-sr";
- const activityValue = getTimeEntryActivity(item);
- const taskTypeValue = getTimeEntryTaskType(item);
-
- return {
- id: `entry-${index + 1}`,
- timeEntryId,
- selectedSrNumber: srNumber,
- selectedActivityValue: activityValue,
- selectedTaskTypeValue: taskTypeValue,
- dayHours: getTimeEntryWeekDayHours(item, weekDays),
- source: "timeEntryWeek",
- originalSignature: "",
- };
- }
- );
- }
-
- 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}`,
- timeEntryId: cleanString(row?.timeEntryId),
- selectedSrNumber: srNumber,
- selectedActivityValue:
- findOptionValue(activityOptions, row?.selectedActivityValue) ||
- cleanString(row?.selectedActivityValue) ||
- "",
- selectedTaskTypeValue:
- findOptionValue(taskTypeOptions, row?.selectedTaskTypeValue) ||
- cleanString(row?.selectedTaskTypeValue) ||
- "",
- dayHours: row?.dayHours || {},
- originalSignature:
- cleanString(row?.originalSignature) ||
- (cleanString(row?.source) === "timeEntryWeek"
- ? createTimeManagementRowSignature({
- ...row,
- 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(
@@ -7479,331 +968,6 @@
appState.forecastUpdateConfirmModal = null;
}
- function shiftCalendarPeriod(delta) {
- const nextDate =
- appState.calendarView === "week"
- ? getShiftedCalendarWeekDate(delta)
- : new Date(appState.calendarYear, appState.calendarMonth + delta, 1);
-
- appState.calendarYear = nextDate.getFullYear();
- appState.calendarMonth = nextDate.getMonth();
- appState.calendarAnchorDateKey = getLocalDateKey(nextDate);
- renderModal();
- }
-
- function goToCurrentCalendarPeriod() {
- const today = new Date();
- appState.calendarYear = today.getFullYear();
- appState.calendarMonth = today.getMonth();
- appState.calendarAnchorDateKey = getLocalDateKey(today);
- renderModal();
- }
-
- function setCalendarView(view) {
- const normalizedView = view === "week" ? "week" : "month";
-
- if (appState.calendarView === normalizedView) {
- return;
- }
-
- appState.calendarView = normalizedView;
- setStoredCalendarView(normalizedView);
-
- if (normalizedView === "week") {
- const today = new Date();
- const anchorDate =
- today.getFullYear() === appState.calendarYear &&
- today.getMonth() === appState.calendarMonth
- ? today
- : new Date(appState.calendarYear, appState.calendarMonth, 1);
-
- appState.calendarAnchorDateKey = getLocalDateKey(anchorDate);
- appState.calendarYear = anchorDate.getFullYear();
- appState.calendarMonth = anchorDate.getMonth();
- } else {
- const anchorDate = getCalendarAnchorDate(
- appState.calendarYear,
- appState.calendarMonth,
- appState.calendarAnchorDateKey
- );
-
- appState.calendarYear = anchorDate.getFullYear();
- appState.calendarMonth = anchorDate.getMonth();
- }
-
- renderModal();
- }
-
- function getShiftedCalendarWeekDate(delta) {
- const anchorDate = getCalendarAnchorDate(
- appState.calendarYear,
- appState.calendarMonth,
- appState.calendarAnchorDateKey
- );
-
- anchorDate.setDate(anchorDate.getDate() + delta * 7);
- return anchorDate;
- }
-
- function shiftCalendarMonth(delta) {
- shiftCalendarPeriod(delta);
- }
-
- function goToCurrentCalendarMonth() {
- goToCurrentCalendarPeriod();
- }
-
- async function submitActionForm(form) {
- if (!appState.dataset || !appState.actionFormModal) {
- return;
- }
-
- if (!form.reportValidity()) {
- return;
- }
-
- const formData = new FormData(form);
- const parentId = cleanString(formData.get("parentId"));
- const ownerEmail = cleanString(formData.get("ownerEmail"));
- const startDate = toIsoDateString(formData.get("startDate"));
- const endDate = toIsoDateString(formData.get("endDate"));
- const payload = {
- parentType: "WORKLOAD",
- parentId,
- action: {
- ownerEmail,
- role: "CLOUD_ARCHITECT",
- name: "SUCCESS_PLAN_CONSUM_RVW",
- team: cleanString(formData.get("team")),
- tags: "",
- startDate,
- endDate,
- status: "COMPLETE",
- complexity: cleanString(formData.get("complexity")),
- notes: [],
- objective: "",
- createdBy: cleanString(formData.get("createdBy")),
- updatedBy: cleanString(formData.get("updatedBy")),
- },
- };
-
- try {
- appState.actionFormModal = {
- ...appState.actionFormModal,
- isSubmitting: true,
- errorMessage: "",
- };
- renderModal();
-
- await fetchJson(API_ENDPOINTS.workloadActions, {
- method: "POST",
- headers: {
- "content-type": "application/json; charset=UTF-8",
- },
- body: JSON.stringify(payload),
- });
-
- await applyCreatedActionToDataset(payload);
- appState.actionFormModal = null;
- renderModal();
- } catch (error) {
- appState.actionFormModal = {
- ...appState.actionFormModal,
- isSubmitting: false,
- errorMessage: getErrorMessage(error),
- };
- renderModal();
- }
- }
-
- async function applyCreatedActionToDataset(payload) {
- if (!appState.dataset) {
- return;
- }
-
- const updatedCustomers = (appState.dataset.customers || []).map((customer) => ({
- ...customer,
- workloads: (customer.workloads || []).map((workload) => {
- if (workload.workloadId !== payload.parentId) {
- return workload;
- }
-
- return {
- ...workload,
- hasAction: true,
- actions: [
- ...(workload.actions || []),
- {
- owner: payload.action.ownerEmail,
- role: payload.action.role,
- name: payload.action.name,
- team: payload.action.team,
- startDate: payload.action.startDate,
- endDate: payload.action.endDate,
- complexity: payload.action.complexity,
- },
- ],
- };
- }),
- }));
- const exportPayload = createExportPayload(
- appState.dataset.user,
- updatedCustomers,
- appState.dataset.exportPayload?.pendingServiceRequests || [],
- appState.dataset.exportPayload?.resourceCurrentUser || null
- );
- const nextDataset = createDatasetSnapshot(exportPayload);
- const previousDetailModal = appState.detailModal
- ? {
- scope: appState.detailModal.scope,
- value: appState.detailModal.value,
- sortKey: appState.detailModal.sortKey,
- sortDirection: appState.detailModal.sortDirection,
- }
- : null;
-
- appState.dataset = nextDataset;
- await writeCachedDataset(exportPayload);
-
- if (previousDetailModal?.scope) {
- restoreDetailModal(previousDetailModal);
- }
- }
-
- function restoreDetailModal(detailState) {
- const eligibleWorkloads = appState.dataset?.summary?.eligibleWorkloads || [];
- let items = [];
- let label = "";
- let title = "";
-
- if (detailState.scope === "stage") {
- items = eligibleWorkloads.filter(
- (workload) => workload.opportunityForecastTypeGroup === detailState.value
- );
- label = `Opportunity type: ${detailState.value}`;
- title = `${detailState.value} workloads`;
- } else if (detailState.scope === "sr") {
- const flag = detailState.value === "true";
- items = eligibleWorkloads.filter((workload) => workload.hasSR === flag);
- label = "Status de SRs";
- title = flag ? "Workloads com SR" : "Workloads sem SR";
- } else if (detailState.scope === "action") {
- const flag = detailState.value === "true";
- items = eligibleWorkloads.filter((workload) => workload.hasAction === flag);
- label = "Status de Consumption Plan/action";
- title = flag ? "Workloads com action" : "Workloads sem action";
- } else if (detailState.scope === "calendar-day") {
- const allWorkloads = appState.dataset?.summary?.allWorkloads || [];
- items = allWorkloads.filter((workload) => {
- return getLocalDateKey(workload.consumptionStartDate) === detailState.value;
- });
- label = "Workload start calendar";
- title = `Workloads starting ${formatDate(detailState.value) || detailState.value}`;
- }
-
- appState.detailModal = {
- label,
- title,
- items,
- totalAcr: items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
- sortKey: detailState.sortKey || DETAIL_SORT_KEYS.adjustedACR,
- sortDirection: detailState.sortDirection || "desc",
- scope: detailState.scope,
- value: detailState.value,
- type: "workloads",
- };
- }
-
- function toggleDetailSort(sortKey) {
- if (!appState.detailModal) {
- return;
- }
-
- const nextDirection =
- appState.detailModal.sortKey === sortKey &&
- appState.detailModal.sortDirection === "asc"
- ? "desc"
- : "asc";
-
- appState.detailModal = {
- ...appState.detailModal,
- sortKey,
- sortDirection: nextDirection,
- };
-
- renderModal();
- }
-
- function getSortedDetailItems() {
- if (!appState.detailModal) {
- return [];
- }
-
- return sortWorkloads(
- appState.detailModal.items,
- appState.detailModal.sortKey,
- appState.detailModal.sortDirection
- );
- }
-
- function sortWorkloads(items, sortKey, sortDirection = "asc") {
- const factor = sortDirection === "desc" ? -1 : 1;
-
- return [...items].sort((left, right) => {
- const comparison = compareWorkloadValues(
- getSortableWorkloadValue(left, sortKey),
- getSortableWorkloadValue(right, sortKey)
- );
-
- if (comparison !== 0) {
- return comparison * factor;
- }
-
- return compareWorkloadValues(
- getSortableWorkloadValue(left, DETAIL_SORT_KEYS.workload),
- getSortableWorkloadValue(right, DETAIL_SORT_KEYS.workload)
- );
- });
- }
-
- function getSortableWorkloadValue(workload, sortKey) {
- switch (sortKey) {
- case DETAIL_SORT_KEYS.opportunityNumber:
- return cleanString(workload.opportunityNumber);
- case DETAIL_SORT_KEYS.customerName:
- return cleanString(workload.customerName);
- case DETAIL_SORT_KEYS.workload:
- return `${cleanString(workload.name)}|${cleanString(workload.description)}`;
- case DETAIL_SORT_KEYS.rampMonths:
- return toNumber(workload.rampMonths);
- case DETAIL_SORT_KEYS.adjustedACR:
- return toNumber(workload.adjustedACR);
- case DETAIL_SORT_KEYS.opportunityForecastTypeGroup:
- return cleanString(workload.opportunityForecastTypeGroup);
- case DETAIL_SORT_KEYS.hasSR:
- return Boolean(workload.hasSR);
- case DETAIL_SORT_KEYS.hasAction:
- return Boolean(workload.hasAction);
- default:
- return cleanString(workload.name);
- }
- }
-
- function compareWorkloadValues(left, right) {
- if (typeof left === "number" || typeof right === "number") {
- return toNumber(left) - toNumber(right);
- }
-
- if (typeof left === "boolean" || typeof right === "boolean") {
- return Number(Boolean(left)) - Number(Boolean(right));
- }
-
- return String(left || "").localeCompare(String(right || ""), undefined, {
- numeric: true,
- sensitivity: "base",
- });
- }
-
function downloadFile(filename, content, contentType) {
const blob = new Blob([content], { type: contentType });
const url = URL.createObjectURL(blob);
@@ -7816,635 +980,6 @@
}, 1000);
}
- function createExportHtml(dataset) {
- const json = JSON.stringify(dataset.exportPayload);
-
- return `
-
-
-
-
- Arch Panel Dashboard
-
-
-
-
-
-
-
-`;
- }
-
function toggleTheme() {
appState.theme = appState.theme === "dark" ? "light" : "dark";
setStoredTheme(appState.theme);
@@ -8457,3662 +992,6 @@
renderModal();
}
- function applyDialogTheme(overlay) {
- const dialog = overlay?.querySelector(`#${IDS.dialog}`);
-
- if (!dialog) {
- return;
- }
-
- dialog.setAttribute("data-theme", appState.theme);
- }
-
- function ensureStyles() {
- if (document.getElementById(IDS.style)) {
- return;
- }
-
- const style = document.createElement("style");
- style.id = IDS.style;
- style.textContent = `
- html.${CLASSES.modalOpen},
- body.${CLASSES.modalOpen} {
- overflow: hidden !important;
- }
-
- .arch-panel-extension-entry {
- --arch-panel-bg: rgba(255, 255, 255, 0.12);
- --arch-panel-bg-hover: rgba(255, 255, 255, 0.2);
- --arch-panel-border: rgba(255, 255, 255, 0.26);
- --arch-panel-fg: #ffffff;
- --arch-panel-focus: rgba(255, 255, 255, 0.72);
- --arch-panel-font: inherit;
- --arch-workbench-header: #4b335d;
- --arch-workbench-accent: #6c4d80;
- display: inline-flex;
- align-items: center;
- margin-inline-end: 12px;
- flex: 0 0 auto;
- }
-
- .arch-panel-extension-button {
- appearance: none;
- border: 1px solid var(--arch-panel-border);
- background: var(--arch-panel-bg);
- color: var(--arch-panel-fg);
- border-radius: 999px;
- min-height: 36px;
- padding: 0 14px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- font-family: var(--arch-panel-font);
- font-size: 13px;
- font-weight: 600;
- line-height: 1;
- letter-spacing: 0.01em;
- cursor: pointer;
- transition:
- background-color 160ms ease,
- border-color 160ms ease,
- transform 160ms ease;
- }
-
- .arch-panel-extension-button:hover {
- background: var(--arch-panel-bg-hover);
- }
-
- .arch-panel-extension-button:active {
- transform: translateY(1px);
- }
-
- .arch-panel-extension-button:focus-visible,
- .arch-panel-extension-close:focus-visible,
- .arch-panel-extension-theme-toggle:focus-visible,
- .arch-panel-extension-sidebar-button:focus-visible,
- .arch-panel-extension-utility-button:focus-visible,
- .arch-panel-extension-summary-card-button:focus-visible,
- .arch-panel-extension-sort-button:focus-visible,
- .arch-panel-extension-indicator-card:focus-visible,
- .arch-panel-extension-row-link:focus-visible {
- outline: 2px solid var(--arch-panel-focus);
- outline-offset: 2px;
- }
-
- #${IDS.hcmTile} {
- cursor: pointer;
- }
-
- #${IDS.hcmTile}:focus-visible {
- outline: 2px solid #0b5cab;
- outline-offset: 3px;
- }
-
- .arch-panel-extension-overlay {
- position: fixed;
- inset: 0;
- z-index: 2147483647;
- display: flex;
- opacity: 0;
- pointer-events: none;
- background: rgba(13, 17, 24, 0.56);
- backdrop-filter: blur(8px);
- transition: opacity 160ms ease;
- }
-
- .arch-panel-extension-overlay.${CLASSES.overlayOpen} {
- opacity: 1;
- pointer-events: auto;
- }
-
- .arch-panel-extension-dialog {
- width: 100vw;
- height: 100vh;
- display: grid;
- grid-template-rows: auto 1fr;
- --wb-app-bg: #f5f4f3;
- --wb-panel: #ffffff;
- --wb-panel-soft: #fafafa;
- --wb-sidebar: #f3f2f1;
- --wb-border: #d9d9d9;
- --wb-border-strong: #c8c8c8;
- --wb-row-divider: #e6e6e6;
- --wb-divider: #ececec;
- --wb-row-hover: #f9fbfc;
- --wb-text: #1f1f1f;
- --wb-text-secondary: #5f6670;
- --wb-text-muted: #7b8189;
- --wb-link: #006b9a;
- --wb-info: #dceef7;
- --wb-success: #3fa35c;
- --wb-warning: #d94f2b;
- --wb-shadow: 0 1px 2px rgba(31, 31, 31, 0.04);
- --arch-workbench-header-fallback: #3f2d4f;
- --arch-workbench-accent-fallback: #684b78;
- --arch-shell-header-text: #ffffff;
- --arch-shell-header-muted: rgba(255, 255, 255, 0.82);
- --arch-shell-header-subtle: rgba(255, 255, 255, 0.68);
- background: var(--wb-app-bg);
- color: var(--wb-text);
- font-family: "Oracle Sans", "Segoe UI", Arial, sans-serif;
- }
-
- .arch-panel-extension-dialog[data-theme="dark"] {
- --wb-app-bg: #141b24;
- --wb-panel: #1f2833;
- --wb-panel-soft: #273240;
- --wb-sidebar: #19222c;
- --wb-border: #364150;
- --wb-border-strong: #566171;
- --wb-row-divider: #333d4b;
- --wb-divider: #303a46;
- --wb-row-hover: #273240;
- --wb-text: #f4f7fb;
- --wb-text-secondary: #c6d0da;
- --wb-text-muted: #9ca8b5;
- --wb-link: #84c7e3;
- --wb-info: #233847;
- --wb-success: #5ec280;
- --wb-warning: #f0946c;
- --wb-shadow: none;
- --arch-workbench-header-fallback: #202a36;
- --arch-workbench-accent-fallback: #39495b;
- }
-
- .arch-panel-extension-dialog * {
- box-sizing: border-box;
- }
-
- .arch-panel-extension-shell-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 24px;
- min-height: 64px;
- padding: 12px 24px;
- background:
- linear-gradient(
- 135deg,
- var(--arch-workbench-header, var(--arch-workbench-header-fallback)) 0%,
- var(--arch-workbench-accent, var(--arch-workbench-accent-fallback)) 100%
- );
- border-bottom: 1px solid rgba(255, 255, 255, 0.16);
- box-shadow: 0 8px 24px rgba(31, 31, 31, 0.16);
- color: var(--arch-shell-header-text);
- }
-
- .arch-panel-extension-brand {
- display: flex;
- align-items: center;
- gap: 14px;
- min-width: 0;
- }
-
- .arch-panel-extension-brand-mark {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 40px;
- height: 40px;
- border-radius: 6px;
- background: rgba(255, 255, 255, 0.18);
- color: var(--arch-shell-header-text);
- font-size: 14px;
- font-weight: 700;
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
- }
-
- .arch-panel-extension-brand-label {
- margin: 0 0 2px;
- color: var(--arch-shell-header-muted);
- font-size: 11px;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- }
-
- .arch-panel-extension-title {
- margin: 0;
- color: var(--arch-shell-header-text);
- font-size: 24px;
- line-height: 1.2;
- font-weight: 600;
- letter-spacing: -0.01em;
- }
-
- .arch-panel-extension-shell-actions {
- display: flex;
- align-items: center;
- gap: 12px;
- flex: 0 0 auto;
- }
-
- .arch-panel-extension-shell-note {
- max-width: 36vw;
- overflow: hidden;
- color: var(--arch-shell-header-muted);
- font-size: 12px;
- line-height: 1.35;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- .arch-panel-extension-theme-toggle {
- appearance: none;
- min-height: 34px;
- padding: 0 14px;
- border: 1px solid rgba(255, 255, 255, 0.28);
- border-radius: 4px;
- background: rgba(255, 255, 255, 0.12);
- color: var(--arch-shell-header-text);
- font: inherit;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease;
- }
-
- .arch-panel-extension-shell-header .arch-panel-extension-theme-toggle:hover {
- border-color: rgba(255, 255, 255, 0.44);
- background: rgba(255, 255, 255, 0.2);
- }
-
- .arch-panel-extension-close {
- appearance: none;
- min-height: 34px;
- padding: 0 14px;
- border: 1px solid var(--wb-border);
- border-radius: 4px;
- background: var(--wb-panel-soft);
- color: var(--wb-text);
- font: inherit;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- box-shadow: none;
- }
-
- .arch-panel-extension-close:hover {
- border-color: var(--wb-border-strong);
- background: var(--wb-row-hover);
- }
-
- .arch-panel-extension-shell-header .arch-panel-extension-close {
- border-color: rgba(255, 255, 255, 0.28);
- background: rgba(255, 255, 255, 0.12);
- color: var(--arch-shell-header-text);
- }
-
- .arch-panel-extension-shell-header .arch-panel-extension-close:hover {
- border-color: rgba(255, 255, 255, 0.42);
- background: rgba(255, 255, 255, 0.16);
- }
-
- .arch-panel-extension-detail-dialog .arch-panel-extension-close {
- border-color: var(--wb-border);
- background: var(--wb-panel-soft);
- color: var(--wb-text);
- box-shadow: none;
- }
-
- .arch-panel-extension-sidebar-button,
- .arch-panel-extension-utility-button {
- appearance: none;
- min-height: 34px;
- padding: 0 14px;
- border: 1px solid var(--wb-border);
- border-radius: 4px;
- background: var(--wb-panel);
- color: var(--wb-text);
- font: inherit;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- }
-
- .arch-panel-extension-utility-button.is-primary {
- border-color: #0b5cab;
- background: #0b5cab;
- color: #ffffff;
- text-shadow: 0 1px 0 rgba(0, 0, 0, 0.16);
- box-shadow: 0 8px 18px rgba(11, 92, 171, 0.24);
- }
-
- .arch-panel-extension-utility-button.is-primary:hover {
- border-color: #084a8a;
- background: #084a8a;
- }
-
- .arch-panel-extension-utility-button.is-primary:disabled {
- border-color: var(--wb-border);
- background: var(--wb-panel-soft);
- color: var(--wb-text-muted);
- box-shadow: none;
- cursor: not-allowed;
- }
-
- .arch-panel-extension-utility-button.is-outline {
- border-color: #0b5cab;
- background: transparent;
- color: #0b5cab;
- }
-
- .arch-panel-extension-utility-button.is-outline:hover:not(:disabled) {
- background: rgba(11, 92, 171, 0.08);
- }
-
- .arch-panel-extension-utility-button.is-outline:disabled {
- border-color: var(--wb-border);
- background: transparent;
- color: var(--wb-text-muted);
- cursor: not-allowed;
- opacity: 0.62;
- }
-
- .arch-panel-extension-shell-body {
- display: grid;
- grid-template-columns: 260px minmax(0, 1fr);
- min-height: 0;
- position: relative;
- }
-
- .arch-panel-extension-shell-body.is-sidebar-collapsed {
- grid-template-columns: minmax(0, 1fr);
- }
-
- .arch-panel-extension-sidebar {
- background: var(--wb-sidebar);
- border-right: 1px solid var(--wb-border);
- padding: 20px 14px;
- }
-
- .arch-panel-extension-sidebar.is-collapsed {
- display: none;
- }
-
- .arch-panel-extension-sidebar-group + .arch-panel-extension-sidebar-group {
- margin-top: 18px;
- padding-top: 18px;
- border-top: 1px solid var(--wb-border);
- }
-
- .arch-panel-extension-sidebar-label,
- .arch-panel-extension-page-eyebrow,
- .arch-panel-extension-panel-label,
- .arch-panel-extension-state-eyebrow {
- margin: 0 0 8px;
- color: var(--wb-text-muted);
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- }
-
- .arch-panel-extension-sidebar-data {
- display: grid;
- gap: 6px;
- color: var(--wb-text-secondary);
- font-size: 12px;
- }
-
- .arch-panel-extension-sidebar-data strong {
- color: var(--wb-text);
- font-size: 13px;
- }
-
- .arch-panel-extension-current-user-card {
- display: grid;
- justify-items: start;
- gap: 10px;
- }
-
- .arch-panel-extension-current-user-avatar {
- width: 48px;
- height: 48px;
- border: 1px solid var(--wb-border);
- border-radius: 999px;
- background: var(--wb-panel);
- box-shadow: var(--wb-shadow);
- color: var(--wb-text);
- object-fit: cover;
- }
-
- .arch-panel-extension-current-user-avatar.is-fallback {
- display: inline-grid;
- place-items: center;
- background: var(--arch-workbench-header, var(--arch-workbench-header-fallback));
- color: #ffffff;
- font-size: 15px;
- font-weight: 700;
- letter-spacing: 0.02em;
- }
-
- .arch-panel-extension-sidebar-button {
- width: 100%;
- justify-content: center;
- display: inline-flex;
- align-items: center;
- border-color: var(--wb-border);
- background: var(--wb-panel);
- color: var(--wb-text);
- }
-
- .arch-panel-extension-sidebar-button + .arch-panel-extension-sidebar-button {
- margin-top: 8px;
- }
-
- .arch-panel-extension-main-shell {
- display: flex;
- flex-direction: column;
- min-width: 0;
- overflow: auto;
- overflow-anchor: none;
- background: var(--wb-app-bg);
- padding: 20px 24px 24px;
- }
-
- .arch-panel-extension-main-shell:has(.arch-panel-extension-state-card) {
- grid-column: 1 / -1;
- align-items: stretch;
- justify-content: center;
- min-height: min(720px, calc(100vh - 120px));
- }
-
- .arch-panel-extension-page-hero {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 24px;
- margin-bottom: 18px;
- }
-
- .arch-panel-extension-page-title,
- .arch-panel-extension-state-title {
- margin: 0;
- color: var(--wb-text);
- font-size: 34px;
- line-height: 1.15;
- font-weight: 700;
- }
-
- .arch-panel-extension-page-subtitle,
- .arch-panel-extension-state-copy {
- margin: 10px 0 0;
- max-width: 760px;
- color: var(--wb-text-secondary);
- font-size: 13px;
- line-height: 1.55;
- }
-
- .arch-panel-extension-hero-aside {
- display: grid;
- gap: 8px;
- justify-items: end;
- }
-
- .arch-panel-extension-hero-pill {
- display: inline-flex;
- align-items: center;
- min-height: 28px;
- padding: 0 10px;
- border: 1px solid var(--wb-border);
- border-radius: 14px;
- background: var(--wb-info);
- color: var(--wb-text);
- font-size: 12px;
- font-weight: 600;
- }
-
- .arch-panel-extension-hero-note {
- color: var(--wb-text-secondary);
- font-size: 12px;
- }
-
- .arch-panel-extension-summary-row,
- .arch-panel-extension-indicator-grid,
- .arch-panel-extension-status-grid,
- .arch-panel-extension-content-grid,
- .arch-panel-extension-boolean-grid {
- display: grid;
- gap: 14px;
- }
-
- .arch-panel-extension-summary-row {
- grid-template-columns: repeat(4, minmax(0, 1fr));
- margin-bottom: 16px;
- }
-
- .arch-panel-extension-indicator-grid {
- grid-template-columns: repeat(4, minmax(0, 1fr));
- margin-bottom: 16px;
- }
-
- .arch-panel-extension-status-grid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- margin-bottom: 16px;
- }
-
- .arch-panel-extension-content-grid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-
- .arch-panel-extension-summary-card,
- .arch-panel-extension-panel,
- .arch-panel-extension-indicator-card,
- .arch-panel-extension-state-card {
- border: 1px solid var(--wb-border);
- border-radius: 8px;
- background: var(--wb-panel);
- box-shadow: var(--wb-shadow);
- }
-
- .arch-panel-extension-summary-card,
- .arch-panel-extension-state-card,
- .arch-panel-extension-indicator-card {
- padding: 16px;
- }
-
- .arch-panel-extension-state-card {
- width: 100%;
- min-height: 320px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- padding: 34px 40px;
- }
-
- .arch-panel-extension-loading-progress {
- display: grid;
- gap: 10px;
- width: 100%;
- margin-top: 30px;
- }
-
- .arch-panel-extension-loading-progress-meta {
- display: flex;
- align-items: center;
- justify-content: flex-start;
- gap: 16px;
- color: var(--wb-text-secondary);
- font-size: 12px;
- font-weight: 700;
- }
-
- .arch-panel-extension-loading-progress-track {
- position: relative;
- height: 30px;
- overflow: hidden;
- border: 1px solid color-mix(in srgb, var(--wb-accent) 50%, var(--wb-border-strong));
- border-radius: 8px;
- background: color-mix(in srgb, var(--wb-panel-soft) 68%, #000000);
- }
-
- .arch-panel-extension-loading-progress-fill {
- position: absolute;
- inset: 0 auto 0 0;
- width: 0;
- min-width: 0;
- border-radius: inherit;
- background: linear-gradient(90deg, #1f7a4d, #2fbf71);
- transition: width 520ms ease;
- }
-
- .arch-panel-extension-loading-progress-value {
- position: absolute;
- inset: 0 auto 0 50%;
- transform: translateX(-50%);
- display: flex;
- align-items: center;
- justify-content: center;
- min-width: 54px;
- margin: 4px 0;
- padding: 0 8px;
- border: 1px solid rgba(255, 255, 255, 0.26);
- border-radius: 999px;
- background: rgba(7, 12, 18, 0.72);
- color: #ffffff;
- font-size: 12px;
- font-weight: 800;
- line-height: 1;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
- text-shadow: none;
- pointer-events: none;
- }
-
- .arch-panel-extension-summary-card-button {
- appearance: none;
- width: 100%;
- text-align: left;
- color: inherit;
- font: inherit;
- line-height: inherit;
- cursor: pointer;
- }
-
- .arch-panel-extension-panel {
- padding: 16px;
- }
-
- .arch-panel-extension-summary-label,
- .arch-panel-extension-summary-detail,
- .arch-panel-extension-indicator-label,
- .arch-panel-extension-indicator-subtitle,
- .arch-panel-extension-indicator-total,
- .arch-panel-extension-insight-detail,
- .arch-panel-extension-stage-detail {
- margin: 0;
- }
-
- .arch-panel-extension-summary-label,
- .arch-panel-extension-indicator-label {
- color: var(--wb-text-secondary);
- font-size: 12px;
- }
-
- .arch-panel-extension-summary-value,
- .arch-panel-extension-indicator-value {
- margin: 8px 0 4px;
- color: var(--wb-text);
- font-size: 26px;
- line-height: 1.2;
- font-weight: 700;
- }
-
- .arch-panel-extension-summary-detail,
- .arch-panel-extension-indicator-subtitle,
- .arch-panel-extension-indicator-total {
- color: var(--wb-text-muted);
- font-size: 11px;
- }
-
- .arch-panel-extension-indicator-card {
- appearance: none;
- text-align: left;
- color: inherit;
- cursor: pointer;
- }
-
- .arch-panel-extension-boolean-grid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- margin-top: 12px;
- }
-
- .arch-panel-extension-progress {
- display: grid;
- gap: 8px;
- margin-bottom: 12px;
- }
-
- .arch-panel-extension-progress-track {
- display: flex;
- width: 100%;
- height: 10px;
- overflow: hidden;
- border-radius: 999px;
- background: var(--wb-panel-soft);
- border: 1px solid var(--wb-border);
- }
-
- .arch-panel-extension-progress-segment {
- height: 100%;
- }
-
- .arch-panel-extension-progress-segment.is-success {
- background: linear-gradient(90deg, #7fd29a, #3fa35c);
- }
-
- .arch-panel-extension-progress-segment.is-danger {
- background: linear-gradient(90deg, #f2a28d, #d94f2b);
- }
-
- .arch-panel-extension-progress-labels {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- color: var(--wb-text-secondary);
- font-size: 11px;
- font-weight: 600;
- }
-
- .arch-panel-extension-panel-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- margin-bottom: 14px;
- }
-
- .arch-panel-extension-panel-title {
- margin: 0;
- color: var(--wb-text);
- font-size: 16px;
- font-weight: 600;
- }
-
- .arch-panel-extension-panel-meta {
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--wb-text-muted);
- font-size: 12px;
- }
-
- .arch-panel-extension-operations-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 14px;
- }
-
- .arch-panel-extension-sr-hour-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 14px;
- align-content: start;
- }
-
- .arch-panel-extension-calendar-card {
- min-width: 0;
- }
-
- .arch-panel-extension-calendar-actions {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- color: var(--wb-text-muted);
- font-size: 12px;
- }
-
- .arch-panel-extension-calendar-view-toggle {
- display: inline-flex;
- align-items: center;
- overflow: hidden;
- border: 1px solid var(--wb-border);
- border-radius: 4px;
- background: var(--wb-panel-soft);
- }
-
- .arch-panel-extension-calendar-view-button {
- appearance: none;
- min-height: 28px;
- padding: 0 9px;
- border: 0;
- border-right: 1px solid var(--wb-border);
- background: transparent;
- color: var(--wb-text-secondary);
- font: inherit;
- font-size: 12px;
- font-weight: 700;
- cursor: pointer;
- }
-
- .arch-panel-extension-calendar-view-button:last-child {
- border-right: 0;
- }
-
- .arch-panel-extension-calendar-view-button:hover {
- background: var(--wb-row-hover);
- color: var(--wb-text);
- }
-
- .arch-panel-extension-calendar-view-button.is-active {
- background: #0b5cab;
- color: #ffffff;
- }
-
- .arch-panel-extension-calendar-nav {
- appearance: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- border: 1px solid var(--wb-border);
- border-radius: 4px;
- background: var(--wb-panel-soft);
- color: var(--wb-text);
- font-size: 18px;
- line-height: 1;
- cursor: pointer;
- }
-
- .arch-panel-extension-calendar-nav:hover {
- border-color: var(--wb-accent);
- color: var(--wb-accent);
- }
-
- .arch-panel-extension-calendar-today {
- appearance: none;
- min-height: 28px;
- padding: 0 10px;
- border: 1px solid var(--wb-border);
- border-radius: 4px;
- background: var(--wb-panel-soft);
- color: var(--wb-text);
- font: inherit;
- font-size: 12px;
- font-weight: 700;
- cursor: pointer;
- }
-
- .arch-panel-extension-calendar-today:hover {
- border-color: var(--wb-accent);
- color: var(--wb-accent);
- }
-
- .arch-panel-extension-calendar-weekdays,
- .arch-panel-extension-calendar-grid {
- display: grid;
- grid-template-columns: repeat(7, minmax(0, 1fr));
- }
-
- .arch-panel-extension-calendar-weekdays {
- gap: 6px;
- margin-bottom: 6px;
- color: var(--wb-text-muted);
- font-size: 11px;
- font-weight: 700;
- text-transform: uppercase;
- }
-
- .arch-panel-extension-calendar-weekdays span {
- padding: 0 6px;
- }
-
- .arch-panel-extension-calendar-grid {
- gap: 6px;
- }
-
- .arch-panel-extension-calendar-grid.is-week {
- min-height: 300px;
- }
-
- .arch-panel-extension-calendar-day {
- appearance: none;
- position: relative;
- display: block;
- width: 100%;
- text-align: left;
- min-height: 74px;
- overflow: hidden;
- border: 1px solid var(--wb-border);
- border-radius: 6px;
- background: var(--wb-panel-soft);
- padding: 7px;
- font: inherit;
- cursor: pointer;
- }
-
- .arch-panel-extension-calendar-day.is-week-day {
- display: flex;
- flex-direction: column;
- min-height: 300px;
- padding: 0;
- overflow: hidden;
- }
-
- .arch-panel-extension-calendar-week-day-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- min-height: 38px;
- padding: 8px 9px;
- border-bottom: 1px solid var(--wb-border);
- background: color-mix(in srgb, var(--wb-panel) 72%, var(--wb-panel-soft));
- }
-
- .arch-panel-extension-calendar-day.is-week-day .arch-panel-extension-calendar-week-number {
- position: static;
- font-size: 9px;
- opacity: 0.76;
- }
-
- .arch-panel-extension-calendar-week-workloads {
- display: flex;
- flex: 1;
- flex-direction: column;
- gap: 7px;
- padding: 8px;
- overflow: auto;
- }
-
- .arch-panel-extension-calendar-week-workload {
- display: block;
- border-left: 3px solid #d94f2b;
- border-radius: 5px;
- background: rgba(217, 79, 43, 0.14);
- padding: 7px 8px;
- color: var(--wb-text);
- text-align: left;
- }
-
- .arch-panel-extension-calendar-week-workload-title,
- .arch-panel-extension-calendar-week-workload-meta {
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .arch-panel-extension-calendar-week-workload-title {
- font-size: 11px;
- font-weight: 700;
- line-height: 1.25;
- }
-
- .arch-panel-extension-calendar-week-workload-meta {
- margin-top: 3px;
- color: var(--wb-text-secondary);
- font-size: 10px;
- line-height: 1.2;
- white-space: nowrap;
- }
-
- .arch-panel-extension-calendar-week-empty {
- margin: auto;
- color: var(--wb-text-muted);
- font-size: 11px;
- font-weight: 600;
- }
-
- .arch-panel-extension-calendar-day:disabled {
- cursor: default;
- }
-
- .arch-panel-extension-calendar-day.has-workload:hover {
- border-color: #f26c4f;
- background: rgba(217, 79, 43, 0.22);
- }
-
- .arch-panel-extension-calendar-day.is-muted {
- opacity: 0.42;
- }
-
- .arch-panel-extension-calendar-day.is-today {
- border-color: var(--wb-accent);
- box-shadow: inset 0 0 0 1px var(--wb-accent);
- }
-
- .arch-panel-extension-calendar-day.has-workload {
- border-color: rgba(217, 79, 43, 0.82);
- background: rgba(217, 79, 43, 0.14);
- }
-
- .arch-panel-extension-calendar-date {
- color: var(--wb-text);
- font-size: 12px;
- font-weight: 800;
- }
-
- .arch-panel-extension-calendar-week-number {
- position: absolute;
- right: 7px;
- bottom: 6px;
- color: var(--wb-text-muted);
- font-size: 9px;
- font-weight: 800;
- letter-spacing: 0.04em;
- opacity: 0.72;
- pointer-events: none;
- }
-
- .arch-panel-extension-calendar-workload {
- display: block;
- margin-top: 8px;
- color: var(--wb-text);
- font-size: 11px;
- line-height: 1.25;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- }
-
- .arch-panel-extension-calendar-count {
- position: absolute;
- top: 6px;
- right: 6px;
- display: flex;
- align-items: center;
- justify-content: center;
- min-width: 20px;
- height: 20px;
- padding: 0 6px;
- border-radius: 999px;
- background: #d94f2b;
- color: #ffffff;
- font-size: 11px;
- font-weight: 800;
- }
-
- .arch-panel-extension-empty-state {
- border: 1px dashed var(--wb-border);
- border-radius: 8px;
- padding: 16px;
- color: var(--wb-text-muted);
- font-size: 13px;
- }
-
- .arch-panel-extension-panel-dot {
- width: 4px;
- height: 4px;
- border-radius: 50%;
- background: var(--wb-border-strong);
- }
-
- .arch-panel-extension-table-wrap {
- overflow: auto;
- border: 1px solid var(--wb-border);
- border-radius: 6px;
- }
-
- .arch-panel-extension-table-wrap.is-detail {
- max-height: calc(100vh - 220px);
- }
-
- .arch-panel-extension-table-wrap.is-sr-list {
- overflow: auto;
- }
-
- .arch-panel-extension-table-wrap.is-ramp-pivot {
- max-height: min(460px, calc(100vh - 260px));
- }
-
- .arch-panel-extension-table {
- width: 100%;
- border-collapse: collapse;
- background: var(--wb-panel);
- }
-
- .arch-panel-extension-table th,
- .arch-panel-extension-table td {
- padding: 12px 14px;
- border-bottom: 1px solid var(--wb-row-divider);
- font-size: 12px;
- text-align: left;
- white-space: nowrap;
- }
-
- .arch-panel-extension-table th {
- background: var(--wb-panel-soft);
- color: var(--wb-text-secondary);
- font-weight: 600;
- }
-
- .arch-panel-extension-table td {
- color: var(--wb-text);
- }
-
- .arch-panel-extension-ramp-pivot-table th:first-child {
- min-width: 150px;
- position: sticky;
- left: 0;
- z-index: 1;
- }
-
- .arch-panel-extension-ramp-pivot-table tbody th {
- background: var(--wb-panel-soft);
- color: var(--wb-text-secondary);
- font-weight: 700;
- }
-
- .arch-panel-extension-ramp-pivot-table .is-delta-different {
- color: #d94f2b;
- font-weight: 800;
- }
-
- .arch-panel-extension-sr-table td {
- white-space: normal;
- vertical-align: top;
- }
-
- .arch-panel-extension-sr-main {
- width: 78%;
- }
-
- .arch-panel-extension-sr-side {
- width: 22%;
- text-align: right;
- }
-
- .arch-panel-extension-sr-line {
- display: flex;
- flex-wrap: wrap;
- gap: 10px 14px;
- margin-top: 6px;
- }
-
- .arch-panel-extension-sr-line.is-head {
- margin-top: 0;
- }
-
- .arch-panel-extension-sr-customer {
- color: var(--wb-text);
- font-size: 13px;
- }
-
- .arch-panel-extension-sr-moment {
- color: var(--wb-text);
- font-size: 18px;
- font-weight: 700;
- }
-
- .arch-panel-extension-sr-exact,
- .arch-panel-extension-sr-hours {
- margin-top: 6px;
- color: var(--wb-text-secondary);
- font-size: 12px;
- }
-
- .arch-panel-extension-table-empty {
- padding: 18px;
- border: 1px dashed var(--wb-border);
- border-radius: 8px;
- color: var(--wb-text-secondary);
- font-size: 13px;
- }
-
- .arch-panel-extension-table-wrap.is-main .arch-panel-extension-table {
- table-layout: fixed;
- }
-
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-col-opportunity {
- width: 14%;
- }
-
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-col-customer {
- width: 16%;
- }
-
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-col-workload {
- width: auto;
- }
-
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-col-acr {
- width: 11%;
- }
-
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-col-type {
- width: 11%;
- }
-
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-col-flag {
- width: 9%;
- }
-
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-table
- th:nth-child(3),
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-table
- td:nth-child(3) {
- white-space: normal;
- overflow-wrap: anywhere;
- word-break: break-word;
- }
-
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-table
- td:nth-child(3)
- .arch-panel-extension-row-link {
- display: block;
- }
-
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-table
- th:nth-child(2),
- .arch-panel-extension-table-wrap.is-main
- .arch-panel-extension-table
- td:nth-child(2) {
- white-space: normal;
- overflow-wrap: anywhere;
- }
-
- .arch-panel-extension-badge {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 54px;
- min-height: 24px;
- padding: 0 10px;
- border-radius: 999px;
- font-size: 11px;
- font-weight: 700;
- letter-spacing: 0.02em;
- }
-
- .arch-panel-extension-badge.is-success {
- background: #dcfce7;
- border: 1px solid #86efac;
- color: #166534;
- }
-
- .arch-panel-extension-badge.is-danger {
- background: #fee2e2;
- border: 1px solid #fca5a5;
- color: #991b1b;
- }
-
- .arch-panel-extension-action-cell {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- }
-
- .arch-panel-extension-add-action-button {
- appearance: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 26px;
- height: 26px;
- padding: 0;
- border: 1px solid var(--wb-border);
- border-radius: 999px;
- background: var(--wb-panel-soft);
- color: var(--wb-text);
- font: inherit;
- font-size: 18px;
- line-height: 1;
- cursor: pointer;
- }
-
- .arch-panel-extension-table tbody tr:hover {
- background: var(--wb-row-hover);
- }
-
- .arch-panel-extension-table th.is-numeric,
- .arch-panel-extension-table td.is-numeric {
- text-align: right;
- }
-
- .arch-panel-extension-row-link {
- appearance: none;
- padding: 0;
- border: 0;
- background: transparent;
- color: var(--wb-link);
- font: inherit;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- text-decoration: none;
- }
-
- .arch-panel-extension-workload-link-cell {
- display: inline-flex;
- align-items: center;
- gap: 7px;
- }
-
- .arch-panel-extension-ramp-alert-button {
- appearance: none;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 18px;
- height: 18px;
- padding: 0;
- border: 1px solid #d94f2b;
- border-radius: 999px;
- background: #fee2e2;
- color: #991b1b;
- font: inherit;
- font-size: 12px;
- font-weight: 900;
- line-height: 1;
- cursor: pointer;
- }
-
- .arch-panel-extension-ramp-alert-button:hover {
- background: #fecaca;
- }
-
- .arch-panel-extension-insight-list,
- .arch-panel-extension-stage-list,
- .arch-panel-extension-side-column {
- display: grid;
- gap: 14px;
- }
-
- .arch-panel-extension-insight {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 16px;
- padding-top: 12px;
- border-top: 1px solid var(--wb-divider);
- }
-
- .arch-panel-extension-insight:first-child {
- padding-top: 0;
- border-top: 0;
- }
-
- .arch-panel-extension-insight-label,
- .arch-panel-extension-stage-label {
- margin: 0 0 4px;
- color: var(--wb-text);
- font-size: 12px;
- font-weight: 600;
- }
-
- .arch-panel-extension-insight-detail,
- .arch-panel-extension-stage-detail {
- color: var(--wb-text-muted);
- font-size: 11px;
- line-height: 1.45;
- }
-
- .arch-panel-extension-insight-value {
- margin: 0;
- color: var(--wb-text);
- font-size: 16px;
- font-weight: 700;
- text-align: right;
- }
-
- .arch-panel-extension-stage-row {
- display: grid;
- gap: 8px;
- }
-
- .arch-panel-extension-stage-copy {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 12px;
- }
-
- .arch-panel-extension-stage-values {
- display: grid;
- justify-items: end;
- gap: 2px;
- color: var(--wb-text-secondary);
- font-size: 12px;
- }
-
- .arch-panel-extension-stage-detail-row {
- color: var(--wb-text-secondary);
- font-size: 12px;
- }
-
- .arch-panel-extension-stage-bar {
- position: relative;
- height: 8px;
- border-radius: 999px;
- overflow: hidden;
- background: color-mix(in srgb, var(--wb-text-secondary) 14%, transparent);
- }
-
- .arch-panel-extension-stage-bar span {
- position: absolute;
- inset: 0 auto 0 0;
- border-radius: inherit;
- background: linear-gradient(90deg, var(--wb-accent), color-mix(in srgb, var(--wb-accent) 52%, #ffffff));
- }
-
- .arch-panel-extension-detail-overlay {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 16px;
- background: rgba(10, 14, 20, 0.68);
- backdrop-filter: blur(6px);
- }
-
- .arch-panel-extension-time-drawer {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- z-index: 2;
- width: min(560px, calc(100vw - 32px));
- display: flex;
- flex-direction: column;
- gap: 14px;
- padding: 18px;
- border-left: 1px solid var(--wb-border);
- background: var(--wb-panel);
- box-shadow: -18px 0 42px rgba(0, 0, 0, 0.28);
- overflow: auto;
- animation: arch-panel-extension-slide-in 180ms ease-out;
- }
-
- .arch-panel-extension-time-drawer-head {
- position: sticky;
- top: -18px;
- z-index: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 14px;
- padding: 0 0 12px;
- border-bottom: 1px solid var(--wb-border);
- background: var(--wb-panel);
- }
-
- .arch-panel-extension-time-drawer-close {
- border-color: var(--wb-border);
- background: var(--wb-panel-soft);
- color: var(--wb-text);
- box-shadow: none;
- }
-
- .arch-panel-extension-time-drawer-close:hover {
- border-color: var(--wb-border-strong);
- background: var(--wb-row-hover);
- }
-
- .arch-panel-extension-time-drawer-actions {
- position: sticky;
- bottom: -18px;
- display: flex;
- justify-content: flex-end;
- margin: auto -18px -18px;
- padding: 12px 18px 18px;
- border-top: 1px solid var(--wb-border);
- background: linear-gradient(180deg, rgba(23, 30, 39, 0), var(--wb-panel) 30%);
- }
-
- .arch-panel-extension-time-drawer-meta {
- display: flex;
- gap: 8px;
- color: var(--wb-text-muted);
- font-size: 12px;
- }
-
- .arch-panel-extension-time-drawer-state,
- .arch-panel-extension-time-drawer-json {
- border: 1px solid var(--wb-border);
- border-radius: 8px;
- background: var(--wb-panel-soft);
- padding: 14px;
- }
-
- .arch-panel-extension-time-drawer-state.is-error {
- border-color: rgba(217, 79, 43, 0.42);
- }
-
- .arch-panel-extension-time-entry-group {
- display: grid;
- gap: 10px;
- }
-
- .arch-panel-extension-time-entry-group + .arch-panel-extension-time-entry-group {
- padding-top: 12px;
- border-top: 1px solid var(--wb-border);
- }
-
- .arch-panel-extension-time-entry-group-head {
- display: flex;
- align-items: flex-end;
- justify-content: space-between;
- gap: 12px;
- }
-
- .arch-panel-extension-time-entry-group-head h4 {
- margin: 3px 0 0;
- color: var(--wb-text);
- font-size: 16px;
- line-height: 1.25;
- }
-
- .arch-panel-extension-time-entry-group-head span {
- color: var(--wb-text-muted);
- font-size: 12px;
- white-space: nowrap;
- }
-
- .arch-panel-extension-time-entry-table-wrap {
- overflow: auto;
- border: 1px solid var(--wb-border);
- border-radius: 8px;
- }
-
- .arch-panel-extension-time-entry-table {
- width: 100%;
- border-collapse: collapse;
- background: var(--wb-panel);
- }
-
- .arch-panel-extension-time-entry-table th,
- .arch-panel-extension-time-entry-table td {
- padding: 10px 12px;
- border-bottom: 1px solid var(--wb-row-divider);
- color: var(--wb-text);
- font-size: 12px;
- text-align: left;
- vertical-align: top;
- }
-
- .arch-panel-extension-time-entry-table th {
- background: var(--wb-panel-soft);
- color: var(--wb-text-secondary);
- font-weight: 700;
- }
-
- .arch-panel-extension-time-entry-table tfoot th {
- border-bottom: 0;
- color: var(--wb-text);
- }
-
- .arch-panel-extension-time-entry-table .is-numeric {
- text-align: right;
- white-space: nowrap;
- }
-
- .arch-panel-extension-time-drawer-json summary {
- color: var(--wb-link);
- font-size: 12px;
- font-weight: 700;
- cursor: pointer;
- }
-
- .arch-panel-extension-time-drawer-json pre {
- max-height: 320px;
- margin: 12px 0 0;
- overflow: auto;
- color: var(--wb-text);
- font-size: 11px;
- line-height: 1.5;
- white-space: pre-wrap;
- }
-
- @keyframes arch-panel-extension-slide-in {
- from {
- transform: translateX(100%);
- }
- to {
- transform: translateX(0);
- }
- }
-
- .arch-panel-extension-detail-dialog {
- width: min(1520px, calc(100vw - 32px));
- max-height: calc(100vh - 88px);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- border: 1px solid var(--wb-border);
- border-radius: 10px;
- background: var(--wb-panel);
- box-shadow: var(--wb-shadow);
- padding: 18px;
- }
-
- .arch-panel-extension-ramp-dialog {
- width: min(1320px, calc(100vw - 48px));
- max-height: calc(100vh - 120px);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- border: 1px solid var(--wb-border);
- border-radius: 10px;
- background: var(--wb-panel);
- box-shadow: var(--wb-shadow);
- padding: 18px;
- }
-
- .arch-panel-extension-action-dialog {
- width: min(980px, calc(100vw - 32px));
- max-height: calc(100vh - 88px);
- overflow: auto;
- 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-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;
- --arch-time-management-scrollbar-gutter: 16px;
- gap: 4px;
- min-height: 0;
- overflow: visible;
- }
-
- .arch-panel-extension-time-management-card.is-error {
- border-color: rgba(217, 79, 43, 0.42);
- }
-
- .arch-panel-extension-time-management-toolbar {
- display: flex;
- align-items: end;
- gap: 14px;
- flex-wrap: wrap;
- }
-
- .arch-panel-extension-time-management-hour-indicators {
- display: grid;
- grid-template-columns: repeat(4, minmax(92px, 1fr));
- gap: 8px;
- flex: 1 1 520px;
- }
-
- .arch-panel-extension-time-management-hour-indicator {
- display: grid;
- gap: 3px;
- min-height: 48px;
- align-content: center;
- border: 1px solid var(--wb-border);
- border-radius: 8px;
- background: var(--wb-panel);
- padding: 7px 10px;
- }
-
- .arch-panel-extension-time-management-hour-indicator span {
- color: var(--wb-text-muted);
- font-size: 10px;
- font-weight: 800;
- letter-spacing: 0.06em;
- text-transform: uppercase;
- }
-
- .arch-panel-extension-time-management-hour-indicator strong {
- color: var(--wb-text);
- font-size: 16px;
- font-weight: 800;
- line-height: 1.1;
- }
-
- .arch-panel-extension-time-management-hour-indicator .arch-panel-extension-skeleton-text {
- width: 72%;
- }
-
- .arch-panel-extension-time-management-hour-indicator .arch-panel-extension-skeleton-title {
- width: 42%;
- height: 16px;
- }
-
- .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: 4px 0;
- scrollbar-gutter: stable;
- }
-
- .arch-panel-extension-time-management-rows:has(.arch-panel-extension-combobox:focus-within) {
- overflow: visible;
- }
-
- .arch-panel-extension-time-management-empty {
- grid-column: 1 / -1;
- text-align: center;
- }
-
- .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: center;
- gap: 6px;
- padding-right: var(--arch-time-management-scrollbar-gutter);
- }
-
- .arch-panel-extension-time-management-column-label {
- color: var(--wb-text-secondary);
- font-size: 12px;
- font-weight: 600;
- line-height: 1.2;
- }
-
- .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-duplicate-highlight {
- border-radius: 10px;
- animation: arch-panel-extension-duplicate-pulse 1100ms ease-in-out 2;
- box-shadow:
- inset 0 0 0 3px rgba(217, 79, 43, 0.9),
- 0 0 0 2px rgba(217, 79, 43, 0.12);
- padding: 4px;
- margin: -4px 0;
- }
-
- .arch-panel-extension-time-management-entry-row.is-compact .arch-panel-extension-form-field {
- gap: 0;
- }
-
- @keyframes arch-panel-extension-duplicate-pulse {
- 0% {
- box-shadow:
- inset 0 0 0 2px rgba(217, 79, 43, 0.35),
- 0 0 0 0 rgba(217, 79, 43, 0.22);
- }
- 45% {
- box-shadow:
- inset 0 0 0 3px rgba(217, 79, 43, 0.95),
- 0 0 0 6px rgba(217, 79, 43, 0.14);
- }
- 100% {
- box-shadow:
- inset 0 0 0 3px rgba(217, 79, 43, 0.72),
- 0 0 0 0 rgba(217, 79, 43, 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;
- align-content: center;
- justify-items: center;
- gap: 1px;
- min-height: 22px;
- padding: 0;
- color: var(--wb-text-secondary);
- line-height: 1.15;
- text-align: center;
- }
-
- .arch-panel-extension-time-management-day-head strong {
- color: var(--wb-text-secondary);
- font-size: 12px;
- font-weight: 600;
- }
-
- .arch-panel-extension-time-management-day-head span {
- color: var(--wb-text-muted);
- font-size: 11px;
- font-weight: 600;
- text-transform: none;
- }
-
- .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);
- overflow: auto;
- border: 1px solid var(--wb-border);
- border-radius: 10px;
- background: var(--wb-panel);
- box-shadow: var(--wb-shadow);
- padding: 18px;
- }
-
- .arch-panel-extension-detail-title {
- margin: 0;
- color: var(--wb-text);
- font-size: 18px;
- font-weight: 700;
- }
-
- .arch-panel-extension-detail-subtitle {
- margin: 0 0 14px;
- color: var(--wb-text-muted);
- font-size: 12px;
- line-height: 1.5;
- overflow-wrap: anywhere;
- }
-
- .arch-panel-extension-detail-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- margin-bottom: 8px;
- }
-
- .arch-panel-extension-detail-meta {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 8px;
- margin-bottom: 14px;
- color: var(--wb-text-muted);
- font-size: 12px;
- }
-
- .arch-panel-extension-detail-actions {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 10px;
- position: sticky;
- bottom: 0;
- margin-top: 16px;
- padding-top: 12px;
- 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-time-management-save-error {
- margin: 0 auto 0 0;
- color: var(--wb-warning);
- font-size: 12px;
- font-weight: 700;
- }
-
- .arch-panel-extension-form-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 14px;
- margin-top: 18px;
- }
-
- .arch-panel-extension-form-field {
- display: grid;
- gap: 6px;
- }
-
- .arch-panel-extension-form-field span {
- color: var(--wb-text-secondary);
- font-size: 12px;
- font-weight: 600;
- }
-
- .arch-panel-extension-form-field input,
- .arch-panel-extension-form-field select {
- width: 100%;
- min-height: 38px;
- padding: 0 12px;
- border: 1px solid var(--wb-border);
- border-radius: 6px;
- background: var(--wb-panel-soft);
- color: var(--wb-text);
- 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;
- font-size: 12px;
- font-weight: 600;
- }
-
- .arch-panel-extension-sort-button {
- appearance: none;
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 0;
- border: 0;
- background: transparent;
- color: inherit;
- font: inherit;
- font-weight: 600;
- cursor: pointer;
- }
-
- .arch-panel-extension-sort-button.is-numeric {
- justify-content: flex-end;
- width: 100%;
- }
-
- .arch-panel-extension-table-wrap.is-detail {
- flex: 1 1 auto;
- min-height: 0;
- max-height: calc(100vh - 270px);
- overflow: auto;
- }
-
- .arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table th {
- position: sticky;
- top: 0;
- z-index: 3;
- }
-
- .arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table thead {
- position: relative;
- z-index: 3;
- }
-
- .arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table {
- border-collapse: separate;
- border-spacing: 0;
- }
-
- .arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table {
- table-layout: fixed;
- }
-
- .arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table th,
- .arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table td {
- white-space: normal;
- vertical-align: top;
- overflow-wrap: anywhere;
- }
-
- .arch-panel-extension-state-actions {
- display: flex;
- gap: 12px;
- margin-top: 18px;
- }
-
- .arch-panel-extension-state-card.is-error {
- border-color: rgba(217, 79, 43, 0.42);
- }
-
- @media (max-width: 1280px) {
- .arch-panel-extension-summary-row,
- .arch-panel-extension-indicator-grid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- }
-
- @media (max-width: 1100px) {
- .arch-panel-extension-shell-body,
- .arch-panel-extension-content-grid,
- .arch-panel-extension-status-grid,
- .arch-panel-extension-operations-grid {
- grid-template-columns: 1fr;
- }
-
- .arch-panel-extension-sidebar {
- display: none;
- }
- }
-
- @media (max-width: 780px) {
- .arch-panel-extension-shell-header,
- .arch-panel-extension-page-hero {
- flex-direction: column;
- align-items: flex-start;
- }
-
- .arch-panel-extension-summary-row,
- .arch-panel-extension-indicator-grid,
- .arch-panel-extension-boolean-grid,
- .arch-panel-extension-sr-hour-grid {
- grid-template-columns: 1fr;
- }
-
- .arch-panel-extension-state-card {
- min-height: 260px;
- padding: 26px 24px;
- }
- }
-
- @media (max-width: 980px) {
- .arch-panel-extension-action-dialog {
- width: min(720px, calc(100vw - 24px));
- }
-
- .arch-panel-extension-form-grid {
- grid-template-columns: 1fr;
- }
- }
- `;
-
- document.head.appendChild(style);
- }
-
- function applyTheme(header, functionsContainer, anchor, mount, overlay) {
- const headerStyle = window.getComputedStyle(header);
- const functionsStyle = window.getComputedStyle(functionsContainer);
- const sourceBackground = resolveBackgroundColor(header, headerStyle);
- const sourceForeground = resolveForegroundColor(
- header,
- functionsContainer,
- anchor,
- headerStyle,
- functionsStyle
- );
- const workbenchHeader = mix(sourceBackground, { r: 75, g: 51, b: 93, a: 1 }, 0.52);
- const workbenchAccent = mix(workbenchHeader, { r: 108, g: 77, b: 128, a: 1 }, 0.38);
- const focusColor = mix(sourceForeground, workbenchHeader, 0.28);
- const themeTargets = [mount, overlay].filter(Boolean);
-
- for (const target of themeTargets) {
- target.style.setProperty(
- "--arch-panel-bg",
- toColor(mix(sourceBackground, sourceForeground, 0.12))
- );
- target.style.setProperty(
- "--arch-panel-bg-hover",
- toColor(mix(sourceBackground, sourceForeground, 0.22))
- );
- target.style.setProperty(
- "--arch-panel-border",
- toColor(mix(sourceBackground, sourceForeground, 0.32))
- );
- target.style.setProperty("--arch-panel-fg", toColor(sourceForeground, 1));
- target.style.setProperty("--arch-panel-focus", toColor(focusColor, 0.94));
- target.style.setProperty(
- "--arch-panel-font",
- functionsStyle.fontFamily || headerStyle.fontFamily || "inherit"
- );
- target.style.setProperty("--arch-workbench-header", toColor(workbenchHeader, 1));
- target.style.setProperty("--arch-workbench-accent", toColor(workbenchAccent, 1));
- }
- }
-
- function resolveForegroundColor(
- header,
- functionsContainer,
- anchor,
- headerStyle,
- functionsStyle
- ) {
- const salesPlanningElement = findTextElement(header, "Sales Planning");
- const salesPlanningColor = salesPlanningElement
- ? parseColor(window.getComputedStyle(salesPlanningElement).color)
- : null;
- const functionsItemColor = findVisibleChildColor(functionsContainer);
- const userColor = anchor
- ? parseColor(window.getComputedStyle(anchor).color)
- : null;
-
- return (
- userColor ||
- functionsItemColor ||
- parseColor(functionsStyle.color) ||
- salesPlanningColor ||
- parseColor(headerStyle.color) || {
- r: 255,
- g: 255,
- b: 255,
- a: 1,
- }
- );
- }
-
- function resolveBackgroundColor(element, style) {
- const directBackground = parseColor(style.backgroundColor);
-
- if (directBackground && directBackground.a > 0) {
- return directBackground;
- }
-
- let current = element.parentElement;
-
- while (current) {
- const currentBackground = parseColor(
- window.getComputedStyle(current).backgroundColor
- );
-
- if (currentBackground && currentBackground.a > 0) {
- return currentBackground;
- }
-
- current = current.parentElement;
- }
-
- return { r: 75, g: 51, b: 93, a: 1 };
- }
-
- function parseColor(value) {
- if (!value || value === "transparent") {
- return null;
- }
-
- const rgbMatch = value.match(
- /rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})(?:[\s,/]+([.\d]+))?\s*\)/i
- );
-
- if (rgbMatch) {
- return {
- r: Number.parseInt(rgbMatch[1], 10),
- g: Number.parseInt(rgbMatch[2], 10),
- b: Number.parseInt(rgbMatch[3], 10),
- a: rgbMatch[4] === undefined ? 1 : Number.parseFloat(rgbMatch[4]),
- };
- }
-
- const hexMatch = value.match(/^#([\da-f]{3,8})$/i);
-
- if (!hexMatch) {
- return null;
- }
-
- const hexValue = hexMatch[1];
-
- if (hexValue.length === 3 || hexValue.length === 4) {
- const [r, g, b, a = "f"] = hexValue.split("");
-
- return {
- r: Number.parseInt(r + r, 16),
- g: Number.parseInt(g + g, 16),
- b: Number.parseInt(b + b, 16),
- a: Number.parseInt(a + a, 16) / 255,
- };
- }
-
- if (hexValue.length === 6 || hexValue.length === 8) {
- return {
- r: Number.parseInt(hexValue.slice(0, 2), 16),
- g: Number.parseInt(hexValue.slice(2, 4), 16),
- b: Number.parseInt(hexValue.slice(4, 6), 16),
- a:
- hexValue.length === 8
- ? Number.parseInt(hexValue.slice(6, 8), 16) / 255
- : 1,
- };
- }
-
- return null;
- }
-
- function mix(base, tint, amount) {
- const ratio = clamp(amount, 0, 1);
-
- return {
- r: Math.round(base.r + (tint.r - base.r) * ratio),
- g: Math.round(base.g + (tint.g - base.g) * ratio),
- b: Math.round(base.b + (tint.b - base.b) * ratio),
- a: base.a + (tint.a - base.a) * ratio,
- };
- }
-
- function toColor(color, alphaOverride) {
- const alpha = alphaOverride ?? color.a ?? 1;
-
- return `rgba(${color.r}, ${color.g}, ${color.b}, ${clamp(alpha, 0, 1)})`;
- }
-
- function clamp(value, min, max) {
- return Math.min(Math.max(value, min), max);
- }
-
- function findUserAnchor(container) {
- const selector = [
- `button[label="${USER_BUTTON_LABEL}"]`,
- `[role="button"][label="${USER_BUTTON_LABEL}"]`,
- `oj-button[label="${USER_BUTTON_LABEL}"]`,
- `button[aria-label="${USER_BUTTON_LABEL}"]`,
- `[role="button"][aria-label="${USER_BUTTON_LABEL}"]`,
- `[label="${USER_BUTTON_LABEL}"]`,
- `[aria-label="${USER_BUTTON_LABEL}"]`,
- ].join(", ");
-
- const directMatch = container.querySelector(selector);
-
- if (directMatch && isVisible(directMatch)) {
- return directMatch;
- }
-
- const fallbackMatch = Array.from(container.querySelectorAll("*")).find(
- (element) => {
- if (!isVisible(element) || element.closest(`#${IDS.mount}`)) {
- return false;
- }
-
- return (
- element.getAttribute("label") === USER_BUTTON_LABEL ||
- element.getAttribute("aria-label") === USER_BUTTON_LABEL
- );
- }
- );
-
- return fallbackMatch || null;
- }
-
- function findTextElement(container, expectedText) {
- const normalizedExpected = normalizeText(expectedText);
- const elements = Array.from(container.querySelectorAll("*"));
-
- return (
- elements.find((element) => {
- const text = normalizeText(element.textContent || "");
-
- return text && text.includes(normalizedExpected);
- }) || null
- );
- }
-
- function resolveInsertionTarget(container, anchor, mount) {
- const directAnchor = anchor ? getDirectChild(anchor, container) : null;
- const fallbackTarget = findFirstInsertionTarget(container, mount);
- const candidate = directAnchor || fallbackTarget;
-
- if (!candidate) {
- return null;
- }
-
- if (candidate === mount) {
- return findNextValidSibling(container, mount);
- }
-
- return candidate.parentElement === container ? candidate : null;
- }
-
- function getDirectChild(element, container) {
- let current = element;
-
- while (current && current.parentElement !== container) {
- current = current.parentElement;
- }
-
- return current && current.parentElement === container ? current : null;
- }
-
- function findFirstInsertionTarget(container, mount) {
- return Array.from(container.children).find((child) => child !== mount) || null;
- }
-
- function findNextValidSibling(container, mount) {
- let sibling = mount.nextElementSibling;
-
- while (sibling) {
- if (sibling.parentElement === container) {
- return sibling;
- }
-
- sibling = sibling.nextElementSibling;
- }
-
- return null;
- }
-
- function findVisibleChildColor(container) {
- const visibleChild = Array.from(container.querySelectorAll("*")).find(
- (element) => isVisible(element) && !element.closest(`#${IDS.mount}`)
- );
-
- return visibleChild
- ? parseColor(window.getComputedStyle(visibleChild).color)
- : null;
- }
-
- function extractArray(value, candidateKeys = []) {
- if (Array.isArray(value)) {
- return value;
- }
-
- if (!value || typeof value !== "object") {
- return [];
- }
-
- for (const key of candidateKeys) {
- if (Array.isArray(value[key])) {
- return value[key];
- }
- }
-
- for (const nestedValue of Object.values(value)) {
- if (Array.isArray(nestedValue)) {
- return nestedValue;
- }
-
- if (nestedValue && typeof nestedValue === "object") {
- const nestedArray = extractArray(nestedValue, candidateKeys);
-
- if (nestedArray.length > 0) {
- return nestedArray;
- }
- }
- }
-
- return [];
- }
-
- async function mapWithConcurrency(items, concurrency, mapper) {
- const results = new Array(items.length);
- let nextIndex = 0;
-
- async function worker() {
- while (nextIndex < items.length) {
- const currentIndex = nextIndex;
- nextIndex += 1;
- results[currentIndex] = await mapper(items[currentIndex], currentIndex);
- }
- }
-
- const workerCount = Math.min(concurrency, items.length || 1);
- await Promise.all(Array.from({ length: workerCount }, () => worker()));
-
- return results;
- }
-
- function getStoredTheme() {
- try {
- const storedTheme = window.localStorage.getItem(STORAGE_KEYS.theme);
-
- return storedTheme === "dark" ? "dark" : "light";
- } catch {
- return "light";
- }
- }
-
- function setStoredTheme(theme) {
- try {
- window.localStorage.setItem(STORAGE_KEYS.theme, theme);
- } catch {
- // Ignore storage issues and keep the in-memory theme.
- }
- }
-
- function getStoredSidebarCollapsed() {
- try {
- return window.localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === "true";
- } catch {
- return false;
- }
- }
-
- function setStoredSidebarCollapsed(value) {
- try {
- window.localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, String(Boolean(value)));
- } catch {
- // Ignore storage issues and keep the in-memory state.
- }
- }
-
- function getStoredCalendarView() {
- try {
- return window.localStorage.getItem(STORAGE_KEYS.calendarView) === "week"
- ? "week"
- : "month";
- } catch {
- return "month";
- }
- }
-
- function setStoredCalendarView(value) {
- try {
- window.localStorage.setItem(
- STORAGE_KEYS.calendarView,
- value === "week" ? "week" : "month"
- );
- } catch {
- // Ignore storage issues and keep the in-memory calendar view.
- }
- }
-
- function getStoredTimeManagementSelectedDate() {
- try {
- const storedDate = window.localStorage.getItem(
- STORAGE_KEYS.timeManagementSelectedDate
- );
-
- return isValidDateInputValue(storedDate)
- ? storedDate
- : getDateInputValue(new Date());
- } catch {
- return getDateInputValue(new Date());
- }
- }
-
- function setStoredTimeManagementSelectedDate(value) {
- if (!isValidDateInputValue(value)) {
- return;
- }
-
- try {
- window.localStorage.setItem(
- STORAGE_KEYS.timeManagementSelectedDate,
- value
- );
- } catch {
- // Ignore storage issues and keep the in-memory selected date.
- }
- }
-
- function isValidDateInputValue(value) {
- if (!/^\d{4}-\d{2}-\d{2}$/.test(String(value || ""))) {
- return false;
- }
-
- const parsedDate = parseDatePreservingDateOnly(value);
-
- return !Number.isNaN(parsedDate.getTime());
- }
-
- function normalizeText(value) {
- return String(value || "").replace(/\s+/g, " ").trim();
- }
-
- function cleanString(value) {
- return normalizeText(value);
- }
-
- function firstNonEmptyString(values) {
- for (const value of values) {
- const normalized = cleanString(value);
-
- if (normalized) {
- return normalized;
- }
- }
-
- return "";
- }
-
- function extractAuthHeaders(tokenPayload) {
- if (!tokenPayload) {
- return {};
- }
-
- if (typeof tokenPayload === "string") {
- return {
- authorization: `Bearer ${tokenPayload}`,
- };
- }
-
- const tokenSources = [
- tokenPayload,
- tokenPayload?.token,
- tokenPayload?.tokens,
- tokenPayload?.oauth,
- tokenPayload?.oauthToken,
- tokenPayload?.authentication,
- tokenPayload?.auth,
- ].filter(Boolean);
- const headers = {};
- const explicitHeaderSources = [
- tokenPayload?.headers,
- tokenPayload?.requestHeaders,
- tokenPayload?.authHeaders,
- ].filter((value) => value && typeof value === "object");
-
- for (const explicitHeaderSource of explicitHeaderSources) {
- for (const [key, value] of Object.entries(explicitHeaderSource)) {
- const normalizedKey = cleanString(key).toLowerCase();
- const normalizedValue = cleanString(value);
-
- if (!normalizedKey || !normalizedValue) {
- continue;
- }
-
- headers[normalizedKey] = normalizedValue;
- }
- }
-
- for (const tokenSource of tokenSources) {
- const tokenType =
- firstNonEmptyString([
- tokenSource?.tokenType,
- tokenSource?.type,
- tokenSource?.token_type,
- ]) || "Bearer";
- const accessToken = firstNonEmptyString([
- tokenSource?.accessToken,
- tokenSource?.access_token,
- tokenSource?.token,
- tokenSource?.jwt,
- ]);
- const idToken = firstNonEmptyString([
- tokenSource?.idToken,
- tokenSource?.id_token,
- ]);
-
- if (accessToken && !headers.authorization) {
- headers.authorization = `${tokenType} ${accessToken}`;
- }
-
- if (idToken && !headers["x-id-token"]) {
- headers["x-id-token"] = idToken;
- }
- }
-
- return headers;
- }
-
- function normalizeString(value) {
- return normalizeText(value).toUpperCase();
- }
-
- function toBridgeRequestUrl(url) {
- const normalized = resolveRequestUrl(url);
-
- if (!normalized) {
- return normalized;
- }
-
- try {
- const parsedUrl = new URL(normalized, window.location.origin);
-
- if (parsedUrl.origin === window.location.origin) {
- return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
- }
-
- return parsedUrl.toString();
- } catch {
- return normalized;
- }
- }
-
- function resolveRequestUrl(url) {
- const normalized = cleanString(url);
-
- if (!normalized) {
- return "";
- }
-
- try {
- const baseOrigin =
- isHcmWelcomePage() && normalized.startsWith("/")
- ? TARGET_ORIGIN
- : window.location.origin;
-
- return new URL(normalized, baseOrigin).toString();
- } catch {
- return normalized;
- }
- }
-
- function toNumber(value) {
- if (typeof value === "number" && Number.isFinite(value)) {
- return value;
- }
-
- const numeric = Number.parseFloat(String(value ?? "").replace(/,/g, ""));
- return Number.isFinite(numeric) ? numeric : 0;
- }
-
- function toIsoDateString(value) {
- const normalized = cleanString(value);
-
- if (!normalized) {
- return "";
- }
-
- const date = new Date(`${normalized}T12:00:00.000Z`);
-
- return Number.isNaN(date.getTime()) ? normalized : date.toISOString();
- }
-
- function isVisible(element) {
- const style = window.getComputedStyle(element);
- const rect = element.getBoundingClientRect();
-
- return (
- style.display !== "none" &&
- style.visibility !== "hidden" &&
- Number.parseFloat(style.opacity || "1") > 0 &&
- rect.width > 0 &&
- rect.height > 0
- );
- }
-
- function formatWholeNumber(value) {
- return new Intl.NumberFormat("en-US", {
- maximumFractionDigits: 0,
- }).format(value);
- }
-
- function formatCurrency(value) {
- return new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- maximumFractionDigits: 0,
- }).format(value || 0);
- }
-
- function formatHours(value) {
- return new Intl.NumberFormat("pt-BR", {
- maximumFractionDigits: 2,
- }).format(toNumber(value));
- }
-
- function formatWeekNumber(value) {
- return String(toNumber(value)).padStart(2, "0");
- }
-
- function formatOperationalWeekLabel(weekInfo) {
- if (!weekInfo) {
- return "";
- }
-
- return `FY${formatWeekNumber(weekInfo.fiscalYear % 100)}-Q${
- weekInfo.quarter
- }-W${formatWeekNumber(weekInfo.weekNumber)}`;
- }
-
- function formatPercent(value) {
- return new Intl.NumberFormat("en-US", {
- maximumFractionDigits: 1,
- }).format(value || 0) + "%";
- }
-
- function formatProgressPercent(value) {
- return new Intl.NumberFormat("en-US", {
- maximumFractionDigits: 0,
- }).format(value || 0) + "%";
- }
-
- function formatRelativeMoment(value) {
- if (!value) {
- return "-";
- }
-
- const target = new Date(value);
-
- if (Number.isNaN(target.getTime())) {
- return String(value);
- }
-
- let remaining = Math.max(Date.now() - target.getTime(), 0);
- const day = 24 * 60 * 60 * 1000;
- const hour = 60 * 60 * 1000;
- const minute = 60 * 1000;
- const second = 1000;
- const days = Math.floor(remaining / day);
- remaining -= days * day;
- const hours = Math.floor(remaining / hour);
- remaining -= hours * hour;
- const minutes = Math.floor(remaining / minute);
- remaining -= minutes * minute;
- const seconds = Math.floor(remaining / second);
-
- return `${days}d ${hours}h ${minutes}m ${seconds}s`;
- }
-
- function formatDate(value) {
- if (!value) {
- return "";
- }
-
- const date = parseDatePreservingDateOnly(value);
-
- if (Number.isNaN(date.getTime())) {
- return String(value);
- }
-
- return new Intl.DateTimeFormat("en-US", {
- dateStyle: "medium",
- }).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 formatDateInputDisplay(value) {
- const date = parseDatePreservingDateOnly(value);
-
- if (Number.isNaN(date.getTime())) {
- return cleanString(value);
- }
-
- const day = String(date.getDate()).padStart(2, "0");
- const month = String(date.getMonth() + 1).padStart(2, "0");
- const year = date.getFullYear();
-
- return `${day}/${month}/${year}`;
- }
-
- 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 "";
- }
-
- const date = new Date(value);
-
- if (Number.isNaN(date.getTime())) {
- return String(value);
- }
-
- return new Intl.DateTimeFormat("en-US", {
- dateStyle: "medium",
- timeStyle: "short",
- }).format(date);
- }
-
- function getLocalDateKey(value) {
- if (!value) {
- return "";
- }
-
- 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 getOperationalWeekInfo(value) {
- const date = parseDatePreservingDateOnly(value);
-
- if (Number.isNaN(date.getTime())) {
- return null;
- }
-
- const operationalYearStart = new Date(date.getFullYear(), 5, 1);
-
- if (date < operationalYearStart) {
- operationalYearStart.setFullYear(operationalYearStart.getFullYear() - 1);
- }
-
- const normalizedDate = new Date(
- date.getFullYear(),
- date.getMonth(),
- date.getDate()
- );
- const normalizedStart = new Date(
- operationalYearStart.getFullYear(),
- operationalYearStart.getMonth(),
- operationalYearStart.getDate()
- );
- const daysSinceStart = Math.floor(
- (normalizedDate.getTime() - normalizedStart.getTime()) / 86400000
- );
- const monthsSinceStart =
- (normalizedDate.getFullYear() - normalizedStart.getFullYear()) * 12 +
- normalizedDate.getMonth() -
- normalizedStart.getMonth();
-
- return {
- fiscalYear: operationalYearStart.getFullYear() + 1,
- quarter: Math.floor(monthsSinceStart / 3) + 1,
- weekNumber: Math.floor(daysSinceStart / 7) + 1,
- };
- }
-
- function getCalendarRowWeekDate(rowStartDate) {
- const weekDate = new Date(rowStartDate);
-
- // The calendar renders rows from Sunday, but ISO week numbering starts on Monday.
- if (weekDate.getDay() === 0) {
- weekDate.setDate(weekDate.getDate() + 1);
- }
-
- return weekDate;
- }
-
- function normalizeTimeEntriesSummary(payload) {
- if (Array.isArray(payload)) {
- return payload;
- }
-
- const items = extractArray(payload, [
- "items",
- "data",
- "results",
- "content",
- "entries",
- "timeEntries",
- "summary",
- "summaries",
- ]);
-
- if (items.length) {
- return items;
- }
-
- if (payload && typeof payload === "object") {
- return [payload];
- }
-
- return [];
- }
-
- function groupTimeEntriesByResourceName(items) {
- const groups = new Map();
-
- for (const item of items) {
- const resourceName =
- getTimeEntryResourceName(item) || "Sem resourceName";
- const group = groups.get(resourceName) || {
- resourceName,
- items: [],
- };
-
- group.items.push(item);
- groups.set(resourceName, group);
- }
-
- return Array.from(groups.values()).sort((left, right) => {
- return left.resourceName.localeCompare(right.resourceName);
- });
- }
-
- function getTimeEntryResourceName(item) {
- const directValue = getFieldValue(item, [
- "resourceName",
- "ResourceName",
- "resource_name",
- "resource.name",
- "Resource.Name",
- "resource.displayName",
- "Resource.DisplayName",
- "resource.display_name",
- "employeeName",
- "EmployeeName",
- "personName",
- "PersonName",
- ]);
-
- if (directValue) {
- return cleanString(directValue);
- }
-
- const flattenedMatch = flattenDisplayRecord(item).find(([key, value]) => {
- return key.toLowerCase().endsWith("resourcename") && cleanString(value);
- });
-
- return flattenedMatch ? cleanString(flattenedMatch[1]) : "";
- }
-
- function getTimeEntrySrNumber(item) {
- return cleanString(
- getTimeEntryField(item, [
- "srNumber",
- "SrNumber",
- "SRNumber",
- "serviceRequestNumber",
- "ServiceRequestNumber",
- "serviceRequest.srNumber",
- "ServiceRequest.SrNumber",
- ])
- );
- }
-
- function getTimeEntryId(item) {
- return cleanString(
- getTimeEntryField(item, [
- "id",
- "Id",
- "ID",
- "timeEntryId",
- "TimeEntryId",
- "timeEntryID",
- "TimeEntryID",
- ])
- );
- }
-
- function getTimeEntryActivity(item) {
- return cleanString(
- getTimeEntryField(item, [
- "activityCode",
- "ActivityCode",
- "activity.code",
- "Activity.Code",
- ])
- );
- }
-
- function getTimeEntryTaskType(item) {
- return cleanString(
- getTimeEntryField(item, [
- "TaskType",
- "taskType",
- "task_type",
- "task.type",
- "Task.Type",
- ])
- );
- }
-
- function getTimeEntryTimeSpent(item) {
- return toNumber(
- getTimeEntryField(item, [
- "timeSpent",
- "TimeSpent",
- "time_spent",
- "time.spent",
- "Time.Spent",
- ])
- );
- }
-
- 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);
-
- if (directValue !== undefined && directValue !== null && directValue !== "") {
- return directValue;
- }
-
- const normalizedKeys = keys.map((key) => key.toLowerCase().replace(/[^a-z0-9]/g, ""));
- const flattenedMatch = flattenDisplayRecord(item).find(([key, value]) => {
- const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
-
- return normalizedKeys.includes(normalizedKey) && cleanString(value);
- });
-
- return flattenedMatch ? flattenedMatch[1] : "";
- }
-
- function flattenDisplayRecord(value, prefix = "", output = []) {
- if (!value || typeof value !== "object" || Array.isArray(value)) {
- if (prefix) {
- output.push([prefix, value]);
- }
-
- return output;
- }
-
- for (const [key, nestedValue] of Object.entries(value)) {
- const nextKey = prefix ? `${prefix}.${key}` : key;
-
- if (
- nestedValue &&
- typeof nestedValue === "object" &&
- !Array.isArray(nestedValue)
- ) {
- flattenDisplayRecord(nestedValue, nextKey, output);
- } else {
- output.push([nextKey, nestedValue]);
- }
- }
-
- return output;
- }
-
- function formatDisplayValue(value) {
- if (value === null || value === undefined || value === "") {
- return "-";
- }
-
- if (Array.isArray(value) || typeof value === "object") {
- return JSON.stringify(value);
- }
-
- return String(value);
- }
-
- function parseDatePreservingDateOnly(value) {
- if (value instanceof Date) {
- return value;
- }
-
- if (typeof value === "string") {
- const dateOnlyMatch = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
-
- if (dateOnlyMatch) {
- return new Date(
- Number(dateOnlyMatch[1]),
- Number(dateOnlyMatch[2]) - 1,
- Number(dateOnlyMatch[3])
- );
- }
- }
-
- return new Date(value);
- }
-
- function getErrorMessage(error) {
- if (error instanceof Error) {
- return error.message;
- }
-
- return "Unexpected error while fetching live data.";
- }
-
- function escapeHtml(value) {
- return String(value ?? "")
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
- }
-
- function cssEscape(value) {
- if (window.CSS?.escape) {
- return window.CSS.escape(String(value || ""));
- }
-
- return String(value || "").replace(/["\\]/g, "\\$&");
- }
-
- function lockScroll() {
- document.documentElement.classList.add(CLASSES.modalOpen);
- document.body.classList.add(CLASSES.modalOpen);
- }
-
- function unlockScroll() {
- document.documentElement.classList.remove(CLASSES.modalOpen);
- document.body.classList.remove(CLASSES.modalOpen);
- }
-
function removeModal() {
const overlay = document.getElementById(IDS.overlay);
@@ -12168,69 +1047,6 @@
closeModal();
}
- function handleBridgeMessage(event) {
- const message = event.data;
-
- if (!message || message.source !== BRIDGE.source) {
- return;
- }
-
- if (message.type === BRIDGE.workbenchAuthHeadersType) {
- rememberWorkbenchAuthHeaders(message.headers);
- return;
- }
-
- if (event.source !== window) {
- return;
- }
-
- if (message.type === BRIDGE.readyType) {
- return;
- }
-
- if (
- message.type !== BRIDGE.resultType &&
- message.type !== BRIDGE.headersResultType
- ) {
- return;
- }
-
- const pending = pendingBridgeRequests.get(message.requestId);
-
- if (!pending) {
- return;
- }
-
- pendingBridgeRequests.delete(message.requestId);
- window.clearTimeout(pending.timeout);
- pending.resolve(
- message.type === BRIDGE.headersResultType ? message.headers || {} : message
- );
- }
-
- function rememberWorkbenchAuthHeaders(headers) {
- if (!headers || typeof headers !== "object") {
- return;
- }
-
- const nextHeaders = {
- ...(appState.sessionAuthHeaders || {}),
- };
-
- for (const [key, value] of Object.entries(headers)) {
- const normalizedKey = cleanString(key).toLowerCase();
- const normalizedValue = cleanString(value);
-
- if (!normalizedKey || !normalizedValue) {
- continue;
- }
-
- nextHeaders[normalizedKey] = normalizedValue;
- }
-
- appState.sessionAuthHeaders = nextHeaders;
- }
-
function observePage() {
const observer = new MutationObserver(() => {
scheduleSync();
@@ -12269,7 +1085,7 @@
}
const syncLoadedToken = () => {
- syncWorklistIframeToken({ force: true });
+ hcmPage.syncWorklistIframeToken({ force: true });
};
if (document.readyState === "loading") {
@@ -12287,8 +1103,24 @@
return;
}
+ if (appState.spaFrameLifecycleIntervalId) {
+ return;
+ }
+
const syncFrame = () => {
- ensureSpaFrame();
+ void ensureSpaFrame().catch((error) => {
+ appState.lastSpaFrameLifecycleError =
+ error instanceof Error ? error.message : String(error);
+ });
+ };
+
+ const stopSyncFrame = () => {
+ if (!appState.spaFrameLifecycleIntervalId) {
+ return;
+ }
+
+ window.clearInterval(appState.spaFrameLifecycleIntervalId);
+ appState.spaFrameLifecycleIntervalId = null;
};
if (document.readyState === "loading") {
@@ -12299,7 +1131,9 @@
window.addEventListener("load", syncFrame);
window.addEventListener("pageshow", syncFrame);
- window.setInterval(syncFrame, 5000);
+ window.addEventListener("pagehide", stopSyncFrame);
+ window.addEventListener("beforeunload", stopSyncFrame);
+ appState.spaFrameLifecycleIntervalId = window.setInterval(syncFrame, 15000);
}
window.addEventListener("message", handleBridgeMessage);
diff --git a/manifest.json b/manifest.json
index f306458..d6ed3cc 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,10 +1,9 @@
-{
+{
"manifest_version": 3,
- "name": "Arch Panel Injector",
- "version": "1.0.4",
+ "name": "Arch Panel",
+ "version": "1.0.5",
"description": "Insere o botao Arch panel no header do Oracle Workload Workbench.",
"permissions": [
- "cookies",
"declarativeNetRequestWithHostAccess",
"scripting",
"tabs"
@@ -35,7 +34,35 @@
"https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html*",
"https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*"
],
+ "css": [
+ "src/content/styles.css"
+ ],
"js": [
+ "src/shared/config.js",
+ "src/shared/utils.js",
+ "src/services/preferences-service.js",
+ "src/services/storage-service.js",
+ "src/services/chrome-service.js",
+ "src/services/transport-service.js",
+ "src/repositories/cache-repository.js",
+ "src/repositories/workbench-repository.js",
+ "src/repositories/comcip-repository.js",
+ "src/domain/ramp-forecast.js",
+ "src/domain/workload-dataset.js",
+ "src/domain/workload-calendar.js",
+ "src/domain/time-management.js",
+ "src/domain/time-management-data.js",
+ "src/content/shell-view.js",
+ "src/content/templates.js",
+ "src/content/hcm-page.js",
+ "src/content/modal-events.js",
+ "src/content/workbench-controller.js",
+ "src/content/manage-time-controller.js",
+ "src/content/export-html.js",
+ "src/views/render-helpers.js",
+ "src/views/dashboard-view.js",
+ "src/views/workbench-modals-view.js",
+ "src/views/manage-time-view.js",
"content-script.js"
],
"run_at": "document_start"
@@ -53,128 +80,3 @@
}
]
}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/page-bridge.js b/page-bridge.js
index 143ccc3..702f557 100644
--- a/page-bridge.js
+++ b/page-bridge.js
@@ -10,6 +10,12 @@
const EXTRA_CAPTURE_ORIGINS = new Set([
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com",
]);
+ const ALLOWED_TOP_MESSAGE_ORIGINS = new Set([
+ "https://eeho.fa.us2.oraclecloud.com",
+ "https://spa.oracle.com",
+ ]);
+ const AUTH_STORAGE_HINT_PATTERN =
+ /(authorization|token|wwb|wlwb|provider|spa-ts|spats)/i;
const SAFE_HEADER_BLOCKLIST = new Set([
"accept-encoding",
"accept-language",
@@ -48,6 +54,51 @@
}
}
+ function getAllowedTopMessageOrigin() {
+ const candidates = [];
+
+ try {
+ const referrerOrigin = document.referrer
+ ? new URL(document.referrer).origin
+ : "";
+
+ if (referrerOrigin) {
+ candidates.push(referrerOrigin);
+ }
+ } catch {
+ // Referrer parsing is best-effort; fall back to ancestorOrigins/current origin.
+ }
+
+ try {
+ const ancestorOrigins = Array.from(window.location?.ancestorOrigins || []);
+ candidates.push(...ancestorOrigins);
+ } catch {
+ // ancestorOrigins is browser-specific and may be unavailable.
+ }
+
+ if (window.top === window) {
+ candidates.push(getSafeTargetOrigin());
+ }
+
+ for (const origin of candidates) {
+ if (ALLOWED_TOP_MESSAGE_ORIGINS.has(origin)) {
+ return origin;
+ }
+ }
+
+ return "";
+ }
+
+ function safePostToTop(payload) {
+ const targetOrigin = getAllowedTopMessageOrigin();
+
+ if (!targetOrigin) {
+ return false;
+ }
+
+ return safePostMessage(payload, window.top, targetOrigin);
+ }
+
const nativeFetch = window.fetch.bind(window);
const originalOpen = window.XMLHttpRequest.prototype.open;
const originalSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
@@ -215,15 +266,13 @@
return;
}
- safePostMessage(
+ safePostToTop(
{
source: MESSAGE_SOURCE,
type: "ARCH_PANEL_WORKBENCH_AUTH_HEADERS",
url: normalizeUrl(url),
headers: authHeaders,
- },
- window.top,
- "*"
+ }
);
}
@@ -356,7 +405,7 @@
const key = storage.key(index) || "";
const value = storage.getItem(key) || "";
- if (key || value) {
+ if (isAuthStorageCandidate(key, value)) {
candidates.push([key, value]);
}
}
@@ -372,6 +421,25 @@
return headers;
}
+ function isAuthStorageCandidate(key, value) {
+ const normalizedKey = String(key || "");
+ const normalizedValue = String(value || "").trim();
+
+ if (!normalizedKey && !normalizedValue) {
+ return false;
+ }
+
+ if (AUTH_STORAGE_HINT_PATTERN.test(normalizedKey)) {
+ return true;
+ }
+
+ if (looksLikeJwt(normalizedValue)) {
+ return true;
+ }
+
+ return AUTH_STORAGE_HINT_PATTERN.test(normalizedValue);
+ }
+
function collectAuthFromStorageEntry(headers, key, value) {
const normalizedKey = String(key || "").toLowerCase();
const normalizedValue = String(value || "").trim();
diff --git a/src/background/config.js b/src/background/config.js
new file mode 100644
index 0000000..dad9ee1
--- /dev/null
+++ b/src/background/config.js
@@ -0,0 +1,39 @@
+(() => {
+ const COMCIP_ORIGIN = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
+ const COMCIP_APP_URL =
+ `${COMCIP_ORIGIN}/ic/builder/rt/oalset_semc/live/webApps/Dashboard/?page=shell&shell=main&main=service-requests-detailed-view`;
+ 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*`;
+ const SPA_WORKBENCH_FRAME_URL =
+ "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench";
+ const SPA_WORKBENCH_FRAME_ID = "arch-panel-extension-spa-frame";
+ const SPA_WORKBENCH_FRAME_HOST_ID = "arch-panel-extension-spa-frame-host";
+ const SPA_FRAME_DNR_RULE_ID = 92001;
+
+ globalThis.ArchPanelBackgroundConfig = Object.freeze({
+ COMCIP_ORIGIN,
+ COMCIP_APP_URL,
+ COMCIP_QUERY_URL,
+ COMCIP_CLIENT_ID_PROBE_URL,
+ COMCIP_TIME_ENTRY_APP_URL,
+ COMCIP_TIME_ENTRY_CLIENT_ID_PROBE_URL,
+ COMCIP_TIME_ENTRY_APP_VERSION,
+ COMCIP_TAB_URL_PATTERN,
+ COMCIP_TIME_ENTRY_TAB_URL_PATTERN,
+ SPA_WORKBENCH_FRAME_URL,
+ SPA_WORKBENCH_FRAME_ID,
+ SPA_WORKBENCH_FRAME_HOST_ID,
+ SPA_FRAME_DNR_RULE_ID,
+ });
+})();
diff --git a/src/content/export-html.js b/src/content/export-html.js
new file mode 100644
index 0000000..423cd10
--- /dev/null
+++ b/src/content/export-html.js
@@ -0,0 +1,653 @@
+(() => {
+ function escapeJsonForInlineScript(json) {
+ return String(json || "")
+ .replace(//g, "\\u003e")
+ .replace(/&/g, "\\u0026")
+ .replace(/\u2028/g, "\\u2028")
+ .replace(/\u2029/g, "\\u2029");
+ }
+
+ function createExportHtml(dataset) {
+ const json = escapeJsonForInlineScript(JSON.stringify(dataset.exportPayload));
+
+ return `
+
+
+
+
+ Arch Panel Dashboard
+
+
+
+
+
+
+
+`;
+ }
+
+
+ globalThis.ArchPanelExportHtml = Object.freeze({
+ createExportHtml,
+ });
+})();
diff --git a/src/content/hcm-page.js b/src/content/hcm-page.js
new file mode 100644
index 0000000..185142e
--- /dev/null
+++ b/src/content/hcm-page.js
@@ -0,0 +1,118 @@
+(() => {
+ function create({
+ HCM_MY_INFORMATION_APPS_GROUP_ID,
+ WORKLIST_ORIGIN,
+ WORKLIST_SAASUI_PATHNAME,
+ IDS,
+ cleanString,
+ hcmTileTemplate,
+ onOpenArchPanel,
+ writeCachedWorklistToken,
+ }) {
+ let lastPersistedWorklistIframeUrl = "";
+
+ function syncHcmTile() {
+ const appsGroup = document.getElementById(HCM_MY_INFORMATION_APPS_GROUP_ID);
+ const addTile = appsGroup?.querySelector(
+ ".flat-grid-cell.flat-grid-cell-addicon"
+ );
+
+ if (!addTile?.parentElement) {
+ document.getElementById(IDS.hcmTile)?.remove();
+ return;
+ }
+
+ const tile = document.getElementById(IDS.hcmTile) || createHcmTile();
+
+ if (
+ tile.parentElement !== addTile.parentElement ||
+ tile.nextElementSibling !== addTile
+ ) {
+ addTile.parentElement.insertBefore(tile, addTile);
+ }
+ }
+
+ function syncWorklistIframeToken(options = {}) {
+ for (const frame of document.querySelectorAll("iframe")) {
+ const tokenPayload = extractWorklistIframeToken(frame);
+
+ if (tokenPayload?.token) {
+ persistWorklistIframeToken(tokenPayload, options);
+ return tokenPayload;
+ }
+ }
+
+ return null;
+ }
+
+ function extractWorklistIframeToken(frame) {
+ const rawSrc = cleanString(frame?.getAttribute?.("src") || frame?.src);
+
+ if (!rawSrc) {
+ return null;
+ }
+
+ try {
+ const url = new URL(rawSrc, window.location.href);
+ const token = cleanString(url.searchParams.get("token"));
+
+ if (
+ url.origin !== WORKLIST_ORIGIN ||
+ url.pathname !== WORKLIST_SAASUI_PATHNAME ||
+ !token
+ ) {
+ return null;
+ }
+
+ return {
+ token,
+ url: url.toString(),
+ };
+ } catch {
+ return null;
+ }
+ }
+
+ function persistWorklistIframeToken(tokenPayload, options = {}) {
+ if (!options.force && tokenPayload.url === lastPersistedWorklistIframeUrl) {
+ return;
+ }
+
+ lastPersistedWorklistIframeUrl = tokenPayload.url;
+ void writeCachedWorklistToken(tokenPayload);
+ }
+
+ function createHcmTile() {
+ const template = document.createElement("template");
+ template.innerHTML = hcmTileTemplate(IDS.hcmTile);
+ const tile = template.content.firstElementChild;
+
+ tile.addEventListener("click", openHcmArchPanelTile);
+ tile.addEventListener("keydown", (event) => {
+ if (event.key === "Enter" || event.key === " ") {
+ openHcmArchPanelTile(event);
+ }
+ });
+ tile.tabIndex = 0;
+ tile.setAttribute("role", "button");
+ tile.setAttribute("aria-label", "Open Arch Panel");
+
+ return tile;
+ }
+
+ function openHcmArchPanelTile(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ onOpenArchPanel();
+ }
+
+ return Object.freeze({
+ syncHcmTile,
+ syncWorklistIframeToken,
+ });
+ }
+
+ globalThis.ArchPanelHcmPage = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/content/manage-time-controller.js b/src/content/manage-time-controller.js
new file mode 100644
index 0000000..70b71ce
--- /dev/null
+++ b/src/content/manage-time-controller.js
@@ -0,0 +1,944 @@
+(() => {
+ function create(dependencies) {
+ const {
+ appState,
+ COMCIP_REQUEST,
+ comcipRepository,
+ readCachedTaskTypePayload,
+ writeCachedTaskTypePayload,
+ getStoredTimeManagementSelectedDate,
+ setStoredTimeManagementSelectedDate,
+ renderModal,
+ cleanString,
+ normalizeString,
+ getDateInputValue,
+ parseDatePreservingDateOnly,
+ getFieldValue,
+ extractArray,
+ getErrorMessage,
+ cssEscape,
+ getTimeManagementRows,
+ isTimeManagementSaveEnabled,
+ getTimeManagementPendingChanges,
+ createTimeManagementSavePayload,
+ isNonServiceRequestSrNumber,
+ normalizeInfoWeekResponse,
+ normalizeTimeManagementServiceRequests,
+ mergeTimeManagementServiceRequests,
+ normalizeTimeManagementEntryRows,
+ normalizeTimeManagementEntryRowSelection,
+ getActivityRequestSrNumbers,
+ normalizeActivityRequest,
+ normalizeTaskTypeRequest,
+ getTimeManagementTaskTypeOptions,
+ getTimeManagementActivityOptions,
+ findTimeManagementServiceRequest,
+ findOptionValue,
+ } = dependencies;
+
+ let requestCounter = 0;
+
+ 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());
+ }
+
+ async function openTimeManagementModal() {
+ const selectedDate = getStoredTimeManagementSelectedDate();
+
+ appState.timeManagementModal = createTimeManagementLoadingState(selectedDate);
+ renderModal();
+ await loadTimeManagementData(selectedDate);
+ }
+
+ async function loadTimeManagementData(selectedDate) {
+ const normalizedDate = cleanString(selectedDate) || getDateInputValue(new Date());
+ const requestId = `${Date.now()}-${++requestCounter}`;
+
+ setStoredTimeManagementSelectedDate(normalizedDate);
+ appState.timeManagementDatePicker = {
+ ...appState.timeManagementDatePicker,
+ isOpen: false,
+ };
+ appState.timeManagementModal = {
+ ...(appState.timeManagementModal || {}),
+ requestId,
+ selectedDate: normalizedDate,
+ status: "loading",
+ errorMessage: "",
+ };
+ renderModal();
+
+ try {
+ const resourcePartyId = getCurrentUserResourcePartyId();
+ const taskTypePromise = fetchTaskTypeRequest();
+
+ if (!resourcePartyId) {
+ throw new Error("resourceCurrentUser.resourcePartyId nao esta disponivel.");
+ }
+
+ const weekInfoPayload = await comcipRepository.fetchInfoWeekRequest(
+ normalizedDate
+ );
+ const weekDays = normalizeInfoWeekResponse(weekInfoPayload);
+ const weekId = cleanString(weekDays[0]?.weekId);
+
+ if (!weekId) {
+ throw new Error("WeekId nao encontrado no retorno de infoWeekRequest.");
+ }
+
+ const timeEntryWeekUrl = comcipRepository.buildTimeEntryWeekUrl(
+ resourcePartyId,
+ weekId
+ );
+ const [srPayload, taskTypePayload, timeEntryWeekPayload] = await Promise.all([
+ comcipRepository.fetchActiveServiceRequests(resourcePartyId, weekId),
+ taskTypePromise,
+ comcipRepository.fetchTimeEntryWeek(timeEntryWeekUrl),
+ ]);
+ const timeEntryRows = normalizeTimeManagementEntryRows(
+ timeEntryWeekPayload,
+ weekDays
+ );
+ const serviceRequests = mergeTimeManagementServiceRequests(
+ normalizeTimeManagementServiceRequests(srPayload),
+ timeEntryRows
+ );
+ const activitySrNumbers = getActivityRequestSrNumbers(
+ srPayload,
+ serviceRequests
+ );
+ const activityRequest = await fetchCachedActivityRequest(activitySrNumbers);
+ const activityRequestUrl = activityRequest.url;
+ const activityPayload = activityRequest.payload;
+ const activityOptionsBySr = normalizeActivityRequest(
+ activityPayload,
+ serviceRequests
+ );
+ const taskTypes = normalizeTaskTypeRequest(taskTypePayload);
+ const rows = timeEntryRows;
+ 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 || "",
+ highlightRowId: "",
+ deletedTimeEntryIds: [],
+ isSaving: false,
+ saveErrorMessage: "",
+ };
+ 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: [],
+ highlightRowId: "",
+ deletedTimeEntryIds: [],
+ isSaving: false,
+ saveErrorMessage: "",
+ };
+ }
+
+ function createBlankTimeManagementEntryRow(srNumber = "", source = "blank") {
+ return {
+ id: `row-${Date.now()}-${Math.random().toString(16).slice(2)}`,
+ timeEntryId: "",
+ selectedSrNumber: cleanString(srNumber),
+ selectedActivityValue: "",
+ selectedTaskTypeValue: "",
+ dayHours: {},
+ source,
+ originalSignature: "",
+ };
+ }
+
+ function addTimeManagementEntryRow(type) {
+ if (!appState.timeManagementModal) {
+ return;
+ }
+
+ const existingDuplicateRow = findExistingTimeManagementDuplicateRow(
+ appState.timeManagementModal,
+ type
+ );
+
+ if (existingDuplicateRow) {
+ highlightTimeManagementDuplicateRow(existingDuplicateRow.id);
+ return;
+ }
+
+ const srNumber = type === "non-service" ? "non-service-sr" : "";
+ const nextRow = createBlankTimeManagementEntryRow(srNumber, "manual");
+ const nextRows = [
+ ...getTimeManagementRows(appState.timeManagementModal),
+ nextRow,
+ ];
+
+ appState.timeManagementModal = {
+ ...appState.timeManagementModal,
+ timeEntryRows: nextRows,
+ highlightRowId: "",
+ saveErrorMessage: "",
+ };
+ appState.pendingFocusSelector = getTimeManagementComboboxFocusSelector(
+ nextRow.id,
+ type === "non-service" ? "taskType" : "sr"
+ );
+ renderModal();
+ }
+
+ function removeTimeManagementEntryRow(rowId) {
+ if (!appState.timeManagementModal) {
+ return;
+ }
+
+ const normalizedRowId = cleanString(rowId);
+ const rows = getTimeManagementRows(appState.timeManagementModal);
+ const removedRow = rows.find(
+ (row) => cleanString(row?.id) === normalizedRowId
+ );
+ const removedTimeEntryId = cleanString(removedRow?.timeEntryId);
+ const nextRows = rows.filter(
+ (row) => cleanString(row?.id) !== normalizedRowId
+ );
+ const deletedTimeEntryIds = removedTimeEntryId
+ ? Array.from(
+ new Set([
+ ...(appState.timeManagementModal.deletedTimeEntryIds || []),
+ removedTimeEntryId,
+ ])
+ )
+ : appState.timeManagementModal.deletedTimeEntryIds || [];
+
+ appState.timeManagementModal = {
+ ...appState.timeManagementModal,
+ timeEntryRows: nextRows,
+ selectedSrNumber: nextRows[0]?.selectedSrNumber || "",
+ selectedActivityValue: nextRows[0]?.selectedActivityValue || "",
+ selectedTaskTypeValue: nextRows[0]?.selectedTaskTypeValue || "",
+ highlightRowId: "",
+ deletedTimeEntryIds,
+ saveErrorMessage: "",
+ };
+ appState.pendingFocusSelector = getTimeManagementPostDeleteFocusSelector(
+ nextRows,
+ rows,
+ normalizedRowId
+ );
+ renderModal();
+ }
+
+ function getTimeManagementComboboxFocusSelector(rowId, field) {
+ return `.arch-panel-extension-combobox input[data-row-id="${cssEscape(rowId)}"][data-combobox-field="${cssEscape(field)}"]`;
+ }
+
+ function getTimeManagementPostDeleteFocusSelector(
+ nextRows,
+ previousRows,
+ removedRowId
+ ) {
+ if (!nextRows.length) {
+ return "";
+ }
+
+ const removedIndex = previousRows.findIndex(
+ (row) => cleanString(row?.id) === cleanString(removedRowId)
+ );
+ const focusIndex = Math.min(Math.max(removedIndex, 0), nextRows.length - 1);
+ const focusRow = nextRows[focusIndex];
+
+ return focusRow?.id
+ ? `.arch-panel-extension-time-management-delete[data-row-id="${cssEscape(focusRow.id)}"]`
+ : "";
+ }
+
+ 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 duplicateRow = findDuplicateTimeManagementSrRow(
+ appState.timeManagementModal,
+ selectedSrNumber,
+ rowId
+ );
+
+ if (duplicateRow) {
+ highlightTimeManagementDuplicateRow(duplicateRow.id);
+ return;
+ }
+
+ 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 || "",
+ highlightRowId: "",
+ saveErrorMessage: "",
+ };
+ 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 duplicateRow = findDuplicateTimeManagementNonServiceTaskTypeRow(
+ appState.timeManagementModal,
+ value,
+ rowId
+ );
+
+ if (duplicateRow) {
+ highlightTimeManagementDuplicateRow(duplicateRow.id);
+ return;
+ }
+ }
+
+ const nextRows = updateTimeManagementRows(appState.timeManagementModal, rowId, {
+ ...(targetRow || createBlankTimeManagementEntryRow()),
+ [fieldName]: value,
+ });
+
+ appState.timeManagementModal = {
+ ...appState.timeManagementModal,
+ timeEntryRows: nextRows,
+ [fieldName]: value,
+ highlightRowId: "",
+ saveErrorMessage: "",
+ };
+ }
+
+ function findDuplicateTimeManagementSrRow(modal, srNumber, currentRowId = "") {
+ const normalizedSrNumber = normalizeString(srNumber);
+ const normalizedCurrentRowId = cleanString(currentRowId);
+
+ if (!normalizedSrNumber || isNonServiceRequestSrNumber(srNumber)) {
+ return null;
+ }
+
+ return (
+ getTimeManagementRows(modal).find((row) => {
+ return (
+ cleanString(row?.id) !== normalizedCurrentRowId &&
+ normalizeString(row?.selectedSrNumber) === normalizedSrNumber
+ );
+ }) || null
+ );
+ }
+
+ function findExistingTimeManagementDuplicateRow(modal, type) {
+ const seen = new Map();
+
+ for (const row of getTimeManagementRows(modal)) {
+ const rowId = cleanString(row?.id);
+ let key = "";
+
+ if (type === "non-service") {
+ if (!isNonServiceRequestSrNumber(row?.selectedSrNumber)) {
+ continue;
+ }
+
+ key = normalizeString(row?.selectedTaskTypeValue);
+ } else {
+ if (isNonServiceRequestSrNumber(row?.selectedSrNumber)) {
+ continue;
+ }
+
+ key = normalizeString(row?.selectedSrNumber);
+ }
+
+ if (!key) {
+ continue;
+ }
+
+ if (seen.has(key)) {
+ return (
+ getTimeManagementRows(modal).find(
+ (candidate) => cleanString(candidate?.id) === seen.get(key)
+ ) || row
+ );
+ }
+
+ seen.set(key, rowId);
+ }
+
+ return null;
+ }
+
+ function findDuplicateTimeManagementNonServiceTaskTypeRow(
+ modal,
+ taskTypeValue,
+ currentRowId = ""
+ ) {
+ const normalizedTaskTypeValue = normalizeString(taskTypeValue);
+ const normalizedCurrentRowId = cleanString(currentRowId);
+
+ if (!normalizedTaskTypeValue) {
+ return null;
+ }
+
+ const currentRow = getTimeManagementRowById(modal, currentRowId);
+
+ if (!isNonServiceRequestSrNumber(currentRow?.selectedSrNumber)) {
+ return null;
+ }
+
+ return (
+ getTimeManagementRows(modal).find((row) => {
+ return (
+ cleanString(row?.id) !== normalizedCurrentRowId &&
+ isNonServiceRequestSrNumber(row?.selectedSrNumber) &&
+ normalizeString(row?.selectedTaskTypeValue) === normalizedTaskTypeValue
+ );
+ }) || null
+ );
+ }
+
+ function highlightTimeManagementDuplicateRow(rowId) {
+ const normalizedRowId = cleanString(rowId);
+
+ if (!appState.timeManagementModal || !normalizedRowId) {
+ return;
+ }
+
+ appState.pendingTimeManagementHighlightRowId = normalizedRowId;
+ appState.timeManagementModal = {
+ ...appState.timeManagementModal,
+ highlightRowId: normalizedRowId,
+ };
+ renderModal();
+ }
+
+ 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
+ ),
+ highlightRowId: "",
+ saveErrorMessage: "",
+ };
+ renderModal();
+ }
+
+ async function saveTimeManagementChanges() {
+ const modal = appState.timeManagementModal;
+
+ if (!modal || modal.isSaving || !isTimeManagementSaveEnabled(modal)) {
+ return;
+ }
+
+ appState.timeManagementModal = {
+ ...modal,
+ isSaving: true,
+ saveErrorMessage: "",
+ };
+ renderModal();
+
+ try {
+ const resourceId = getCurrentUserResourcePartyId();
+ const weekId = cleanString(modal.weekId);
+
+ if (!resourceId) {
+ throw new Error("resourceCurrentUser.resourcePartyId nao esta disponivel.");
+ }
+
+ if (!weekId) {
+ throw new Error("WeekId nao encontrado para gravacao do apontamento.");
+ }
+
+ const changes = getTimeManagementPendingChanges(modal);
+ const baseUrl = COMCIP_REQUEST.timeEntriesUrl;
+ const updatedPayload = changes.updatedRows.map((row) =>
+ createTimeManagementSavePayload(row, modal.weekDays, weekId, resourceId, {
+ includeId: true,
+ })
+ );
+ const newPayload = changes.newRows.map((row) =>
+ createTimeManagementSavePayload(row, modal.weekDays, weekId, resourceId)
+ );
+
+ for (const timeEntryId of changes.deletedIds) {
+ await comcipRepository.mutateComcipPayload(
+ "DELETE",
+ `${baseUrl}/${encodeURIComponent(timeEntryId)}`,
+ null,
+ "timeEntryDelete"
+ );
+ }
+
+ if (updatedPayload.length) {
+ await comcipRepository.mutateComcipPayload(
+ "PUT",
+ baseUrl,
+ updatedPayload,
+ "timeEntryUpdate"
+ );
+ }
+
+ if (newPayload.length) {
+ await comcipRepository.mutateComcipPayload(
+ "POST",
+ baseUrl,
+ newPayload,
+ "timeEntryCreate"
+ );
+ }
+
+ await loadTimeManagementData(
+ modal.selectedDate || getDateInputValue(new Date())
+ );
+ } catch (error) {
+ appState.timeManagementModal = {
+ ...(appState.timeManagementModal || modal),
+ isSaving: false,
+ saveErrorMessage: getErrorMessage(error),
+ };
+ renderModal();
+ }
+ }
+
+ 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 fetchCachedActivityRequest(srNumbers) {
+ const cacheKey = getActivityRequestCacheKey(srNumbers);
+
+ if (!cacheKey) {
+ return {
+ url: "",
+ payload: null,
+ };
+ }
+
+ const cachedRequest = appState.timeManagementActivityCache.get(cacheKey);
+
+ if (cachedRequest?.payload) {
+ return cachedRequest;
+ }
+
+ if (cachedRequest?.promise) {
+ return cachedRequest.promise;
+ }
+
+ const requestPromise = comcipRepository
+ .fetchActivityRequest(srNumbers)
+ .then((result) => {
+ appState.timeManagementActivityCache.set(cacheKey, {
+ url: result.url,
+ payload: result.payload,
+ });
+
+ return result;
+ })
+ .catch((error) => {
+ appState.timeManagementActivityCache.delete(cacheKey);
+ throw error;
+ });
+
+ appState.timeManagementActivityCache.set(cacheKey, {
+ promise: requestPromise,
+ });
+
+ return requestPromise;
+ }
+
+ function getActivityRequestCacheKey(srNumbers) {
+ const normalizedSrNumbers = (srNumbers || [])
+ .map((srNumber) => cleanString(srNumber).toUpperCase())
+ .filter((srNumber) => /^SR\d+$/i.test(srNumber))
+ .sort();
+
+ return normalizedSrNumbers.join("|");
+ }
+
+ 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 comcipRepository.fetchTaskTypePayload();
+ await writeCachedTaskTypePayload(payload);
+
+ return payload;
+ }
+
+ 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",
+ ])
+ );
+ }
+
+ return Object.freeze({
+ toggleTimeManagementDatePicker,
+ shiftTimeManagementDatePickerMonth,
+ selectTimeManagementDate,
+ shiftTimeManagementDateByWeeks,
+ openTimeManagementModal,
+ loadTimeManagementData,
+ addTimeManagementEntryRow,
+ removeTimeManagementEntryRow,
+ updateTimeManagementSelectedSr,
+ updateTimeManagementField,
+ updateTimeManagementDayHours,
+ saveTimeManagementChanges,
+ selectTimeManagementComboboxOption,
+ filterTimeManagementCombobox,
+ handleTimeManagementComboboxKeydown,
+ refreshTaskTypeCache,
+ });
+ }
+
+ globalThis.ArchPanelManageTimeController = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/content/modal-events.js b/src/content/modal-events.js
new file mode 100644
index 0000000..e444fac
--- /dev/null
+++ b/src/content/modal-events.js
@@ -0,0 +1,533 @@
+(() => {
+ function create(dependencies) {
+ const {
+ appState,
+ renderModal,
+ closeModal,
+ closeDetailModal,
+ toggleTheme,
+ toggleSidebar,
+ refreshData,
+ shiftCalendarPeriod,
+ goToCurrentCalendarPeriod,
+ setCalendarView,
+ downloadFile,
+ createExportHtml,
+ openDetailModal,
+ openActionFormModal,
+ openRampComparisonModal,
+ openForecastUpdateConfirmModal,
+ submitForecastUpdate,
+ openTimeEntriesDrawer,
+ openTimeManagementModal,
+ addTimeManagementEntryRow,
+ saveTimeManagementChanges,
+ removeTimeManagementEntryRow,
+ toggleTimeManagementDatePicker,
+ shiftTimeManagementDateByWeeks,
+ shiftTimeManagementDatePickerMonth,
+ selectTimeManagementDate,
+ selectTimeManagementComboboxOption,
+ submitActionForm,
+ toggleDetailSort,
+ loadTimeManagementData,
+ updateTimeManagementSelectedSr,
+ updateTimeManagementField,
+ updateTimeManagementDayHours,
+ filterTimeManagementCombobox,
+ handleTimeManagementComboboxKeydown,
+ } = dependencies;
+
+ function handleOverlayClick(event) {
+ const overlay = document.getElementById(
+ globalThis.ArchPanelConfig.IDS.overlay
+ );
+
+ if (!overlay) {
+ return;
+ }
+
+ if (event.target === overlay) {
+ if (appState.actionFormModal) {
+ appState.actionFormModal = null;
+ renderModal();
+ } else if (appState.timeManagementModal) {
+ appState.timeManagementModal = null;
+ renderModal();
+ } else if (appState.forecastUpdateConfirmModal) {
+ appState.forecastUpdateConfirmModal = null;
+ renderModal();
+ } else if (appState.rampComparisonModal) {
+ appState.rampComparisonModal = null;
+ renderModal();
+ } else if (appState.detailModal) {
+ closeDetailModal();
+ renderModal();
+ } else {
+ closeModal();
+ }
+
+ return;
+ }
+
+ const actionElement = event.target.closest("[data-action]");
+
+ if (!actionElement) {
+ return;
+ }
+
+ 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") {
+ closeModal();
+ return;
+ }
+
+ if (action === "toggle-theme") {
+ toggleTheme();
+ return;
+ }
+
+ if (action === "toggle-sidebar") {
+ toggleSidebar();
+ return;
+ }
+
+ if (action === "refresh") {
+ void refreshData();
+ return;
+ }
+
+ if (action === "calendar-prev") {
+ shiftCalendarPeriod(-1);
+ return;
+ }
+
+ if (action === "calendar-next") {
+ shiftCalendarPeriod(1);
+ return;
+ }
+
+ if (action === "calendar-today") {
+ goToCurrentCalendarPeriod();
+ return;
+ }
+
+ if (action === "calendar-view") {
+ const calendarView = actionElement.getAttribute("data-calendar-view");
+
+ if (calendarView) {
+ setCalendarView(calendarView);
+ }
+ return;
+ }
+
+ if (action === "download-json") {
+ if (appState.dataset) {
+ downloadFile(
+ "arch-panel-data.json",
+ JSON.stringify(appState.dataset.exportPayload, null, 2),
+ "application/json"
+ );
+ }
+
+ return;
+ }
+
+ if (action === "download-html") {
+ if (appState.dataset) {
+ downloadFile(
+ "arch-panel-dashboard.html",
+ createExportHtml(appState.dataset),
+ "text/html"
+ );
+ }
+
+ return;
+ }
+
+ if (action === "open-detail") {
+ const detailScope = actionElement.getAttribute("data-detail-scope");
+ const detailValue = actionElement.getAttribute("data-detail-value");
+
+ openDetailModal(detailScope, detailValue);
+ return;
+ }
+
+ if (action === "open-action-form") {
+ const workloadId = actionElement.getAttribute("data-workload-id");
+
+ if (workloadId) {
+ openActionFormModal(workloadId);
+ }
+
+ return;
+ }
+
+ if (action === "open-ramp-comparison") {
+ const workloadId = actionElement.getAttribute("data-workload-id");
+
+ if (workloadId) {
+ openRampComparisonModal(workloadId);
+ }
+
+ return;
+ }
+
+ if (action === "close-ramp-comparison") {
+ appState.forecastUpdateConfirmModal = null;
+ appState.rampComparisonModal = null;
+ renderModal();
+ return;
+ }
+
+ if (action === "open-forecast-update-confirm") {
+ openForecastUpdateConfirmModal();
+ return;
+ }
+
+ if (action === "close-forecast-update-confirm") {
+ appState.forecastUpdateConfirmModal = null;
+ renderModal();
+ return;
+ }
+
+ if (action === "submit-forecast-update") {
+ void submitForecastUpdate();
+ return;
+ }
+
+ if (action === "close-action-form") {
+ appState.actionFormModal = null;
+ renderModal();
+ return;
+ }
+
+ if (action === "open-time-entries") {
+ const srNumber = actionElement.getAttribute("data-sr-number");
+
+ if (srNumber) {
+ void openTimeEntriesDrawer(srNumber);
+ }
+
+ return;
+ }
+
+ if (action === "close-time-entries") {
+ appState.timeEntriesDrawer = null;
+ renderModal();
+ return;
+ }
+
+ if (action === "retry-time-entries") {
+ const srNumber = appState.timeEntriesDrawer?.srNumber;
+
+ if (srNumber) {
+ void openTimeEntriesDrawer(srNumber);
+ }
+
+ 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-save") {
+ void saveTimeManagementChanges();
+ 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");
+
+ if (form instanceof HTMLFormElement) {
+ void submitActionForm(form);
+ }
+
+ return;
+ }
+
+ if (action === "close-detail") {
+ closeDetailModal();
+ renderModal();
+ return;
+ }
+
+ if (action === "sort-detail") {
+ const sortKey = actionElement.getAttribute("data-sort-key");
+
+ if (sortKey) {
+ toggleDetailSort(sortKey);
+ }
+ }
+ }
+
+ 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"]')
+ );
+ }
+
+ return Object.freeze({
+ handleOverlayClick,
+ handleOverlayChange,
+ handleOverlayInput,
+ handleOverlayKeydown,
+ });
+ }
+
+ globalThis.ArchPanelModalEvents = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/content/shell-view.js b/src/content/shell-view.js
new file mode 100644
index 0000000..33e60d3
--- /dev/null
+++ b/src/content/shell-view.js
@@ -0,0 +1,328 @@
+(() => {
+ function create({
+ IDS,
+ CLASSES,
+ USER_BUTTON_LABEL,
+ normalizeText,
+ isVisible,
+ }) {
+ function applyDialogTheme(overlay, theme) {
+ const dialog = overlay?.querySelector(`#${IDS.dialog}`);
+
+ if (!dialog) {
+ return;
+ }
+
+ dialog.setAttribute("data-theme", theme);
+ }
+
+ function ensureStyles() {
+ // Styles are loaded by manifest.json. This function remains as a stable callsite.
+ }
+
+ function applyTheme(header, functionsContainer, anchor, mount, overlay) {
+ const headerStyle = window.getComputedStyle(header);
+ const functionsStyle = window.getComputedStyle(functionsContainer);
+ const sourceBackground = resolveBackgroundColor(header, headerStyle);
+ const sourceForeground = resolveForegroundColor(
+ header,
+ functionsContainer,
+ anchor,
+ headerStyle,
+ functionsStyle
+ );
+ const workbenchHeader = mix(
+ sourceBackground,
+ { r: 75, g: 51, b: 93, a: 1 },
+ 0.52
+ );
+ const workbenchAccent = mix(
+ workbenchHeader,
+ { r: 108, g: 77, b: 128, a: 1 },
+ 0.38
+ );
+ const focusColor = mix(sourceForeground, workbenchHeader, 0.28);
+ const themeTargets = [mount, overlay].filter(Boolean);
+
+ for (const target of themeTargets) {
+ target.style.setProperty(
+ "--arch-panel-bg",
+ toColor(mix(sourceBackground, sourceForeground, 0.12))
+ );
+ target.style.setProperty(
+ "--arch-panel-bg-hover",
+ toColor(mix(sourceBackground, sourceForeground, 0.22))
+ );
+ target.style.setProperty(
+ "--arch-panel-border",
+ toColor(mix(sourceBackground, sourceForeground, 0.32))
+ );
+ target.style.setProperty("--arch-panel-fg", toColor(sourceForeground, 1));
+ target.style.setProperty("--arch-panel-focus", toColor(focusColor, 0.94));
+ target.style.setProperty(
+ "--arch-panel-font",
+ functionsStyle.fontFamily || headerStyle.fontFamily || "inherit"
+ );
+ target.style.setProperty(
+ "--arch-workbench-header",
+ toColor(workbenchHeader, 1)
+ );
+ target.style.setProperty(
+ "--arch-workbench-accent",
+ toColor(workbenchAccent, 1)
+ );
+ }
+ }
+
+ function resolveForegroundColor(
+ header,
+ functionsContainer,
+ anchor,
+ headerStyle,
+ functionsStyle
+ ) {
+ const salesPlanningElement = findTextElement(header, "Sales Planning");
+ const salesPlanningColor = salesPlanningElement
+ ? parseColor(window.getComputedStyle(salesPlanningElement).color)
+ : null;
+ const functionsItemColor = findVisibleChildColor(functionsContainer);
+ const userColor = anchor
+ ? parseColor(window.getComputedStyle(anchor).color)
+ : null;
+
+ return (
+ userColor ||
+ functionsItemColor ||
+ parseColor(functionsStyle.color) ||
+ salesPlanningColor ||
+ parseColor(headerStyle.color) || {
+ r: 255,
+ g: 255,
+ b: 255,
+ a: 1,
+ }
+ );
+ }
+
+ function resolveBackgroundColor(element, style) {
+ const directBackground = parseColor(style.backgroundColor);
+
+ if (directBackground && directBackground.a > 0) {
+ return directBackground;
+ }
+
+ let current = element.parentElement;
+
+ while (current) {
+ const currentBackground = parseColor(
+ window.getComputedStyle(current).backgroundColor
+ );
+
+ if (currentBackground && currentBackground.a > 0) {
+ return currentBackground;
+ }
+
+ current = current.parentElement;
+ }
+
+ return { r: 75, g: 51, b: 93, a: 1 };
+ }
+
+ function parseColor(value) {
+ if (!value || value === "transparent") {
+ return null;
+ }
+
+ const rgbMatch = value.match(
+ /rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})(?:[\s,/]+([.\d]+))?\s*\)/i
+ );
+
+ if (rgbMatch) {
+ return {
+ r: Number.parseInt(rgbMatch[1], 10),
+ g: Number.parseInt(rgbMatch[2], 10),
+ b: Number.parseInt(rgbMatch[3], 10),
+ a: rgbMatch[4] === undefined ? 1 : Number.parseFloat(rgbMatch[4]),
+ };
+ }
+
+ const hexMatch = value.match(/^#([\da-f]{3,8})$/i);
+
+ if (!hexMatch) {
+ return null;
+ }
+
+ const hexValue = hexMatch[1];
+
+ if (hexValue.length === 3 || hexValue.length === 4) {
+ const [r, g, b, a = "f"] = hexValue.split("");
+
+ return {
+ r: Number.parseInt(r + r, 16),
+ g: Number.parseInt(g + g, 16),
+ b: Number.parseInt(b + b, 16),
+ a: Number.parseInt(a + a, 16) / 255,
+ };
+ }
+
+ if (hexValue.length === 6 || hexValue.length === 8) {
+ return {
+ r: Number.parseInt(hexValue.slice(0, 2), 16),
+ g: Number.parseInt(hexValue.slice(2, 4), 16),
+ b: Number.parseInt(hexValue.slice(4, 6), 16),
+ a:
+ hexValue.length === 8
+ ? Number.parseInt(hexValue.slice(6, 8), 16) / 255
+ : 1,
+ };
+ }
+
+ return null;
+ }
+
+ function mix(base, tint, amount) {
+ const ratio = clamp(amount, 0, 1);
+
+ return {
+ r: Math.round(base.r + (tint.r - base.r) * ratio),
+ g: Math.round(base.g + (tint.g - base.g) * ratio),
+ b: Math.round(base.b + (tint.b - base.b) * ratio),
+ a: base.a + (tint.a - base.a) * ratio,
+ };
+ }
+
+ function toColor(color, alphaOverride) {
+ const alpha = alphaOverride ?? color.a ?? 1;
+
+ return `rgba(${color.r}, ${color.g}, ${color.b}, ${clamp(alpha, 0, 1)})`;
+ }
+
+ function clamp(value, min, max) {
+ return Math.min(Math.max(value, min), max);
+ }
+
+ function findUserAnchor(container) {
+ const selector = [
+ `button[label="${USER_BUTTON_LABEL}"]`,
+ `[role="button"][label="${USER_BUTTON_LABEL}"]`,
+ `oj-button[label="${USER_BUTTON_LABEL}"]`,
+ `button[aria-label="${USER_BUTTON_LABEL}"]`,
+ `[role="button"][aria-label="${USER_BUTTON_LABEL}"]`,
+ `[label="${USER_BUTTON_LABEL}"]`,
+ `[aria-label="${USER_BUTTON_LABEL}"]`,
+ ].join(", ");
+
+ const directMatch = container.querySelector(selector);
+
+ if (directMatch && isVisible(directMatch)) {
+ return directMatch;
+ }
+
+ const fallbackMatch = Array.from(container.querySelectorAll("*")).find(
+ (element) => {
+ if (!isVisible(element) || element.closest(`#${IDS.mount}`)) {
+ return false;
+ }
+
+ return (
+ element.getAttribute("label") === USER_BUTTON_LABEL ||
+ element.getAttribute("aria-label") === USER_BUTTON_LABEL
+ );
+ }
+ );
+
+ return fallbackMatch || null;
+ }
+
+ function findTextElement(container, expectedText) {
+ const normalizedExpected = normalizeText(expectedText);
+ const elements = Array.from(container.querySelectorAll("*"));
+
+ return (
+ elements.find((element) => {
+ const text = normalizeText(element.textContent || "");
+
+ return text && text.includes(normalizedExpected);
+ }) || null
+ );
+ }
+
+ function resolveInsertionTarget(container, anchor, mount) {
+ const directAnchor = anchor ? getDirectChild(anchor, container) : null;
+ const fallbackTarget = findFirstInsertionTarget(container, mount);
+ const candidate = directAnchor || fallbackTarget;
+
+ if (!candidate) {
+ return null;
+ }
+
+ if (candidate === mount) {
+ return findNextValidSibling(container, mount);
+ }
+
+ return candidate.parentElement === container ? candidate : null;
+ }
+
+ function getDirectChild(element, container) {
+ let current = element;
+
+ while (current && current.parentElement !== container) {
+ current = current.parentElement;
+ }
+
+ return current && current.parentElement === container ? current : null;
+ }
+
+ function findFirstInsertionTarget(container, mount) {
+ return Array.from(container.children).find((child) => child !== mount) || null;
+ }
+
+ function findNextValidSibling(container, mount) {
+ let sibling = mount.nextElementSibling;
+
+ while (sibling) {
+ if (sibling.parentElement === container) {
+ return sibling;
+ }
+
+ sibling = sibling.nextElementSibling;
+ }
+
+ return null;
+ }
+
+ function findVisibleChildColor(container) {
+ const visibleChild = Array.from(container.querySelectorAll("*")).find(
+ (element) => isVisible(element) && !element.closest(`#${IDS.mount}`)
+ );
+
+ return visibleChild
+ ? parseColor(window.getComputedStyle(visibleChild).color)
+ : null;
+ }
+
+ function lockScroll() {
+ document.documentElement.classList.add(CLASSES.modalOpen);
+ document.body?.classList.add(CLASSES.modalOpen);
+ }
+
+ function unlockScroll() {
+ document.documentElement.classList.remove(CLASSES.modalOpen);
+ document.body?.classList.remove(CLASSES.modalOpen);
+ }
+
+ return Object.freeze({
+ applyDialogTheme,
+ ensureStyles,
+ applyTheme,
+ findUserAnchor,
+ resolveInsertionTarget,
+ lockScroll,
+ unlockScroll,
+ });
+ }
+
+ globalThis.ArchPanelShellView = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/content/styles.css b/src/content/styles.css
new file mode 100644
index 0000000..fc5c255
--- /dev/null
+++ b/src/content/styles.css
@@ -0,0 +1,2530 @@
+html.arch-panel-extension-modal-open,
+body.arch-panel-extension-modal-open {
+ overflow: hidden !important;
+}
+
+.arch-panel-extension-entry {
+ --arch-panel-bg: rgba(255, 255, 255, 0.12);
+ --arch-panel-bg-hover: rgba(255, 255, 255, 0.2);
+ --arch-panel-border: rgba(255, 255, 255, 0.26);
+ --arch-panel-fg: #ffffff;
+ --arch-panel-focus: rgba(255, 255, 255, 0.72);
+ --arch-panel-font: inherit;
+ --arch-workbench-header: #4b335d;
+ --arch-workbench-accent: #6c4d80;
+ display: inline-flex;
+ align-items: center;
+ margin-inline-end: 12px;
+ flex: 0 0 auto;
+}
+
+.arch-panel-extension-button {
+ appearance: none;
+ border: 1px solid var(--arch-panel-border);
+ background: var(--arch-panel-bg);
+ color: var(--arch-panel-fg);
+ border-radius: 999px;
+ min-height: 36px;
+ padding: 0 14px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-family: var(--arch-panel-font);
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1;
+ letter-spacing: 0.01em;
+ cursor: pointer;
+ transition:
+ background-color 160ms ease,
+ border-color 160ms ease,
+ transform 160ms ease;
+}
+
+.arch-panel-extension-button:hover {
+ background: var(--arch-panel-bg-hover);
+}
+
+.arch-panel-extension-button:active {
+ transform: translateY(1px);
+}
+
+.arch-panel-extension-button:focus-visible,
+.arch-panel-extension-close:focus-visible,
+.arch-panel-extension-theme-toggle:focus-visible,
+.arch-panel-extension-sidebar-button:focus-visible,
+.arch-panel-extension-utility-button:focus-visible,
+.arch-panel-extension-summary-card-button:focus-visible,
+.arch-panel-extension-sort-button:focus-visible,
+.arch-panel-extension-indicator-card:focus-visible,
+.arch-panel-extension-row-link:focus-visible {
+ outline: 2px solid var(--arch-panel-focus);
+ outline-offset: 2px;
+}
+
+#arch-panel-extension-hcm-tile {
+ cursor: pointer;
+}
+
+#arch-panel-extension-hcm-tile:focus-visible {
+ outline: 2px solid #0b5cab;
+ outline-offset: 3px;
+}
+
+.arch-panel-extension-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 2147483647;
+ display: flex;
+ opacity: 0;
+ pointer-events: none;
+ background: rgba(13, 17, 24, 0.56);
+ backdrop-filter: blur(8px);
+ transition: opacity 160ms ease;
+}
+
+.arch-panel-extension-overlay.is-open {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.arch-panel-extension-dialog {
+ width: 100vw;
+ height: 100vh;
+ display: grid;
+ grid-template-rows: auto 1fr;
+ --wb-app-bg: #f5f4f3;
+ --wb-panel: #ffffff;
+ --wb-panel-soft: #fafafa;
+ --wb-sidebar: #f3f2f1;
+ --wb-border: #d9d9d9;
+ --wb-border-strong: #c8c8c8;
+ --wb-row-divider: #e6e6e6;
+ --wb-divider: #ececec;
+ --wb-row-hover: #f9fbfc;
+ --wb-text: #1f1f1f;
+ --wb-text-secondary: #5f6670;
+ --wb-text-muted: #7b8189;
+ --wb-link: #006b9a;
+ --wb-info: #dceef7;
+ --wb-success: #3fa35c;
+ --wb-warning: #d94f2b;
+ --wb-shadow: 0 1px 2px rgba(31, 31, 31, 0.04);
+ --arch-workbench-header-fallback: #3f2d4f;
+ --arch-workbench-accent-fallback: #684b78;
+ --arch-shell-header-text: #ffffff;
+ --arch-shell-header-muted: rgba(255, 255, 255, 0.82);
+ --arch-shell-header-subtle: rgba(255, 255, 255, 0.68);
+ background: var(--wb-app-bg);
+ color: var(--wb-text);
+ font-family: "Oracle Sans", "Segoe UI", Arial, sans-serif;
+}
+
+.arch-panel-extension-dialog[data-theme="dark"] {
+ --wb-app-bg: #141b24;
+ --wb-panel: #1f2833;
+ --wb-panel-soft: #273240;
+ --wb-sidebar: #19222c;
+ --wb-border: #364150;
+ --wb-border-strong: #566171;
+ --wb-row-divider: #333d4b;
+ --wb-divider: #303a46;
+ --wb-row-hover: #273240;
+ --wb-text: #f4f7fb;
+ --wb-text-secondary: #c6d0da;
+ --wb-text-muted: #9ca8b5;
+ --wb-link: #84c7e3;
+ --wb-info: #233847;
+ --wb-success: #5ec280;
+ --wb-warning: #f0946c;
+ --wb-shadow: none;
+ --arch-workbench-header-fallback: #202a36;
+ --arch-workbench-accent-fallback: #39495b;
+}
+
+.arch-panel-extension-dialog * {
+ box-sizing: border-box;
+}
+
+.arch-panel-extension-shell-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+ min-height: 64px;
+ padding: 12px 24px;
+ background:
+ linear-gradient(
+ 135deg,
+ var(--arch-workbench-header, var(--arch-workbench-header-fallback)) 0%,
+ var(--arch-workbench-accent, var(--arch-workbench-accent-fallback)) 100%
+ );
+ border-bottom: 1px solid rgba(255, 255, 255, 0.16);
+ box-shadow: 0 8px 24px rgba(31, 31, 31, 0.16);
+ color: var(--arch-shell-header-text);
+}
+
+.arch-panel-extension-brand {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ min-width: 0;
+}
+
+.arch-panel-extension-brand-mark {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 40px;
+ height: 40px;
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.18);
+ color: var(--arch-shell-header-text);
+ font-size: 14px;
+ font-weight: 700;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
+}
+
+.arch-panel-extension-brand-label {
+ margin: 0 0 2px;
+ color: var(--arch-shell-header-muted);
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.arch-panel-extension-title {
+ margin: 0;
+ color: var(--arch-shell-header-text);
+ font-size: 24px;
+ line-height: 1.2;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+}
+
+.arch-panel-extension-shell-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex: 0 0 auto;
+}
+
+.arch-panel-extension-shell-note {
+ max-width: 36vw;
+ overflow: hidden;
+ color: var(--arch-shell-header-muted);
+ font-size: 12px;
+ line-height: 1.35;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.arch-panel-extension-theme-toggle {
+ appearance: none;
+ min-height: 34px;
+ padding: 0 14px;
+ border: 1px solid rgba(255, 255, 255, 0.28);
+ border-radius: 4px;
+ background: rgba(255, 255, 255, 0.12);
+ color: var(--arch-shell-header-text);
+ font: inherit;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease;
+}
+
+.arch-panel-extension-shell-header .arch-panel-extension-theme-toggle:hover {
+ border-color: rgba(255, 255, 255, 0.44);
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.arch-panel-extension-close {
+ appearance: none;
+ min-height: 34px;
+ padding: 0 14px;
+ border: 1px solid var(--wb-border);
+ border-radius: 4px;
+ background: var(--wb-panel-soft);
+ color: var(--wb-text);
+ font: inherit;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: none;
+}
+
+.arch-panel-extension-close:hover {
+ border-color: var(--wb-border-strong);
+ background: var(--wb-row-hover);
+}
+
+.arch-panel-extension-shell-header .arch-panel-extension-close {
+ border-color: rgba(255, 255, 255, 0.28);
+ background: rgba(255, 255, 255, 0.12);
+ color: var(--arch-shell-header-text);
+}
+
+.arch-panel-extension-shell-header .arch-panel-extension-close:hover {
+ border-color: rgba(255, 255, 255, 0.42);
+ background: rgba(255, 255, 255, 0.16);
+}
+
+.arch-panel-extension-detail-dialog .arch-panel-extension-close {
+ border-color: var(--wb-border);
+ background: var(--wb-panel-soft);
+ color: var(--wb-text);
+ box-shadow: none;
+}
+
+.arch-panel-extension-sidebar-button,
+.arch-panel-extension-utility-button {
+ appearance: none;
+ min-height: 34px;
+ padding: 0 14px;
+ border: 1px solid var(--wb-border);
+ border-radius: 4px;
+ background: var(--wb-panel);
+ color: var(--wb-text);
+ font: inherit;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.arch-panel-extension-utility-button.is-primary {
+ border-color: #0b5cab;
+ background: #0b5cab;
+ color: #ffffff;
+ text-shadow: 0 1px 0 rgba(0, 0, 0, 0.16);
+ box-shadow: 0 8px 18px rgba(11, 92, 171, 0.24);
+}
+
+.arch-panel-extension-utility-button.is-primary:hover {
+ border-color: #084a8a;
+ background: #084a8a;
+}
+
+.arch-panel-extension-utility-button.is-primary:disabled {
+ border-color: var(--wb-border);
+ background: var(--wb-panel-soft);
+ color: var(--wb-text-muted);
+ box-shadow: none;
+ cursor: not-allowed;
+}
+
+.arch-panel-extension-utility-button.is-outline {
+ border-color: #0b5cab;
+ background: transparent;
+ color: #0b5cab;
+}
+
+.arch-panel-extension-utility-button.is-outline:hover:not(:disabled) {
+ background: rgba(11, 92, 171, 0.08);
+}
+
+.arch-panel-extension-utility-button.is-outline:disabled {
+ border-color: var(--wb-border);
+ background: transparent;
+ color: var(--wb-text-muted);
+ cursor: not-allowed;
+ opacity: 0.62;
+}
+
+.arch-panel-extension-shell-body {
+ display: grid;
+ grid-template-columns: 260px minmax(0, 1fr);
+ min-height: 0;
+ position: relative;
+}
+
+.arch-panel-extension-shell-body.is-sidebar-collapsed {
+ grid-template-columns: minmax(0, 1fr);
+}
+
+.arch-panel-extension-sidebar {
+ background: var(--wb-sidebar);
+ border-right: 1px solid var(--wb-border);
+ padding: 20px 14px;
+}
+
+.arch-panel-extension-sidebar.is-collapsed {
+ display: none;
+}
+
+.arch-panel-extension-sidebar-group + .arch-panel-extension-sidebar-group {
+ margin-top: 18px;
+ padding-top: 18px;
+ border-top: 1px solid var(--wb-border);
+}
+
+.arch-panel-extension-sidebar-label,
+.arch-panel-extension-page-eyebrow,
+.arch-panel-extension-panel-label,
+.arch-panel-extension-state-eyebrow {
+ margin: 0 0 8px;
+ color: var(--wb-text-muted);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.arch-panel-extension-sidebar-data {
+ display: grid;
+ gap: 6px;
+ color: var(--wb-text-secondary);
+ font-size: 12px;
+}
+
+.arch-panel-extension-sidebar-data strong {
+ color: var(--wb-text);
+ font-size: 13px;
+}
+
+.arch-panel-extension-current-user-card {
+ display: grid;
+ justify-items: start;
+ gap: 10px;
+}
+
+.arch-panel-extension-current-user-avatar {
+ width: 48px;
+ height: 48px;
+ border: 1px solid var(--wb-border);
+ border-radius: 999px;
+ background: var(--wb-panel);
+ box-shadow: var(--wb-shadow);
+ color: var(--wb-text);
+ object-fit: cover;
+}
+
+.arch-panel-extension-current-user-avatar.is-fallback {
+ display: inline-grid;
+ place-items: center;
+ background: var(--arch-workbench-header, var(--arch-workbench-header-fallback));
+ color: #ffffff;
+ font-size: 15px;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+}
+
+.arch-panel-extension-sidebar-button {
+ width: 100%;
+ justify-content: center;
+ display: inline-flex;
+ align-items: center;
+ border-color: var(--wb-border);
+ background: var(--wb-panel);
+ color: var(--wb-text);
+}
+
+.arch-panel-extension-sidebar-button + .arch-panel-extension-sidebar-button {
+ margin-top: 8px;
+}
+
+.arch-panel-extension-main-shell {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ overflow: auto;
+ overflow-anchor: none;
+ background: var(--wb-app-bg);
+ padding: 20px 24px 24px;
+}
+
+.arch-panel-extension-main-shell:has(.arch-panel-extension-state-card) {
+ grid-column: 1 / -1;
+ align-items: stretch;
+ justify-content: center;
+ min-height: min(720px, calc(100vh - 120px));
+}
+
+.arch-panel-extension-page-hero {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 24px;
+ margin-bottom: 18px;
+}
+
+.arch-panel-extension-page-title,
+.arch-panel-extension-state-title {
+ margin: 0;
+ color: var(--wb-text);
+ font-size: 34px;
+ line-height: 1.15;
+ font-weight: 700;
+}
+
+.arch-panel-extension-page-subtitle,
+.arch-panel-extension-state-copy {
+ margin: 10px 0 0;
+ max-width: 760px;
+ color: var(--wb-text-secondary);
+ font-size: 13px;
+ line-height: 1.55;
+}
+
+.arch-panel-extension-hero-aside {
+ display: grid;
+ gap: 8px;
+ justify-items: end;
+}
+
+.arch-panel-extension-hero-pill {
+ display: inline-flex;
+ align-items: center;
+ min-height: 28px;
+ padding: 0 10px;
+ border: 1px solid var(--wb-border);
+ border-radius: 14px;
+ background: var(--wb-info);
+ color: var(--wb-text);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.arch-panel-extension-hero-note {
+ color: var(--wb-text-secondary);
+ font-size: 12px;
+}
+
+.arch-panel-extension-summary-row,
+.arch-panel-extension-indicator-grid,
+.arch-panel-extension-status-grid,
+.arch-panel-extension-content-grid,
+.arch-panel-extension-boolean-grid {
+ display: grid;
+ gap: 14px;
+}
+
+.arch-panel-extension-summary-row {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ margin-bottom: 16px;
+}
+
+.arch-panel-extension-indicator-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ margin-bottom: 16px;
+}
+
+.arch-panel-extension-status-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ margin-bottom: 16px;
+}
+
+.arch-panel-extension-content-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.arch-panel-extension-summary-card,
+.arch-panel-extension-panel,
+.arch-panel-extension-indicator-card,
+.arch-panel-extension-state-card {
+ border: 1px solid var(--wb-border);
+ border-radius: 8px;
+ background: var(--wb-panel);
+ box-shadow: var(--wb-shadow);
+}
+
+.arch-panel-extension-summary-card,
+.arch-panel-extension-state-card,
+.arch-panel-extension-indicator-card {
+ padding: 16px;
+}
+
+.arch-panel-extension-state-card {
+ width: 100%;
+ min-height: 320px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 34px 40px;
+}
+
+.arch-panel-extension-loading-progress {
+ display: grid;
+ gap: 10px;
+ width: 100%;
+ margin-top: 30px;
+}
+
+.arch-panel-extension-loading-progress-meta {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 16px;
+ color: var(--wb-text-secondary);
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.arch-panel-extension-loading-progress-track {
+ position: relative;
+ height: 30px;
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, var(--wb-accent) 50%, var(--wb-border-strong));
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--wb-panel-soft) 68%, #000000);
+}
+
+.arch-panel-extension-loading-progress-fill {
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 0;
+ min-width: 0;
+ border-radius: inherit;
+ background: linear-gradient(90deg, #1f7a4d, #2fbf71);
+ transition: width 520ms ease;
+}
+
+.arch-panel-extension-loading-progress-value {
+ position: absolute;
+ inset: 0 auto 0 50%;
+ transform: translateX(-50%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 54px;
+ margin: 4px 0;
+ padding: 0 8px;
+ border: 1px solid rgba(255, 255, 255, 0.26);
+ border-radius: 999px;
+ background: rgba(7, 12, 18, 0.72);
+ color: #ffffff;
+ font-size: 12px;
+ font-weight: 800;
+ line-height: 1;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
+ text-shadow: none;
+ pointer-events: none;
+}
+
+.arch-panel-extension-summary-card-button {
+ appearance: none;
+ width: 100%;
+ text-align: left;
+ color: inherit;
+ font: inherit;
+ line-height: inherit;
+ cursor: pointer;
+}
+
+.arch-panel-extension-panel {
+ padding: 16px;
+}
+
+.arch-panel-extension-summary-label,
+.arch-panel-extension-summary-detail,
+.arch-panel-extension-indicator-label,
+.arch-panel-extension-indicator-subtitle,
+.arch-panel-extension-indicator-total,
+.arch-panel-extension-insight-detail,
+.arch-panel-extension-stage-detail {
+ margin: 0;
+}
+
+.arch-panel-extension-summary-label,
+.arch-panel-extension-indicator-label {
+ color: var(--wb-text-secondary);
+ font-size: 12px;
+}
+
+.arch-panel-extension-summary-value,
+.arch-panel-extension-indicator-value {
+ margin: 8px 0 4px;
+ color: var(--wb-text);
+ font-size: 26px;
+ line-height: 1.2;
+ font-weight: 700;
+}
+
+.arch-panel-extension-summary-detail,
+.arch-panel-extension-indicator-subtitle,
+.arch-panel-extension-indicator-total {
+ color: var(--wb-text-muted);
+ font-size: 11px;
+}
+
+.arch-panel-extension-indicator-card {
+ appearance: none;
+ text-align: left;
+ color: inherit;
+ cursor: pointer;
+}
+
+.arch-panel-extension-boolean-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ margin-top: 12px;
+}
+
+.arch-panel-extension-progress {
+ display: grid;
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+.arch-panel-extension-progress-track {
+ display: flex;
+ width: 100%;
+ height: 10px;
+ overflow: hidden;
+ border-radius: 999px;
+ background: var(--wb-panel-soft);
+ border: 1px solid var(--wb-border);
+}
+
+.arch-panel-extension-progress-segment {
+ height: 100%;
+}
+
+.arch-panel-extension-progress-segment.is-success {
+ background: linear-gradient(90deg, #7fd29a, #3fa35c);
+}
+
+.arch-panel-extension-progress-segment.is-danger {
+ background: linear-gradient(90deg, #f2a28d, #d94f2b);
+}
+
+.arch-panel-extension-progress-labels {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ color: var(--wb-text-secondary);
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.arch-panel-extension-panel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ margin-bottom: 14px;
+}
+
+.arch-panel-extension-panel-title {
+ margin: 0;
+ color: var(--wb-text);
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.arch-panel-extension-panel-meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--wb-text-muted);
+ font-size: 12px;
+}
+
+.arch-panel-extension-operations-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 14px;
+}
+
+.arch-panel-extension-sr-hour-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 14px;
+ align-content: start;
+}
+
+.arch-panel-extension-calendar-card {
+ min-width: 0;
+}
+
+.arch-panel-extension-calendar-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--wb-text-muted);
+ font-size: 12px;
+}
+
+.arch-panel-extension-calendar-view-toggle {
+ display: inline-flex;
+ align-items: center;
+ overflow: hidden;
+ border: 1px solid var(--wb-border);
+ border-radius: 4px;
+ background: var(--wb-panel-soft);
+}
+
+.arch-panel-extension-calendar-view-button {
+ appearance: none;
+ min-height: 28px;
+ padding: 0 9px;
+ border: 0;
+ border-right: 1px solid var(--wb-border);
+ background: transparent;
+ color: var(--wb-text-secondary);
+ font: inherit;
+ font-size: 12px;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.arch-panel-extension-calendar-view-button:last-child {
+ border-right: 0;
+}
+
+.arch-panel-extension-calendar-view-button:hover {
+ background: var(--wb-row-hover);
+ color: var(--wb-text);
+}
+
+.arch-panel-extension-calendar-view-button.is-active {
+ background: #0b5cab;
+ color: #ffffff;
+}
+
+.arch-panel-extension-calendar-nav {
+ appearance: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ border: 1px solid var(--wb-border);
+ border-radius: 4px;
+ background: var(--wb-panel-soft);
+ color: var(--wb-text);
+ font-size: 18px;
+ line-height: 1;
+ cursor: pointer;
+}
+
+.arch-panel-extension-calendar-nav:hover {
+ border-color: var(--wb-accent);
+ color: var(--wb-accent);
+}
+
+.arch-panel-extension-calendar-today {
+ appearance: none;
+ min-height: 28px;
+ padding: 0 10px;
+ border: 1px solid var(--wb-border);
+ border-radius: 4px;
+ background: var(--wb-panel-soft);
+ color: var(--wb-text);
+ font: inherit;
+ font-size: 12px;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.arch-panel-extension-calendar-today:hover {
+ border-color: var(--wb-accent);
+ color: var(--wb-accent);
+}
+
+.arch-panel-extension-calendar-weekdays,
+.arch-panel-extension-calendar-grid {
+ display: grid;
+ grid-template-columns: repeat(7, minmax(0, 1fr));
+}
+
+.arch-panel-extension-calendar-weekdays {
+ gap: 6px;
+ margin-bottom: 6px;
+ color: var(--wb-text-muted);
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.arch-panel-extension-calendar-weekdays span {
+ padding: 0 6px;
+}
+
+.arch-panel-extension-calendar-grid {
+ gap: 6px;
+}
+
+.arch-panel-extension-calendar-grid.is-week {
+ min-height: 300px;
+}
+
+.arch-panel-extension-calendar-day {
+ appearance: none;
+ position: relative;
+ display: block;
+ width: 100%;
+ text-align: left;
+ min-height: 74px;
+ overflow: hidden;
+ border: 1px solid var(--wb-border);
+ border-radius: 6px;
+ background: var(--wb-panel-soft);
+ padding: 7px;
+ font: inherit;
+ cursor: pointer;
+}
+
+.arch-panel-extension-calendar-day.is-week-day {
+ display: flex;
+ flex-direction: column;
+ min-height: 300px;
+ padding: 0;
+ overflow: hidden;
+}
+
+.arch-panel-extension-calendar-week-day-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ min-height: 38px;
+ padding: 8px 9px;
+ border-bottom: 1px solid var(--wb-border);
+ background: color-mix(in srgb, var(--wb-panel) 72%, var(--wb-panel-soft));
+}
+
+.arch-panel-extension-calendar-day.is-week-day .arch-panel-extension-calendar-week-number {
+ position: static;
+ font-size: 9px;
+ opacity: 0.76;
+}
+
+.arch-panel-extension-calendar-week-workloads {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ gap: 7px;
+ padding: 8px;
+ overflow: auto;
+}
+
+.arch-panel-extension-calendar-week-workload {
+ display: block;
+ border-left: 3px solid #d94f2b;
+ border-radius: 5px;
+ background: rgba(217, 79, 43, 0.14);
+ padding: 7px 8px;
+ color: var(--wb-text);
+ text-align: left;
+}
+
+.arch-panel-extension-calendar-week-workload-title,
+.arch-panel-extension-calendar-week-workload-meta {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.arch-panel-extension-calendar-week-workload-title {
+ font-size: 11px;
+ font-weight: 700;
+ line-height: 1.25;
+}
+
+.arch-panel-extension-calendar-week-workload-meta {
+ margin-top: 3px;
+ color: var(--wb-text-secondary);
+ font-size: 10px;
+ line-height: 1.2;
+ white-space: nowrap;
+}
+
+.arch-panel-extension-calendar-week-empty {
+ margin: auto;
+ color: var(--wb-text-muted);
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.arch-panel-extension-calendar-day:disabled {
+ cursor: default;
+}
+
+.arch-panel-extension-calendar-day.has-workload:hover {
+ border-color: #f26c4f;
+ background: rgba(217, 79, 43, 0.22);
+}
+
+.arch-panel-extension-calendar-day.is-muted {
+ opacity: 0.42;
+}
+
+.arch-panel-extension-calendar-day.is-today {
+ border-color: var(--wb-accent);
+ box-shadow: inset 0 0 0 1px var(--wb-accent);
+}
+
+.arch-panel-extension-calendar-day.has-workload {
+ border-color: rgba(217, 79, 43, 0.82);
+ background: rgba(217, 79, 43, 0.14);
+}
+
+.arch-panel-extension-calendar-date {
+ color: var(--wb-text);
+ font-size: 12px;
+ font-weight: 800;
+}
+
+.arch-panel-extension-calendar-week-number {
+ position: absolute;
+ right: 7px;
+ bottom: 6px;
+ color: var(--wb-text-muted);
+ font-size: 9px;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+ opacity: 0.72;
+ pointer-events: none;
+}
+
+.arch-panel-extension-calendar-workload {
+ display: block;
+ margin-top: 8px;
+ color: var(--wb-text);
+ font-size: 11px;
+ line-height: 1.25;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+.arch-panel-extension-calendar-count {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 20px;
+ height: 20px;
+ padding: 0 6px;
+ border-radius: 999px;
+ background: #d94f2b;
+ color: #ffffff;
+ font-size: 11px;
+ font-weight: 800;
+}
+
+.arch-panel-extension-empty-state {
+ border: 1px dashed var(--wb-border);
+ border-radius: 8px;
+ padding: 16px;
+ color: var(--wb-text-muted);
+ font-size: 13px;
+}
+
+.arch-panel-extension-panel-dot {
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: var(--wb-border-strong);
+}
+
+.arch-panel-extension-table-wrap {
+ overflow: auto;
+ border: 1px solid var(--wb-border);
+ border-radius: 6px;
+}
+
+.arch-panel-extension-table-wrap.is-detail {
+ max-height: calc(100vh - 220px);
+}
+
+.arch-panel-extension-table-wrap.is-sr-list {
+ overflow: auto;
+}
+
+.arch-panel-extension-table-wrap.is-ramp-pivot {
+ max-height: min(460px, calc(100vh - 260px));
+}
+
+.arch-panel-extension-table {
+ width: 100%;
+ border-collapse: collapse;
+ background: var(--wb-panel);
+}
+
+.arch-panel-extension-table th,
+.arch-panel-extension-table td {
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--wb-row-divider);
+ font-size: 12px;
+ text-align: left;
+ white-space: nowrap;
+}
+
+.arch-panel-extension-table th {
+ background: var(--wb-panel-soft);
+ color: var(--wb-text-secondary);
+ font-weight: 600;
+}
+
+.arch-panel-extension-table td {
+ color: var(--wb-text);
+}
+
+.arch-panel-extension-ramp-pivot-table th:first-child {
+ min-width: 150px;
+ position: sticky;
+ left: 0;
+ z-index: 1;
+}
+
+.arch-panel-extension-ramp-pivot-table tbody th {
+ background: var(--wb-panel-soft);
+ color: var(--wb-text-secondary);
+ font-weight: 700;
+}
+
+.arch-panel-extension-ramp-pivot-table .is-delta-different {
+ color: #d94f2b;
+ font-weight: 800;
+}
+
+.arch-panel-extension-sr-table td {
+ white-space: normal;
+ vertical-align: top;
+}
+
+.arch-panel-extension-sr-main {
+ width: 78%;
+}
+
+.arch-panel-extension-sr-side {
+ width: 22%;
+ text-align: right;
+}
+
+.arch-panel-extension-sr-line {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px 14px;
+ margin-top: 6px;
+}
+
+.arch-panel-extension-sr-line.is-head {
+ margin-top: 0;
+}
+
+.arch-panel-extension-sr-customer {
+ color: var(--wb-text);
+ font-size: 13px;
+}
+
+.arch-panel-extension-sr-moment {
+ color: var(--wb-text);
+ font-size: 18px;
+ font-weight: 700;
+}
+
+.arch-panel-extension-sr-exact,
+.arch-panel-extension-sr-hours {
+ margin-top: 6px;
+ color: var(--wb-text-secondary);
+ font-size: 12px;
+}
+
+.arch-panel-extension-table-empty {
+ padding: 18px;
+ border: 1px dashed var(--wb-border);
+ border-radius: 8px;
+ color: var(--wb-text-secondary);
+ font-size: 13px;
+}
+
+.arch-panel-extension-table-wrap.is-main .arch-panel-extension-table {
+ table-layout: fixed;
+}
+
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-col-opportunity {
+ width: 14%;
+}
+
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-col-customer {
+ width: 16%;
+}
+
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-col-workload {
+ width: auto;
+}
+
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-col-acr {
+ width: 11%;
+}
+
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-col-type {
+ width: 11%;
+}
+
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-col-flag {
+ width: 9%;
+}
+
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-table
+ th:nth-child(3),
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-table
+ td:nth-child(3) {
+ white-space: normal;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-table
+ td:nth-child(3)
+ .arch-panel-extension-row-link {
+ display: block;
+}
+
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-table
+ th:nth-child(2),
+.arch-panel-extension-table-wrap.is-main
+ .arch-panel-extension-table
+ td:nth-child(2) {
+ white-space: normal;
+ overflow-wrap: anywhere;
+}
+
+.arch-panel-extension-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 54px;
+ min-height: 24px;
+ padding: 0 10px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+}
+
+.arch-panel-extension-badge.is-success {
+ background: #dcfce7;
+ border: 1px solid #86efac;
+ color: #166534;
+}
+
+.arch-panel-extension-badge.is-danger {
+ background: #fee2e2;
+ border: 1px solid #fca5a5;
+ color: #991b1b;
+}
+
+.arch-panel-extension-action-cell {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.arch-panel-extension-add-action-button {
+ appearance: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 26px;
+ height: 26px;
+ padding: 0;
+ border: 1px solid var(--wb-border);
+ border-radius: 999px;
+ background: var(--wb-panel-soft);
+ color: var(--wb-text);
+ font: inherit;
+ font-size: 18px;
+ line-height: 1;
+ cursor: pointer;
+}
+
+.arch-panel-extension-table tbody tr:hover {
+ background: var(--wb-row-hover);
+}
+
+.arch-panel-extension-table th.is-numeric,
+.arch-panel-extension-table td.is-numeric {
+ text-align: right;
+}
+
+.arch-panel-extension-row-link {
+ appearance: none;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--wb-link);
+ font: inherit;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ text-decoration: none;
+}
+
+.arch-panel-extension-workload-link-cell {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+}
+
+.arch-panel-extension-ramp-alert-button {
+ appearance: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ padding: 0;
+ border: 1px solid #d94f2b;
+ border-radius: 999px;
+ background: #fee2e2;
+ color: #991b1b;
+ font: inherit;
+ font-size: 12px;
+ font-weight: 900;
+ line-height: 1;
+ cursor: pointer;
+}
+
+.arch-panel-extension-ramp-alert-button:hover {
+ background: #fecaca;
+}
+
+.arch-panel-extension-insight-list,
+.arch-panel-extension-stage-list,
+.arch-panel-extension-side-column {
+ display: grid;
+ gap: 14px;
+}
+
+.arch-panel-extension-insight {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ padding-top: 12px;
+ border-top: 1px solid var(--wb-divider);
+}
+
+.arch-panel-extension-insight:first-child {
+ padding-top: 0;
+ border-top: 0;
+}
+
+.arch-panel-extension-insight-label,
+.arch-panel-extension-stage-label {
+ margin: 0 0 4px;
+ color: var(--wb-text);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.arch-panel-extension-insight-detail,
+.arch-panel-extension-stage-detail {
+ color: var(--wb-text-muted);
+ font-size: 11px;
+ line-height: 1.45;
+}
+
+.arch-panel-extension-insight-value {
+ margin: 0;
+ color: var(--wb-text);
+ font-size: 16px;
+ font-weight: 700;
+ text-align: right;
+}
+
+.arch-panel-extension-stage-row {
+ display: grid;
+ gap: 8px;
+}
+
+.arch-panel-extension-stage-copy {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.arch-panel-extension-stage-values {
+ display: grid;
+ justify-items: end;
+ gap: 2px;
+ color: var(--wb-text-secondary);
+ font-size: 12px;
+}
+
+.arch-panel-extension-stage-detail-row {
+ color: var(--wb-text-secondary);
+ font-size: 12px;
+}
+
+.arch-panel-extension-stage-bar {
+ position: relative;
+ height: 8px;
+ border-radius: 999px;
+ overflow: hidden;
+ background: color-mix(in srgb, var(--wb-text-secondary) 14%, transparent);
+}
+
+.arch-panel-extension-stage-bar span {
+ position: absolute;
+ inset: 0 auto 0 0;
+ border-radius: inherit;
+ background: linear-gradient(90deg, var(--wb-accent), color-mix(in srgb, var(--wb-accent) 52%, #ffffff));
+}
+
+.arch-panel-extension-detail-overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+ background: rgba(10, 14, 20, 0.68);
+ backdrop-filter: blur(6px);
+}
+
+.arch-panel-extension-time-drawer {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 2;
+ width: min(560px, calc(100vw - 32px));
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ padding: 18px;
+ border-left: 1px solid var(--wb-border);
+ background: var(--wb-panel);
+ box-shadow: -18px 0 42px rgba(0, 0, 0, 0.28);
+ overflow: auto;
+ animation: arch-panel-extension-slide-in 180ms ease-out;
+}
+
+.arch-panel-extension-time-drawer-head {
+ position: sticky;
+ top: -18px;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+ padding: 0 0 12px;
+ border-bottom: 1px solid var(--wb-border);
+ background: var(--wb-panel);
+}
+
+.arch-panel-extension-time-drawer-close {
+ border-color: var(--wb-border);
+ background: var(--wb-panel-soft);
+ color: var(--wb-text);
+ box-shadow: none;
+}
+
+.arch-panel-extension-time-drawer-close:hover {
+ border-color: var(--wb-border-strong);
+ background: var(--wb-row-hover);
+}
+
+.arch-panel-extension-time-drawer-actions {
+ position: sticky;
+ bottom: -18px;
+ display: flex;
+ justify-content: flex-end;
+ margin: auto -18px -18px;
+ padding: 12px 18px 18px;
+ border-top: 1px solid var(--wb-border);
+ background: linear-gradient(180deg, rgba(23, 30, 39, 0), var(--wb-panel) 30%);
+}
+
+.arch-panel-extension-time-drawer-meta {
+ display: flex;
+ gap: 8px;
+ color: var(--wb-text-muted);
+ font-size: 12px;
+}
+
+.arch-panel-extension-time-drawer-state,
+.arch-panel-extension-time-drawer-json {
+ border: 1px solid var(--wb-border);
+ border-radius: 8px;
+ background: var(--wb-panel-soft);
+ padding: 14px;
+}
+
+.arch-panel-extension-time-drawer-state.is-error {
+ border-color: rgba(217, 79, 43, 0.42);
+}
+
+.arch-panel-extension-time-entry-group {
+ display: grid;
+ gap: 10px;
+}
+
+.arch-panel-extension-time-entry-group + .arch-panel-extension-time-entry-group {
+ padding-top: 12px;
+ border-top: 1px solid var(--wb-border);
+}
+
+.arch-panel-extension-time-entry-group-head {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.arch-panel-extension-time-entry-group-head h4 {
+ margin: 3px 0 0;
+ color: var(--wb-text);
+ font-size: 16px;
+ line-height: 1.25;
+}
+
+.arch-panel-extension-time-entry-group-head span {
+ color: var(--wb-text-muted);
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+.arch-panel-extension-time-entry-table-wrap {
+ overflow: auto;
+ border: 1px solid var(--wb-border);
+ border-radius: 8px;
+}
+
+.arch-panel-extension-time-entry-table {
+ width: 100%;
+ border-collapse: collapse;
+ background: var(--wb-panel);
+}
+
+.arch-panel-extension-time-entry-table th,
+.arch-panel-extension-time-entry-table td {
+ padding: 10px 12px;
+ border-bottom: 1px solid var(--wb-row-divider);
+ color: var(--wb-text);
+ font-size: 12px;
+ text-align: left;
+ vertical-align: top;
+}
+
+.arch-panel-extension-time-entry-table th {
+ background: var(--wb-panel-soft);
+ color: var(--wb-text-secondary);
+ font-weight: 700;
+}
+
+.arch-panel-extension-time-entry-table tfoot th {
+ border-bottom: 0;
+ color: var(--wb-text);
+}
+
+.arch-panel-extension-time-entry-table .is-numeric {
+ text-align: right;
+ white-space: nowrap;
+}
+
+.arch-panel-extension-time-drawer-json summary {
+ color: var(--wb-link);
+ font-size: 12px;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.arch-panel-extension-time-drawer-json pre {
+ max-height: 320px;
+ margin: 12px 0 0;
+ overflow: auto;
+ color: var(--wb-text);
+ font-size: 11px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+}
+
+@keyframes arch-panel-extension-slide-in {
+ from {
+ transform: translateX(100%);
+ }
+ to {
+ transform: translateX(0);
+ }
+}
+
+.arch-panel-extension-detail-dialog {
+ width: min(1520px, calc(100vw - 32px));
+ max-height: calc(100vh - 88px);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ border: 1px solid var(--wb-border);
+ border-radius: 10px;
+ background: var(--wb-panel);
+ box-shadow: var(--wb-shadow);
+ padding: 18px;
+}
+
+.arch-panel-extension-ramp-dialog {
+ width: min(1320px, calc(100vw - 48px));
+ max-height: calc(100vh - 120px);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ border: 1px solid var(--wb-border);
+ border-radius: 10px;
+ background: var(--wb-panel);
+ box-shadow: var(--wb-shadow);
+ padding: 18px;
+}
+
+.arch-panel-extension-action-dialog {
+ width: min(980px, calc(100vw - 32px));
+ max-height: calc(100vh - 88px);
+ overflow: auto;
+ 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-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;
+ --arch-time-management-scrollbar-gutter: 16px;
+ gap: 4px;
+ min-height: 0;
+ overflow: visible;
+}
+
+.arch-panel-extension-time-management-card.is-error {
+ border-color: rgba(217, 79, 43, 0.42);
+}
+
+.arch-panel-extension-time-management-toolbar {
+ display: flex;
+ align-items: end;
+ gap: 14px;
+ flex-wrap: wrap;
+}
+
+.arch-panel-extension-time-management-hour-indicators {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(92px, 1fr));
+ gap: 8px;
+ flex: 1 1 520px;
+}
+
+.arch-panel-extension-time-management-hour-indicator {
+ display: grid;
+ gap: 3px;
+ min-height: 48px;
+ align-content: center;
+ border: 1px solid var(--wb-border);
+ border-radius: 8px;
+ background: var(--wb-panel);
+ padding: 7px 10px;
+}
+
+.arch-panel-extension-time-management-hour-indicator span {
+ color: var(--wb-text-muted);
+ font-size: 10px;
+ font-weight: 800;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.arch-panel-extension-time-management-hour-indicator strong {
+ color: var(--wb-text);
+ font-size: 16px;
+ font-weight: 800;
+ line-height: 1.1;
+}
+
+.arch-panel-extension-time-management-hour-indicator .arch-panel-extension-skeleton-text {
+ width: 72%;
+}
+
+.arch-panel-extension-time-management-hour-indicator .arch-panel-extension-skeleton-title {
+ width: 42%;
+ height: 16px;
+}
+
+.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: 4px 0;
+ scrollbar-gutter: stable;
+}
+
+.arch-panel-extension-time-management-rows:has(.arch-panel-extension-combobox:focus-within) {
+ overflow: visible;
+}
+
+.arch-panel-extension-time-management-empty {
+ grid-column: 1 / -1;
+ text-align: center;
+}
+
+.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: center;
+ gap: 6px;
+ padding-right: var(--arch-time-management-scrollbar-gutter);
+}
+
+.arch-panel-extension-time-management-column-label {
+ color: var(--wb-text-secondary);
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.2;
+}
+
+.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-duplicate-highlight {
+ border-radius: 10px;
+ animation: arch-panel-extension-duplicate-pulse 1100ms ease-in-out 2;
+ box-shadow:
+ inset 0 0 0 3px rgba(217, 79, 43, 0.9),
+ 0 0 0 2px rgba(217, 79, 43, 0.12);
+ padding: 4px;
+ margin: -4px 0;
+}
+
+.arch-panel-extension-time-management-entry-row.is-compact .arch-panel-extension-form-field {
+ gap: 0;
+}
+
+@keyframes arch-panel-extension-duplicate-pulse {
+ 0% {
+ box-shadow:
+ inset 0 0 0 2px rgba(217, 79, 43, 0.35),
+ 0 0 0 0 rgba(217, 79, 43, 0.22);
+ }
+ 45% {
+ box-shadow:
+ inset 0 0 0 3px rgba(217, 79, 43, 0.95),
+ 0 0 0 6px rgba(217, 79, 43, 0.14);
+ }
+ 100% {
+ box-shadow:
+ inset 0 0 0 3px rgba(217, 79, 43, 0.72),
+ 0 0 0 0 rgba(217, 79, 43, 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;
+ align-content: center;
+ justify-items: center;
+ gap: 1px;
+ min-height: 22px;
+ padding: 0;
+ color: var(--wb-text-secondary);
+ line-height: 1.15;
+ text-align: center;
+}
+
+.arch-panel-extension-time-management-day-head strong {
+ color: var(--wb-text-secondary);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.arch-panel-extension-time-management-day-head span {
+ color: var(--wb-text-muted);
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: none;
+}
+
+.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);
+ overflow: auto;
+ border: 1px solid var(--wb-border);
+ border-radius: 10px;
+ background: var(--wb-panel);
+ box-shadow: var(--wb-shadow);
+ padding: 18px;
+}
+
+.arch-panel-extension-detail-title {
+ margin: 0;
+ color: var(--wb-text);
+ font-size: 18px;
+ font-weight: 700;
+}
+
+.arch-panel-extension-detail-subtitle {
+ margin: 0 0 14px;
+ color: var(--wb-text-muted);
+ font-size: 12px;
+ line-height: 1.5;
+ overflow-wrap: anywhere;
+}
+
+.arch-panel-extension-detail-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ margin-bottom: 8px;
+}
+
+.arch-panel-extension-detail-meta {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 14px;
+ color: var(--wb-text-muted);
+ font-size: 12px;
+}
+
+.arch-panel-extension-detail-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 10px;
+ position: sticky;
+ bottom: 0;
+ margin-top: 16px;
+ padding-top: 12px;
+ 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-time-management-save-error {
+ margin: 0 auto 0 0;
+ color: var(--wb-warning);
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.arch-panel-extension-form-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 14px;
+ margin-top: 18px;
+}
+
+.arch-panel-extension-form-field {
+ display: grid;
+ gap: 6px;
+}
+
+.arch-panel-extension-form-field span {
+ color: var(--wb-text-secondary);
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.arch-panel-extension-form-field input,
+.arch-panel-extension-form-field select {
+ width: 100%;
+ min-height: 38px;
+ padding: 0 12px;
+ border: 1px solid var(--wb-border);
+ border-radius: 6px;
+ background: var(--wb-panel-soft);
+ color: var(--wb-text);
+ 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;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.arch-panel-extension-sort-button {
+ appearance: none;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.arch-panel-extension-sort-button.is-numeric {
+ justify-content: flex-end;
+ width: 100%;
+}
+
+.arch-panel-extension-table-wrap.is-detail {
+ flex: 1 1 auto;
+ min-height: 0;
+ max-height: calc(100vh - 270px);
+ overflow: auto;
+}
+
+.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table th {
+ position: sticky;
+ top: 0;
+ z-index: 3;
+}
+
+.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table thead {
+ position: relative;
+ z-index: 3;
+}
+
+.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table {
+ border-collapse: separate;
+ border-spacing: 0;
+}
+
+.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table {
+ table-layout: fixed;
+}
+
+.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table th,
+.arch-panel-extension-table-wrap.is-detail .arch-panel-extension-table td {
+ white-space: normal;
+ vertical-align: top;
+ overflow-wrap: anywhere;
+}
+
+.arch-panel-extension-state-actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 18px;
+}
+
+.arch-panel-extension-state-card.is-error {
+ border-color: rgba(217, 79, 43, 0.42);
+}
+
+@media (max-width: 1280px) {
+ .arch-panel-extension-summary-row,
+ .arch-panel-extension-indicator-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 1100px) {
+ .arch-panel-extension-shell-body,
+ .arch-panel-extension-content-grid,
+ .arch-panel-extension-status-grid,
+ .arch-panel-extension-operations-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .arch-panel-extension-sidebar {
+ display: none;
+ }
+}
+
+@media (max-width: 780px) {
+ .arch-panel-extension-shell-header,
+ .arch-panel-extension-page-hero {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .arch-panel-extension-summary-row,
+ .arch-panel-extension-indicator-grid,
+ .arch-panel-extension-boolean-grid,
+ .arch-panel-extension-sr-hour-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .arch-panel-extension-state-card {
+ min-height: 260px;
+ padding: 26px 24px;
+ }
+}
+
+@media (max-width: 980px) {
+ .arch-panel-extension-action-dialog {
+ width: min(720px, calc(100vw - 24px));
+ }
+
+ .arch-panel-extension-form-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/src/content/templates.js b/src/content/templates.js
new file mode 100644
index 0000000..2028f3f
--- /dev/null
+++ b/src/content/templates.js
@@ -0,0 +1,15 @@
+(() => {
+ function hcmTile(hcmTileId) {
+ return `
+`;
+ }
+
+ globalThis.ArchPanelTemplates = Object.freeze({
+ hcmTile,
+ });
+})();
diff --git a/src/content/workbench-controller.js b/src/content/workbench-controller.js
new file mode 100644
index 0000000..c938d46
--- /dev/null
+++ b/src/content/workbench-controller.js
@@ -0,0 +1,966 @@
+(() => {
+ function create({
+ appState,
+ IDS,
+ DETAIL_SORT_KEYS,
+ hcmPage,
+ workbenchRepository,
+ comcipRepository,
+ readCachedDataset,
+ writeCachedDataset,
+ readCachedWorklistToken,
+ setStoredCalendarView,
+ refreshTaskTypeCache,
+ renderModal,
+ cleanString,
+ normalizeString,
+ toNumber,
+ toIsoDateString,
+ mapWithConcurrency,
+ getLocalDateKey,
+ getErrorMessage,
+ formatWholeNumber,
+ formatProgressPercent,
+ formatDate,
+ teamContainsUser,
+ stripTransientFields,
+ createExportPayload,
+ createDatasetSnapshot,
+ buildRampForecastComparisonRows,
+ parseForecastMonthNumber,
+ createForecastUpdatePayload,
+ normalizeTimeEntriesSummary,
+ getCalendarAnchorDate,
+ }) {
+ let cacheHydrationPromise = null;
+
+ async function refreshData() {
+ if (appState.status === "loading") {
+ return;
+ }
+
+ try {
+ appState.status = "loading";
+ appState.errorMessage = "";
+ updateLoadingProgress("Requesting current user", 4);
+
+ const user = await fetchCurrentUser();
+
+ updateLoadingProgress("Loading current resource user", 8);
+
+ const resourceCurrentUser = await comcipRepository
+ .fetchResourceCurrentUser(user.userEmail)
+ .catch(() => null);
+
+ updateLoadingProgress("Loading customer summary pages", 12);
+
+ const customers = await workbenchRepository.fetchAllCustomers(
+ user.userEmail
+ );
+
+ updateLoadingProgress(
+ `Loading workloads for ${formatWholeNumber(
+ customers.length
+ )} customers`,
+ 24
+ );
+
+ let completedCustomers = 0;
+ const customersWithWorkloads = await mapWithConcurrency(
+ customers,
+ 4,
+ async (customer, index) => {
+ updateLoadingProgress(
+ `Loading workloads ${index + 1}/${customers.length}: ${customer.name}`,
+ getSegmentProgress(24, 52, completedCustomers, customers.length)
+ );
+
+ const workloads = await workbenchRepository.fetchCustomerWorkloads(
+ customer
+ );
+ completedCustomers += 1;
+ updateLoadingProgress(
+ `Loaded workloads ${completedCustomers}/${customers.length}: ${customer.name}`,
+ getSegmentProgress(24, 52, completedCustomers, customers.length)
+ );
+
+ return {
+ customerId: customer.customerId,
+ name: customer.name,
+ workloads,
+ };
+ }
+ );
+
+ const allWorkloads = customersWithWorkloads.flatMap((customer) => {
+ return customer.workloads.map((workload) => ({
+ ...workload,
+ customerId: customer.customerId,
+ customerName: customer.name,
+ }));
+ });
+
+ updateLoadingProgress(
+ `Loading actions and service requests for ${formatWholeNumber(
+ allWorkloads.length
+ )} workloads`,
+ 56
+ );
+
+ let completedWorkloads = 0;
+ const enrichedWorkloads = await mapWithConcurrency(
+ allWorkloads,
+ 6,
+ async (workload, index) => {
+ updateLoadingProgress(
+ `Loading actions and SRs ${index + 1}/${allWorkloads.length}: ${workload.name}`,
+ getSegmentProgress(56, 86, completedWorkloads, allWorkloads.length)
+ );
+
+ const [actions, serviceRequests] = await Promise.all([
+ workbenchRepository.fetchWorkloadActions(workload.workloadId),
+ workbenchRepository.fetchWorkloadServiceRequests(workload.workloadId),
+ ]);
+ completedWorkloads += 1;
+ updateLoadingProgress(
+ `Loaded actions and SRs ${completedWorkloads}/${allWorkloads.length}: ${workload.name}`,
+ getSegmentProgress(56, 86, completedWorkloads, allWorkloads.length)
+ );
+
+ const hasSR = serviceRequests.some((serviceRequest) => {
+ return (
+ normalizeString(serviceRequest.status) ===
+ "SVC_PENDING_DELIVERY" &&
+ teamContainsUser(serviceRequest.team, user.userEmail)
+ );
+ });
+
+ const hasAction = actions.some((action) => {
+ return (
+ normalizeString(action.owner) === normalizeString(user.userEmail)
+ );
+ });
+
+ return {
+ ...workload,
+ actions,
+ serviceRequests,
+ hasSR,
+ hasAction,
+ };
+ }
+ );
+
+ const customersMap = new Map(
+ customersWithWorkloads.map((customer) => [
+ customer.customerId,
+ {
+ customerId: customer.customerId,
+ name: customer.name,
+ workloads: [],
+ },
+ ])
+ );
+
+ for (const workload of enrichedWorkloads) {
+ const customer = customersMap.get(workload.customerId);
+
+ if (customer) {
+ customer.workloads.push(stripTransientFields(workload));
+ }
+ }
+
+ updateLoadingProgress("Loading pending delivery service requests", 90);
+
+ const pendingServiceRequests = await comcipRepository
+ .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(
+ user,
+ finalCustomers,
+ pendingServiceRequests,
+ resourceCurrentUser
+ );
+ const dataset = createDatasetSnapshot(exportPayload);
+
+ await writeCachedDataset(exportPayload);
+ updateLoadingProgress("Dashboard ready", 100);
+
+ appState.dataset = dataset;
+ appState.status = "ready";
+ appState.loadingMessage = "";
+ appState.loadingProgress = 0;
+ appState.detailModal = null;
+ renderModal();
+ } catch (error) {
+ appState.status = "error";
+ appState.errorMessage = getErrorMessage(error);
+ appState.loadingMessage = "";
+ appState.loadingProgress = 0;
+ renderModal();
+ }
+ }
+
+ function updateLoadingProgress(message, progress) {
+ appState.loadingMessage = message;
+ appState.loadingProgress = Math.max(
+ 0,
+ Math.min(Number(progress) || 0, 100)
+ );
+
+ if (!updateLoadingProgressDom()) {
+ renderModal();
+ window.requestAnimationFrame(() => {
+ updateLoadingProgressDom();
+ });
+ }
+ }
+
+ function updateLoadingProgressDom() {
+ const overlay = document.getElementById(IDS.overlay);
+ const progress = Math.max(0, Math.min(appState.loadingProgress || 0, 100));
+ const stateCopy = overlay?.querySelector(
+ ".arch-panel-extension-state-copy"
+ );
+ const progressRoot = overlay?.querySelector(
+ ".arch-panel-extension-loading-progress"
+ );
+ const progressBar = overlay?.querySelector(
+ ".arch-panel-extension-loading-progress-fill"
+ );
+ const progressTrackValue = overlay?.querySelector(
+ ".arch-panel-extension-loading-progress-value"
+ );
+ const shellNote = overlay?.querySelector(
+ ".arch-panel-extension-shell-note"
+ );
+
+ if (!stateCopy || !progressRoot || !progressBar) {
+ return false;
+ }
+
+ stateCopy.textContent = appState.loadingMessage;
+ progressRoot.setAttribute("aria-valuenow", String(Math.round(progress)));
+ progressBar.style.width = `${progress}%`;
+
+ if (progressTrackValue) {
+ progressTrackValue.textContent = formatProgressPercent(progress);
+ }
+
+ if (shellNote) {
+ shellNote.textContent = appState.loadingMessage || "Loading";
+ }
+
+ return true;
+ }
+
+ function getSegmentProgress(start, end, current, total) {
+ const denominator = Math.max(Number(total) || 0, 1);
+ const ratio = Math.max(
+ 0,
+ Math.min((Number(current) || 0) / denominator, 1)
+ );
+
+ return start + (end - start) * ratio;
+ }
+
+ async function hydrateDatasetFromCache() {
+ if (appState.dataset || cacheHydrationPromise) {
+ return cacheHydrationPromise;
+ }
+
+ appState.status = "loading";
+ appState.errorMessage = "";
+ appState.loadingMessage = "Loading cached snapshot";
+ appState.loadingProgress = 18;
+ renderModal();
+
+ cacheHydrationPromise = (async () => {
+ try {
+ const exportPayload = await readCachedDataset();
+ appState.loadingProgress = exportPayload ? 100 : 0;
+
+ if (exportPayload) {
+ appState.dataset = createDatasetSnapshot(exportPayload);
+ appState.status = "ready";
+ } else {
+ appState.status = "idle";
+ }
+ } catch (error) {
+ appState.status = "error";
+ appState.errorMessage = getErrorMessage(error);
+ } finally {
+ appState.loadingMessage = "";
+ appState.loadingProgress = 0;
+ cacheHydrationPromise = null;
+ renderModal();
+ }
+ })();
+
+ return cacheHydrationPromise;
+ }
+
+ async function fetchCurrentUser() {
+ const currentFrameToken = hcmPage.syncWorklistIframeToken({
+ force: true,
+ })?.token;
+
+ const worklistToken = currentFrameToken || (await readCachedWorklistToken());
+
+ if (!worklistToken) {
+ throw new Error(
+ "Unable to resolve Worklist token. Reload the FuseWelcome page so the Worklist iframe token can be captured before refreshing data."
+ );
+ }
+
+ appState.sessionAuthHeaders = {};
+
+ const user = await workbenchRepository.fetchWorklistCurrentUser(
+ worklistToken
+ );
+
+ appState.sessionAuthHeaders = {};
+
+ return user;
+ }
+
+ function findWorkloadById(workloadId) {
+ if (!appState.dataset || !workloadId) {
+ return null;
+ }
+
+ for (const customer of appState.dataset.customers || []) {
+ for (const workload of customer.workloads || []) {
+ if (workload.workloadId === workloadId) {
+ return {
+ ...workload,
+ customerId: workload.customerId || customer.customerId,
+ customerName: workload.customerName || customer.name,
+ };
+ }
+ }
+ }
+
+ return null;
+ }
+
+ function applyForecastUpdateToDataset(workloadId, payload) {
+ if (!appState.dataset || !workloadId) {
+ return;
+ }
+
+ const previousDetailModal = appState.detailModal
+ ? {
+ scope: appState.detailModal.scope,
+ value: appState.detailModal.value,
+ sortKey: appState.detailModal.sortKey,
+ sortDirection: appState.detailModal.sortDirection,
+ type: appState.detailModal.type,
+ }
+ : null;
+ const updatesByPeriod = new Map(
+ (payload || []).map((item) => [
+ `${toNumber(item.year)}-${toNumber(item.month)}`,
+ toNumber(item.amount),
+ ])
+ );
+
+ function updateForecast(workload) {
+ if (!workload || workload.workloadId !== workloadId) {
+ return workload;
+ }
+
+ workload.forecast = (workload.forecast || []).map((forecastItem) => {
+ const month = parseForecastMonthNumber(forecastItem.month);
+ const year = toNumber(forecastItem.year);
+ const key = `${year}-${month}`;
+
+ if (!updatesByPeriod.has(key)) {
+ return forecastItem;
+ }
+
+ return {
+ ...forecastItem,
+ adjustedConsumptionAmount: updatesByPeriod.get(key),
+ updatedBy: appState.dataset.user?.userEmail || forecastItem.updatedBy,
+ updatedDate: new Date().toISOString(),
+ };
+ });
+
+ return workload;
+ }
+
+ for (const customer of appState.dataset.customers || []) {
+ customer.workloads = (customer.workloads || []).map((workload) =>
+ updateForecast(workload)
+ );
+ }
+
+ for (const customer of appState.dataset.exportPayload?.customers || []) {
+ customer.workloads = (customer.workloads || []).map((workload) =>
+ updateForecast(workload)
+ );
+ }
+
+ appState.dataset = createDatasetSnapshot(appState.dataset.exportPayload);
+
+ if (
+ previousDetailModal?.scope &&
+ previousDetailModal.type !== "serviceRequests"
+ ) {
+ restoreDetailModal(previousDetailModal);
+ }
+
+ void writeCachedDataset(appState.dataset.exportPayload);
+ }
+
+ function openDetailModal(scope, value) {
+ if (!appState.dataset) {
+ return;
+ }
+
+ const eligibleWorkloads = appState.dataset.summary.eligibleWorkloads;
+ let items = [];
+ let label = "";
+ let title = "";
+
+ if (scope === "stage") {
+ items = eligibleWorkloads.filter(
+ (workload) => workload.opportunityForecastTypeGroup === value
+ );
+ label = `Opportunity type: ${value}`;
+ title = `${value} workloads`;
+ } else if (scope === "sr") {
+ const flag = value === "true";
+ items = eligibleWorkloads.filter((workload) => workload.hasSR === flag);
+ label = "Status de SRs";
+ title = flag ? "Workloads com SR" : "Workloads sem SR";
+ } else if (scope === "action") {
+ const flag = value === "true";
+ items = eligibleWorkloads.filter((workload) => workload.hasAction === flag);
+ label = "Status de Consumption Plan/action";
+ title = flag ? "Workloads com action" : "Workloads sem action";
+ } else if (scope === "sr-hours") {
+ const pendingServiceRequests =
+ appState.dataset.summary.pendingServiceRequests || [];
+ const isAboveFour = value === "above-4";
+ items = pendingServiceRequests.filter((serviceRequest) => {
+ const hours = toNumber(serviceRequest.totalHoursWorked);
+ return isAboveFour ? hours > 4 : hours >= 0 && hours <= 4;
+ });
+ items.sort((left, right) => {
+ return toNumber(left.totalHoursWorked) - toNumber(right.totalHoursWorked);
+ });
+ label = "SRs por horas reportadas";
+ title = isAboveFour ? "SRs acima de 4h" : "SRs entre 0 e 4h";
+ } else if (scope === "calendar-day") {
+ const allWorkloads = appState.dataset.summary.allWorkloads || [];
+ items = allWorkloads.filter((workload) => {
+ return getLocalDateKey(workload.consumptionStartDate) === value;
+ });
+ label = "Workload start calendar";
+ title = `Workloads starting ${formatDate(value) || value}`;
+ }
+
+ appState.detailModal = {
+ label,
+ title,
+ items,
+ totalAcr:
+ scope === "sr-hours"
+ ? 0
+ : items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
+ sortKey: DETAIL_SORT_KEYS.adjustedACR,
+ sortDirection: "desc",
+ scope,
+ value,
+ type: scope === "sr-hours" ? "serviceRequests" : "workloads",
+ };
+
+ renderModal();
+ }
+
+ function openActionFormModal(workloadId) {
+ appState.actionFormModal = {
+ workloadId,
+ errorMessage: "",
+ isSubmitting: false,
+ };
+ renderModal();
+ }
+
+ function openRampComparisonModal(workloadId) {
+ const workload = findWorkloadById(workloadId);
+
+ if (!workload) {
+ return;
+ }
+
+ appState.rampComparisonModal = {
+ workload,
+ rows: buildRampForecastComparisonRows([workload]),
+ };
+ renderModal();
+ }
+
+ function openForecastUpdateConfirmModal() {
+ if (!appState.rampComparisonModal || !appState.dataset) {
+ return;
+ }
+
+ const payload = createForecastUpdatePayload(
+ appState.rampComparisonModal.rows || [],
+ appState.dataset?.user?.userEmail || ""
+ );
+
+ appState.forecastUpdateConfirmModal = {
+ workloadId: appState.rampComparisonModal.workload.workloadId,
+ payload,
+ isSubmitting: false,
+ errorMessage: "",
+ };
+ renderModal();
+ }
+
+ async function submitForecastUpdate() {
+ const modal = appState.forecastUpdateConfirmModal;
+
+ if (!modal || modal.isSubmitting) {
+ return;
+ }
+
+ appState.forecastUpdateConfirmModal = {
+ ...modal,
+ isSubmitting: true,
+ errorMessage: "",
+ };
+ renderModal();
+
+ try {
+ const workloadId = cleanString(modal.workloadId);
+ await workbenchRepository.updateWorkloadForecast(
+ workloadId,
+ modal.payload
+ );
+
+ applyForecastUpdateToDataset(workloadId, modal.payload);
+ const updatedWorkload = findWorkloadById(workloadId);
+
+ appState.forecastUpdateConfirmModal = null;
+
+ if (updatedWorkload) {
+ appState.rampComparisonModal = {
+ workload: updatedWorkload,
+ rows: buildRampForecastComparisonRows([updatedWorkload]),
+ };
+ }
+
+ renderModal();
+ } catch (error) {
+ appState.forecastUpdateConfirmModal = {
+ ...modal,
+ isSubmitting: false,
+ errorMessage: getErrorMessage(error),
+ };
+ renderModal();
+ }
+ }
+
+ async function openTimeEntriesDrawer(srNumber) {
+ const normalizedSrNumber = cleanString(srNumber);
+
+ if (!normalizedSrNumber) {
+ return;
+ }
+
+ appState.timeEntriesDrawer = {
+ srNumber: normalizedSrNumber,
+ status: "loading",
+ errorMessage: "",
+ payload: null,
+ items: [],
+ };
+ renderModal();
+
+ try {
+ const response = await comcipRepository.fetchTimeEntriesSummary(
+ normalizedSrNumber
+ );
+
+ appState.timeEntriesDrawer = {
+ srNumber: normalizedSrNumber,
+ status: "ready",
+ errorMessage: "",
+ payload: response.payload,
+ items: normalizeTimeEntriesSummary(response.payload),
+ };
+ } catch (error) {
+ appState.timeEntriesDrawer = {
+ srNumber: normalizedSrNumber,
+ status: "error",
+ errorMessage: getErrorMessage(error),
+ payload: null,
+ items: [],
+ };
+ }
+
+ renderModal();
+ }
+
+ function shiftCalendarPeriod(delta) {
+ const nextDate =
+ appState.calendarView === "week"
+ ? getShiftedCalendarWeekDate(delta)
+ : new Date(appState.calendarYear, appState.calendarMonth + delta, 1);
+
+ appState.calendarYear = nextDate.getFullYear();
+ appState.calendarMonth = nextDate.getMonth();
+ appState.calendarAnchorDateKey = getLocalDateKey(nextDate);
+ renderModal();
+ }
+
+ function goToCurrentCalendarPeriod() {
+ const today = new Date();
+ appState.calendarYear = today.getFullYear();
+ appState.calendarMonth = today.getMonth();
+ appState.calendarAnchorDateKey = getLocalDateKey(today);
+ renderModal();
+ }
+
+ function setCalendarView(view) {
+ const normalizedView = view === "week" ? "week" : "month";
+
+ if (appState.calendarView === normalizedView) {
+ return;
+ }
+
+ appState.calendarView = normalizedView;
+ setStoredCalendarView(normalizedView);
+
+ if (normalizedView === "week") {
+ const today = new Date();
+ const anchorDate =
+ today.getFullYear() === appState.calendarYear &&
+ today.getMonth() === appState.calendarMonth
+ ? today
+ : new Date(appState.calendarYear, appState.calendarMonth, 1);
+
+ appState.calendarAnchorDateKey = getLocalDateKey(anchorDate);
+ appState.calendarYear = anchorDate.getFullYear();
+ appState.calendarMonth = anchorDate.getMonth();
+ } else {
+ const anchorDate = getCalendarAnchorDate(
+ appState.calendarYear,
+ appState.calendarMonth,
+ appState.calendarAnchorDateKey
+ );
+
+ appState.calendarYear = anchorDate.getFullYear();
+ appState.calendarMonth = anchorDate.getMonth();
+ }
+
+ renderModal();
+ }
+
+ function shiftCalendarMonth(delta) {
+ shiftCalendarPeriod(delta);
+ }
+
+ function goToCurrentCalendarMonth() {
+ goToCurrentCalendarPeriod();
+ }
+
+ function getShiftedCalendarWeekDate(delta) {
+ const anchorDate = getCalendarAnchorDate(
+ appState.calendarYear,
+ appState.calendarMonth,
+ appState.calendarAnchorDateKey
+ );
+
+ anchorDate.setDate(anchorDate.getDate() + delta * 7);
+ return anchorDate;
+ }
+
+ async function submitActionForm(form) {
+ if (!appState.dataset || !appState.actionFormModal) {
+ return;
+ }
+
+ if (!form.reportValidity()) {
+ return;
+ }
+
+ const formData = new FormData(form);
+ const parentId = cleanString(formData.get("parentId"));
+ const ownerEmail = cleanString(formData.get("ownerEmail"));
+ const startDate = toIsoDateString(formData.get("startDate"));
+ const endDate = toIsoDateString(formData.get("endDate"));
+ const payload = {
+ parentType: "WORKLOAD",
+ parentId,
+ action: {
+ ownerEmail,
+ role: "CLOUD_ARCHITECT",
+ name: "SUCCESS_PLAN_CONSUM_RVW",
+ team: cleanString(formData.get("team")),
+ tags: "",
+ startDate,
+ endDate,
+ status: "COMPLETE",
+ complexity: cleanString(formData.get("complexity")),
+ notes: [],
+ objective: "",
+ createdBy: cleanString(formData.get("createdBy")),
+ updatedBy: cleanString(formData.get("updatedBy")),
+ },
+ };
+
+ try {
+ appState.actionFormModal = {
+ ...appState.actionFormModal,
+ isSubmitting: true,
+ errorMessage: "",
+ };
+ renderModal();
+
+ await workbenchRepository.createWorkloadAction(payload);
+
+ await applyCreatedActionToDataset(payload);
+ appState.actionFormModal = null;
+ renderModal();
+ } catch (error) {
+ appState.actionFormModal = {
+ ...appState.actionFormModal,
+ isSubmitting: false,
+ errorMessage: getErrorMessage(error),
+ };
+ renderModal();
+ }
+ }
+
+ async function applyCreatedActionToDataset(payload) {
+ if (!appState.dataset) {
+ return;
+ }
+
+ const updatedCustomers = (appState.dataset.customers || []).map(
+ (customer) => ({
+ ...customer,
+ workloads: (customer.workloads || []).map((workload) => {
+ if (workload.workloadId !== payload.parentId) {
+ return workload;
+ }
+
+ return {
+ ...workload,
+ hasAction: true,
+ actions: [
+ ...(workload.actions || []),
+ {
+ owner: payload.action.ownerEmail,
+ role: payload.action.role,
+ name: payload.action.name,
+ team: payload.action.team,
+ startDate: payload.action.startDate,
+ endDate: payload.action.endDate,
+ complexity: payload.action.complexity,
+ },
+ ],
+ };
+ }),
+ })
+ );
+ const exportPayload = createExportPayload(
+ appState.dataset.user,
+ updatedCustomers,
+ appState.dataset.exportPayload?.pendingServiceRequests || [],
+ appState.dataset.exportPayload?.resourceCurrentUser || null
+ );
+ const nextDataset = createDatasetSnapshot(exportPayload);
+ const previousDetailModal = appState.detailModal
+ ? {
+ scope: appState.detailModal.scope,
+ value: appState.detailModal.value,
+ sortKey: appState.detailModal.sortKey,
+ sortDirection: appState.detailModal.sortDirection,
+ }
+ : null;
+
+ appState.dataset = nextDataset;
+ await writeCachedDataset(exportPayload);
+
+ if (previousDetailModal?.scope) {
+ restoreDetailModal(previousDetailModal);
+ }
+ }
+
+ function restoreDetailModal(detailState) {
+ const eligibleWorkloads =
+ appState.dataset?.summary?.eligibleWorkloads || [];
+ let items = [];
+ let label = "";
+ let title = "";
+
+ if (detailState.scope === "stage") {
+ items = eligibleWorkloads.filter(
+ (workload) =>
+ workload.opportunityForecastTypeGroup === detailState.value
+ );
+ label = `Opportunity type: ${detailState.value}`;
+ title = `${detailState.value} workloads`;
+ } else if (detailState.scope === "sr") {
+ const flag = detailState.value === "true";
+ items = eligibleWorkloads.filter((workload) => workload.hasSR === flag);
+ label = "Status de SRs";
+ title = flag ? "Workloads com SR" : "Workloads sem SR";
+ } else if (detailState.scope === "action") {
+ const flag = detailState.value === "true";
+ items = eligibleWorkloads.filter((workload) => workload.hasAction === flag);
+ label = "Status de Consumption Plan/action";
+ title = flag ? "Workloads com action" : "Workloads sem action";
+ } else if (detailState.scope === "calendar-day") {
+ const allWorkloads = appState.dataset?.summary?.allWorkloads || [];
+ items = allWorkloads.filter((workload) => {
+ return getLocalDateKey(workload.consumptionStartDate) === detailState.value;
+ });
+ label = "Workload start calendar";
+ title = `Workloads starting ${
+ formatDate(detailState.value) || detailState.value
+ }`;
+ }
+
+ appState.detailModal = {
+ label,
+ title,
+ items,
+ totalAcr: items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
+ sortKey: detailState.sortKey || DETAIL_SORT_KEYS.adjustedACR,
+ sortDirection: detailState.sortDirection || "desc",
+ scope: detailState.scope,
+ value: detailState.value,
+ type: "workloads",
+ };
+ }
+
+ function toggleDetailSort(sortKey) {
+ if (!appState.detailModal) {
+ return;
+ }
+
+ const nextDirection =
+ appState.detailModal.sortKey === sortKey &&
+ appState.detailModal.sortDirection === "asc"
+ ? "desc"
+ : "asc";
+
+ appState.detailModal = {
+ ...appState.detailModal,
+ sortKey,
+ sortDirection: nextDirection,
+ };
+
+ renderModal();
+ }
+
+ function getSortedDetailItems() {
+ if (!appState.detailModal) {
+ return [];
+ }
+
+ return sortWorkloads(
+ appState.detailModal.items,
+ appState.detailModal.sortKey,
+ appState.detailModal.sortDirection
+ );
+ }
+
+ function sortWorkloads(items, sortKey, sortDirection = "asc") {
+ const factor = sortDirection === "desc" ? -1 : 1;
+
+ return [...items].sort((left, right) => {
+ const comparison = compareWorkloadValues(
+ getSortableWorkloadValue(left, sortKey),
+ getSortableWorkloadValue(right, sortKey)
+ );
+
+ if (comparison !== 0) {
+ return comparison * factor;
+ }
+
+ return compareWorkloadValues(
+ getSortableWorkloadValue(left, DETAIL_SORT_KEYS.workload),
+ getSortableWorkloadValue(right, DETAIL_SORT_KEYS.workload)
+ );
+ });
+ }
+
+ function getSortableWorkloadValue(workload, sortKey) {
+ switch (sortKey) {
+ case DETAIL_SORT_KEYS.opportunityNumber:
+ return cleanString(workload.opportunityNumber);
+ case DETAIL_SORT_KEYS.customerName:
+ return cleanString(workload.customerName);
+ case DETAIL_SORT_KEYS.workload:
+ return `${cleanString(workload.name)}|${cleanString(
+ workload.description
+ )}`;
+ case DETAIL_SORT_KEYS.rampMonths:
+ return toNumber(workload.rampMonths);
+ case DETAIL_SORT_KEYS.adjustedACR:
+ return toNumber(workload.adjustedACR);
+ case DETAIL_SORT_KEYS.opportunityForecastTypeGroup:
+ return cleanString(workload.opportunityForecastTypeGroup);
+ case DETAIL_SORT_KEYS.hasSR:
+ return Boolean(workload.hasSR);
+ case DETAIL_SORT_KEYS.hasAction:
+ return Boolean(workload.hasAction);
+ default:
+ return cleanString(workload.name);
+ }
+ }
+
+ function compareWorkloadValues(left, right) {
+ if (typeof left === "number" || typeof right === "number") {
+ return toNumber(left) - toNumber(right);
+ }
+
+ if (typeof left === "boolean" || typeof right === "boolean") {
+ return Number(Boolean(left)) - Number(Boolean(right));
+ }
+
+ return String(left || "").localeCompare(String(right || ""), undefined, {
+ numeric: true,
+ sensitivity: "base",
+ });
+ }
+
+ return Object.freeze({
+ refreshData,
+ hydrateDatasetFromCache,
+ findWorkloadById,
+ openDetailModal,
+ openActionFormModal,
+ openRampComparisonModal,
+ openForecastUpdateConfirmModal,
+ submitForecastUpdate,
+ openTimeEntriesDrawer,
+ shiftCalendarPeriod,
+ goToCurrentCalendarPeriod,
+ setCalendarView,
+ shiftCalendarMonth,
+ goToCurrentCalendarMonth,
+ submitActionForm,
+ toggleDetailSort,
+ getSortedDetailItems,
+ });
+ }
+
+ globalThis.ArchPanelWorkbenchController = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/domain/ramp-forecast.js b/src/domain/ramp-forecast.js
new file mode 100644
index 0000000..3563b20
--- /dev/null
+++ b/src/domain/ramp-forecast.js
@@ -0,0 +1,216 @@
+(() => {
+ function create(dependencies) {
+ const {
+ toNumber,
+ normalizeString,
+ parseDatePreservingDateOnly,
+ formatWeekNumber,
+ } = dependencies;
+
+ function buildRampForecastComparisonRows(workloads) {
+ return (workloads || [])
+ .flatMap((workload) => {
+ const startDate = parseDatePreservingDateOnly(
+ workload.consumptionStartDate
+ );
+
+ if (Number.isNaN(startDate.getTime())) {
+ return [];
+ }
+
+ const startMonth = startDate.getMonth() + 1;
+ const startYear = startDate.getFullYear();
+ const workloadLabel = `${workload.name || "Unnamed workload"}${
+ workload.description ? ` | ${workload.description}` : ""
+ }`;
+
+ return (workload.forecast || [])
+ .map((forecastItem) => {
+ const forecastMonth = parseForecastMonthNumber(forecastItem.month);
+ const forecastYear = toNumber(forecastItem.year);
+
+ if (!forecastMonth || !forecastYear) {
+ return null;
+ }
+
+ const monthIndex =
+ (forecastYear - startYear) * 12 +
+ (forecastMonth - startMonth) +
+ 1;
+
+ const currentValue = toNumber(
+ forecastItem.adjustedConsumptionAmount
+ );
+ const calculatedValue =
+ monthIndex >= 1 && monthIndex <= 12
+ ? calculateRampForecastAmount({
+ adjustedACR: workload.adjustedACR,
+ rampMonths: workload.rampMonths,
+ monthIndex,
+ consumptionStartDate: startDate,
+ })
+ : 0;
+
+ return {
+ workloadLabel,
+ periodLabel: formatForecastPeriodLabel(forecastYear, forecastMonth),
+ currentValue,
+ calculatedValue,
+ delta: currentValue - calculatedValue,
+ monthIndex,
+ year: forecastYear,
+ month: forecastMonth,
+ sortKey: `${workloadLabel}|${String(forecastYear).padStart(
+ 4,
+ "0"
+ )}-${String(forecastMonth).padStart(2, "0")}`,
+ };
+ })
+ .filter(Boolean);
+ })
+ .sort((left, right) => left.sortKey.localeCompare(right.sortKey));
+ }
+
+ function hasRampForecastMismatch(workload) {
+ return buildRampForecastComparisonRows([workload]).some((row) =>
+ isNonZeroAmount(row.delta)
+ );
+ }
+
+ function calculateRampForecastAmount({
+ adjustedACR,
+ rampMonths,
+ monthIndex,
+ consumptionStartDate,
+ }) {
+ const annualValue = toNumber(adjustedACR);
+ const ramp = Math.max(0, toNumber(rampMonths));
+ const month = Math.max(1, toNumber(monthIndex));
+
+ if (!annualValue) {
+ return 0;
+ }
+
+ if (ramp <= 0) {
+ return Math.round(
+ (annualValue / 12) *
+ getStartMonthProrationFactor(month, consumptionStartDate)
+ );
+ }
+
+ const steadyStateMonthlyAmount = (2 * annualValue) / (24 - ramp);
+ const rampStepCount = ramp + 1;
+ const calculatedAmount =
+ month <= rampStepCount
+ ? steadyStateMonthlyAmount * (month / rampStepCount)
+ : steadyStateMonthlyAmount;
+
+ return Math.round(
+ calculatedAmount * getStartMonthProrationFactor(month, consumptionStartDate)
+ );
+ }
+
+ function getStartMonthProrationFactor(monthIndex, consumptionStartDate) {
+ if (toNumber(monthIndex) !== 1) {
+ return 1;
+ }
+
+ const startDate = parseDatePreservingDateOnly(consumptionStartDate);
+
+ if (Number.isNaN(startDate.getTime())) {
+ return 1;
+ }
+
+ const daysInMonth = new Date(
+ startDate.getFullYear(),
+ startDate.getMonth() + 1,
+ 0
+ ).getDate();
+ const remainingDays = daysInMonth - startDate.getDate() + 1;
+
+ return Math.max(0, Math.min(1, remainingDays / daysInMonth));
+ }
+
+ function isNonZeroAmount(value) {
+ return Math.abs(toNumber(value)) > 0.000001;
+ }
+
+ function parseForecastMonthNumber(value) {
+ if (typeof value === "number") {
+ return value >= 1 && value <= 12 ? value : 0;
+ }
+
+ const normalized = normalizeString(value);
+ const numericMonth = Number.parseInt(normalized, 10);
+
+ if (numericMonth >= 1 && numericMonth <= 12) {
+ return numericMonth;
+ }
+
+ const monthMap = {
+ JAN: 1,
+ JANUARY: 1,
+ FEB: 2,
+ FEBRUARY: 2,
+ MAR: 3,
+ MARCH: 3,
+ APR: 4,
+ APRIL: 4,
+ MAY: 5,
+ JUN: 6,
+ JUNE: 6,
+ JUL: 7,
+ JULY: 7,
+ AUG: 8,
+ AUGUST: 8,
+ SEP: 9,
+ SEPT: 9,
+ SEPTEMBER: 9,
+ OCT: 10,
+ OCTOBER: 10,
+ NOV: 11,
+ NOVEMBER: 11,
+ DEC: 12,
+ DECEMBER: 12,
+ };
+
+ return monthMap[normalized] || 0;
+ }
+
+ function formatForecastPeriodLabel(year, month) {
+ const fiscalYear = month >= 6 ? year + 1 : year;
+ const fiscalYearLabel = formatWeekNumber(fiscalYear % 100);
+ const monthLabel = new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ }).format(new Date(year, month - 1, 1));
+
+ return `FY${fiscalYearLabel} ${monthLabel}`;
+ }
+
+ function createForecastUpdatePayload(rows, userEmail = "") {
+ return (rows || [])
+ .filter((row) => row.year && row.month)
+ .map((row) => ({
+ year: toNumber(row.year),
+ month: toNumber(row.month),
+ amount: toNumber(row.calculatedValue),
+ updatedBy: userEmail,
+ }));
+ }
+
+ return {
+ buildRampForecastComparisonRows,
+ hasRampForecastMismatch,
+ calculateRampForecastAmount,
+ getStartMonthProrationFactor,
+ isNonZeroAmount,
+ parseForecastMonthNumber,
+ formatForecastPeriodLabel,
+ createForecastUpdatePayload,
+ };
+ }
+
+ globalThis.ArchPanelRampForecastDomain = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/domain/time-management-data.js b/src/domain/time-management-data.js
new file mode 100644
index 0000000..54b4ce8
--- /dev/null
+++ b/src/domain/time-management-data.js
@@ -0,0 +1,832 @@
+(() => {
+ function create(dependencies) {
+ const {
+ cleanString,
+ normalizeString,
+ firstNonEmptyString,
+ extractArray,
+ getFieldValue,
+ flattenDisplayRecord,
+ toNumber,
+ parseTimeEntryDate,
+ getLocalDateKey,
+ getTimeManagementDayKey,
+ getTimeManagementWeekdayName,
+ createTimeManagementRowSignature,
+ } = dependencies;
+
+ 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 fallbackTitle = cleanString(
+ getFieldValue(item, ["problemDescription", "ProblemDescription"])
+ );
+
+ return {
+ srNumber,
+ srTitle,
+ label: formatTimeManagementServiceRequestLabel({
+ srNumber,
+ srTitle: srTitle || fallbackTitle,
+ }),
+ };
+ })
+ .filter((item) => item.srNumber);
+ }
+
+ function formatTimeManagementServiceRequestLabel(serviceRequest) {
+ const srNumber = cleanString(serviceRequest?.srNumber);
+ const srTitle = cleanString(serviceRequest?.srTitle || serviceRequest?.label);
+
+ if (srTitle && srNumber) {
+ return `${srTitle} (${srNumber})`;
+ }
+
+ return srTitle || 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: formatTimeManagementServiceRequestLabel({
+ srNumber,
+ srTitle: 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 timeEntryId = getTimeEntryId(item);
+ const srNumber = getTimeEntrySrNumber(item) || "non-service-sr";
+ const activityValue = getTimeEntryActivity(item);
+ const taskTypeValue = getTimeEntryTaskType(item);
+
+ return {
+ id: `entry-${index + 1}`,
+ timeEntryId,
+ selectedSrNumber: srNumber,
+ selectedActivityValue: activityValue,
+ selectedTaskTypeValue: taskTypeValue,
+ dayHours: getTimeEntryWeekDayHours(item, weekDays),
+ source: "timeEntryWeek",
+ originalSignature: "",
+ };
+ }
+ );
+ }
+
+ 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 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
+ );
+ const selectedActivityValue =
+ findOptionValue(activityOptions, row?.selectedActivityValue) ||
+ cleanString(row?.selectedActivityValue) ||
+ "";
+ const selectedTaskTypeValue =
+ findOptionValue(taskTypeOptions, row?.selectedTaskTypeValue) ||
+ cleanString(row?.selectedTaskTypeValue) ||
+ "";
+
+ return {
+ ...row,
+ id: cleanString(row?.id) || `entry-${index + 1}`,
+ timeEntryId: cleanString(row?.timeEntryId),
+ selectedSrNumber: srNumber,
+ selectedActivityValue,
+ selectedTaskTypeValue,
+ dayHours: row?.dayHours || {},
+ originalSignature:
+ cleanString(row?.originalSignature) ||
+ (cleanString(row?.source) === "timeEntryWeek"
+ ? createTimeManagementRowSignature({
+ ...row,
+ selectedSrNumber: srNumber,
+ selectedActivityValue,
+ 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 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 normalizeTimeEntriesSummary(payload) {
+ if (Array.isArray(payload)) {
+ return payload;
+ }
+
+ const items = extractArray(payload, [
+ "items",
+ "data",
+ "results",
+ "content",
+ "entries",
+ "timeEntries",
+ "summary",
+ "summaries",
+ ]);
+
+ if (items.length) {
+ return items;
+ }
+
+ if (payload && typeof payload === "object") {
+ return [payload];
+ }
+
+ return [];
+ }
+
+ function groupTimeEntriesByResourceName(items) {
+ const groups = new Map();
+
+ for (const item of items) {
+ const resourceName = getTimeEntryResourceName(item) || "Sem resourceName";
+ const group = groups.get(resourceName) || {
+ resourceName,
+ items: [],
+ };
+
+ group.items.push(item);
+ groups.set(resourceName, group);
+ }
+
+ return Array.from(groups.values()).sort((left, right) => {
+ return left.resourceName.localeCompare(right.resourceName);
+ });
+ }
+
+ function getTimeEntryResourceName(item) {
+ const directValue = getFieldValue(item, [
+ "resourceName",
+ "ResourceName",
+ "resource_name",
+ "resource.name",
+ "Resource.Name",
+ "resource.displayName",
+ "Resource.DisplayName",
+ "resource.display_name",
+ "employeeName",
+ "EmployeeName",
+ "personName",
+ "PersonName",
+ ]);
+
+ if (directValue) {
+ return cleanString(directValue);
+ }
+
+ const flattenedMatch = flattenDisplayRecord(item).find(([key, value]) => {
+ return key.toLowerCase().endsWith("resourcename") && cleanString(value);
+ });
+
+ return flattenedMatch ? cleanString(flattenedMatch[1]) : "";
+ }
+
+ function getTimeEntrySrNumber(item) {
+ return cleanString(
+ getTimeEntryField(item, [
+ "srNumber",
+ "SrNumber",
+ "SRNumber",
+ "serviceRequestNumber",
+ "ServiceRequestNumber",
+ "serviceRequest.srNumber",
+ "ServiceRequest.SrNumber",
+ ])
+ );
+ }
+
+ function getTimeEntryId(item) {
+ return cleanString(
+ getTimeEntryField(item, [
+ "id",
+ "Id",
+ "ID",
+ "timeEntryId",
+ "TimeEntryId",
+ "timeEntryID",
+ "TimeEntryID",
+ ])
+ );
+ }
+
+ function getTimeEntryActivity(item) {
+ return cleanString(
+ getTimeEntryField(item, [
+ "activityCode",
+ "ActivityCode",
+ "activity.code",
+ "Activity.Code",
+ ])
+ );
+ }
+
+ function getTimeEntryTaskType(item) {
+ return cleanString(
+ getTimeEntryField(item, [
+ "TaskType",
+ "taskType",
+ "task_type",
+ "task.type",
+ "Task.Type",
+ ])
+ );
+ }
+
+ function getTimeEntryTimeSpent(item) {
+ return toNumber(
+ getTimeEntryField(item, [
+ "timeSpent",
+ "TimeSpent",
+ "time_spent",
+ "time.spent",
+ "Time.Spent",
+ ])
+ );
+ }
+
+ 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);
+
+ if (directValue !== undefined && directValue !== null && directValue !== "") {
+ return directValue;
+ }
+
+ const normalizedKeys = keys.map((key) =>
+ key.toLowerCase().replace(/[^a-z0-9]/g, "")
+ );
+ const flattenedMatch = flattenDisplayRecord(item).find(([key, value]) => {
+ const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
+
+ return normalizedKeys.includes(normalizedKey) && cleanString(value);
+ });
+
+ return flattenedMatch ? flattenedMatch[1] : "";
+ }
+
+ return {
+ normalizeInfoWeekResponse,
+ normalizeTimeManagementServiceRequests,
+ formatTimeManagementServiceRequestLabel,
+ mergeTimeManagementServiceRequests,
+ normalizeTimeManagementEntryRows,
+ normalizeTimeManagementEntryRowSelection,
+ getActivityRequestSrNumbers,
+ normalizeActivityRequest,
+ normalizeTaskTypeRequest,
+ getTimeManagementTaskTypeOptions,
+ getTimeManagementActivityOptions,
+ findTimeManagementServiceRequest,
+ findTimeManagementServiceRequestByNumber,
+ findOptionValue,
+ findOptionLabel,
+ normalizeTimeEntriesSummary,
+ groupTimeEntriesByResourceName,
+ getTimeEntryTimeSpent,
+ getTimeEntryActivity,
+ getTimeEntryTaskType,
+ };
+ }
+
+ globalThis.ArchPanelTimeManagementData = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/domain/time-management.js b/src/domain/time-management.js
new file mode 100644
index 0000000..9c0ce42
--- /dev/null
+++ b/src/domain/time-management.js
@@ -0,0 +1,240 @@
+(() => {
+ function create(dependencies) {
+ const {
+ cleanString,
+ normalizeString,
+ toNumber,
+ parseTimeEntryDate,
+ getLocalDateKey,
+ } = dependencies;
+
+ function getTimeManagementRows(modal) {
+ return modal?.timeEntryRows || [];
+ }
+
+ function isTimeManagementSaveEnabled(modal) {
+ const pendingChanges = getTimeManagementPendingChanges(modal);
+ const changedRows = [
+ ...pendingChanges.newRows,
+ ...pendingChanges.updatedRows,
+ ];
+
+ if (
+ !pendingChanges.newRows.length &&
+ !pendingChanges.updatedRows.length &&
+ !pendingChanges.deletedIds.length
+ ) {
+ return false;
+ }
+
+ return changedRows.every((row) => isTimeManagementRowReadyToSave(row));
+ }
+
+ function isTimeManagementRowReadyToSave(row) {
+ const selectedSrNumber = cleanString(row?.selectedSrNumber);
+
+ if (!selectedSrNumber) {
+ return false;
+ }
+
+ if (!hasPositiveTimeManagementHours(row)) {
+ return false;
+ }
+
+ if (isNonServiceRequestSrNumber(selectedSrNumber)) {
+ return Boolean(cleanString(row?.selectedTaskTypeValue));
+ }
+
+ return Boolean(
+ cleanString(row?.selectedActivityValue) &&
+ cleanString(row?.selectedTaskTypeValue)
+ );
+ }
+
+ function hasPositiveTimeManagementHours(row) {
+ return Object.values(row?.dayHours || {}).some((value) => {
+ const normalizedValue = cleanString(value);
+
+ if (!normalizedValue) {
+ return false;
+ }
+
+ const numericValue = Number(normalizedValue.replace(",", "."));
+
+ return Number.isFinite(numericValue) && numericValue > 0;
+ });
+ }
+
+ function getTimeManagementPendingChanges(modal) {
+ const rows = getTimeManagementRows(modal);
+
+ return {
+ newRows: rows.filter((row) => isNewTimeManagementRow(row)),
+ updatedRows: rows.filter((row) => isUpdatedTimeManagementRow(row)),
+ deletedIds: (modal?.deletedTimeEntryIds || [])
+ .map((id) => cleanString(id))
+ .filter(Boolean),
+ };
+ }
+
+ function isNewTimeManagementRow(row) {
+ return cleanString(row?.source) === "manual" && !cleanString(row?.timeEntryId);
+ }
+
+ function isUpdatedTimeManagementRow(row) {
+ const timeEntryId = cleanString(row?.timeEntryId);
+ const originalSignature = cleanString(row?.originalSignature);
+
+ return Boolean(
+ timeEntryId &&
+ originalSignature &&
+ originalSignature !== createTimeManagementRowSignature(row)
+ );
+ }
+
+ function createTimeManagementRowSignature(row) {
+ return JSON.stringify({
+ srNumber: cleanString(row?.selectedSrNumber),
+ activityCode: cleanString(row?.selectedActivityValue),
+ taskType: cleanString(row?.selectedTaskTypeValue),
+ dayHours: normalizeTimeManagementDayHours(row?.dayHours || {}),
+ });
+ }
+
+ function normalizeTimeManagementDayHours(dayHours) {
+ return Object.keys(dayHours || {})
+ .sort()
+ .reduce((normalized, key) => {
+ normalized[key] = formatTimeManagementHourPayloadValue(dayHours[key]);
+ return normalized;
+ }, {});
+ }
+
+ function createTimeManagementSavePayload(
+ row,
+ weekDays,
+ weekId,
+ resourceId,
+ options = {}
+ ) {
+ const selectedSrNumber = cleanString(row?.selectedSrNumber);
+ const payload = {
+ srNumber: selectedSrNumber,
+ activityCode: isNonServiceRequestSrNumber(selectedSrNumber)
+ ? ""
+ : cleanString(row?.selectedActivityValue),
+ taskType: cleanString(row?.selectedTaskTypeValue),
+ deliveryType: "",
+ saturdayTaskHours: 0,
+ sundayTaskHours: 0,
+ mondayTaskHours: 0,
+ tuesdayTaskHours: 0,
+ wednesdayTaskHours: 0,
+ thursdayTaskHours: 0,
+ fridayTaskHours: 0,
+ notes: "",
+ weekId: toNumber(weekId) || weekId,
+ resourceId: cleanString(resourceId),
+ tag: isNonServiceRequestSrNumber(selectedSrNumber)
+ ? "NON_SERVICE"
+ : "SERVICE",
+ };
+
+ if (options.includeId) {
+ const id = cleanString(row?.timeEntryId);
+ payload.id = toNumber(id) || id;
+ }
+
+ for (const day of weekDays || []) {
+ const weekdayName = getTimeManagementWeekdayName(day);
+ const dateKey = getTimeManagementDayKey(day);
+ const payloadKey = `${weekdayName}TaskHours`;
+
+ if (payloadKey in payload) {
+ payload[payloadKey] = formatTimeManagementHourPayloadValue(
+ row?.dayHours?.[dateKey]
+ );
+ }
+ }
+
+ return payload;
+ }
+
+ function formatTimeManagementHourPayloadValue(value) {
+ const normalizedValue = cleanString(value);
+
+ if (!normalizedValue) {
+ return 0;
+ }
+
+ const numericValue = Number(normalizedValue.replace(",", "."));
+
+ return Number.isFinite(numericValue) ? numericValue : 0;
+ }
+
+ function getTimeManagementDayKey(day) {
+ const parsedDate = parseTimeEntryDate(day?.date);
+
+ if (!Number.isNaN(parsedDate.getTime())) {
+ return getLocalDateKey(parsedDate);
+ }
+
+ return cleanString(day?.date);
+ }
+
+ 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 isNonServiceRequestSrNumber(srNumber) {
+ return normalizeString(srNumber) === normalizeString("non-service-sr");
+ }
+
+ return {
+ getTimeManagementRows,
+ isTimeManagementSaveEnabled,
+ isTimeManagementRowReadyToSave,
+ hasPositiveTimeManagementHours,
+ getTimeManagementPendingChanges,
+ isNewTimeManagementRow,
+ isUpdatedTimeManagementRow,
+ createTimeManagementRowSignature,
+ normalizeTimeManagementDayHours,
+ createTimeManagementSavePayload,
+ formatTimeManagementHourPayloadValue,
+ getTimeManagementDayKey,
+ getTimeManagementWeekdayName,
+ isNonServiceRequestSrNumber,
+ };
+ }
+
+ globalThis.ArchPanelTimeManagementDomain = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/domain/workload-calendar.js b/src/domain/workload-calendar.js
new file mode 100644
index 0000000..0366d5d
--- /dev/null
+++ b/src/domain/workload-calendar.js
@@ -0,0 +1,107 @@
+(() => {
+ function create({
+ getLocalDateKey,
+ getOperationalWeekInfo,
+ getCalendarRowWeekDate,
+ formatOperationalWeekLabel,
+ formatDate,
+ parseDatePreservingDateOnly,
+ }) {
+ function buildWorkloadStartCalendar(
+ workloads,
+ year,
+ month,
+ view = "month",
+ anchorDateKey = ""
+ ) {
+ const today = new Date();
+ const normalizedView = view === "week" ? "week" : "month";
+ const monthStart = new Date(year, month, 1);
+ const anchorDate = getCalendarAnchorDate(year, month, anchorDateKey);
+ const calendarStart =
+ normalizedView === "week"
+ ? getWeekStartDate(anchorDate)
+ : getWeekStartDate(monthStart);
+ const dayCount = normalizedView === "week" ? 7 : 42;
+ const workloadMap = new Map();
+
+ for (const workload of workloads || []) {
+ const dateKey = getLocalDateKey(workload.consumptionStartDate);
+
+ if (!dateKey) {
+ continue;
+ }
+
+ const bucket = workloadMap.get(dateKey) || [];
+ bucket.push(workload);
+ workloadMap.set(dateKey, bucket);
+ }
+
+ const days = Array.from({ length: dayCount }, (_, index) => {
+ const date = new Date(calendarStart);
+ date.setDate(calendarStart.getDate() + index);
+ const dateKey = getLocalDateKey(date);
+ const workloadsForDay = workloadMap.get(dateKey) || [];
+
+ return {
+ dateKey,
+ dayNumber: date.getDate(),
+ isWeekStart: index % 7 === 0,
+ operationalWeekLabel: formatOperationalWeekLabel(
+ getOperationalWeekInfo(getCalendarRowWeekDate(date))
+ ),
+ isCurrentMonth: normalizedView === "week" || date.getMonth() === month,
+ isToday: dateKey === getLocalDateKey(today),
+ workloads: workloadsForDay,
+ };
+ });
+ const weekEnd = new Date(calendarStart);
+ weekEnd.setDate(calendarStart.getDate() + 6);
+ const monthLabel = new Intl.DateTimeFormat("en-US", {
+ month: "long",
+ year: "numeric",
+ }).format(monthStart);
+ const weekLabel = `${formatDate(calendarStart)} - ${formatDate(weekEnd)}`;
+
+ return {
+ view: normalizedView,
+ periodLabel: normalizedView === "week" ? weekLabel : monthLabel,
+ days,
+ totalVisibleWorkloads: days.reduce(
+ (sum, day) => sum + day.workloads.length,
+ 0
+ ),
+ };
+ }
+
+ function getCalendarAnchorDate(year, month, anchorDateKey) {
+ const anchorDate = parseDatePreservingDateOnly(anchorDateKey);
+
+ if (!Number.isNaN(anchorDate.getTime())) {
+ return anchorDate;
+ }
+
+ return new Date(year, month, 1);
+ }
+
+ function getWeekStartDate(value) {
+ const date = parseDatePreservingDateOnly(value);
+ const weekStart = Number.isNaN(date.getTime())
+ ? new Date()
+ : new Date(date);
+
+ weekStart.setDate(weekStart.getDate() - weekStart.getDay());
+ return weekStart;
+ }
+
+ return Object.freeze({
+ buildWorkloadStartCalendar,
+ getCalendarAnchorDate,
+ getWeekStartDate,
+ });
+ }
+
+ globalThis.ArchPanelWorkloadCalendarDomain = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/domain/workload-dataset.js b/src/domain/workload-dataset.js
new file mode 100644
index 0000000..28d01a7
--- /dev/null
+++ b/src/domain/workload-dataset.js
@@ -0,0 +1,324 @@
+(() => {
+ function create({
+ ALLOWED_STAGE_TYPES,
+ cleanString,
+ firstNonEmptyString,
+ normalizeString,
+ toNumber,
+ extractArray,
+ formatDateTime,
+ normalizePendingServiceRequests,
+ }) {
+ function normalizeWorkload(item) {
+ const workloadId = cleanString(item?.workloadId || item?.id);
+
+ if (!workloadId) {
+ return null;
+ }
+
+ const opportunityForecastType = item?.opportunityForecastType || "";
+ const opportunityForecastTypeGroup = normalizeForecastType(
+ opportunityForecastType
+ );
+
+ return {
+ workloadId,
+ name: item?.name || "",
+ description: item?.description || "",
+ consumptionStartDate: item?.consumptionStartDate || "",
+ workloadStatus: item?.workloadStatus || "",
+ opportunityForecastType,
+ opportunityForecastTypeGroup,
+ opportunityId: cleanString(item?.opportunityId),
+ opportunityNumber: item?.opportunityNumber || "",
+ adjustedACR: toNumber(item?.adjustedACR),
+ rampMonths: toNumber(item?.rampMonths),
+ forecast: extractArray(item?.forecast, [
+ "items",
+ "content",
+ "results",
+ "data",
+ ]).map((forecastItem) => ({
+ month: forecastItem?.month || "",
+ year: toNumber(forecastItem?.year),
+ projectedConsumptionAmount: toNumber(
+ forecastItem?.projectedConsumptionAmount
+ ),
+ adjustedConsumptionAmount: toNumber(
+ forecastItem?.adjustedConsumptionAmount
+ ),
+ createdBy: forecastItem?.createdBy || "",
+ createdDate: forecastItem?.createdDate || "",
+ updatedBy: forecastItem?.updatedBy || "",
+ updatedDate: forecastItem?.updatedDate || "",
+ })),
+ };
+ }
+
+ function isWonWorkload(workload) {
+ const forecastType = normalizeString(workload.opportunityForecastTypeGroup);
+ const workloadStatus = normalizeString(workload.workloadStatus);
+
+ return forecastType === "WON" || workloadStatus === "WON";
+ }
+
+ function normalizeForecastType(value) {
+ const text = String(value || "").trim();
+
+ if (!text) {
+ return "Unknown";
+ }
+
+ const [head] = text.split(" -");
+ return head.trim() || "Unknown";
+ }
+
+ function normalizeTeamValue(value) {
+ if (Array.isArray(value)) {
+ return value.map((item) => normalizeTeamValue(item)).join(" | ");
+ }
+
+ if (value && typeof value === "object") {
+ return Object.values(value)
+ .map((item) => normalizeTeamValue(item))
+ .join(" | ");
+ }
+
+ return String(value || "");
+ }
+
+ function teamContainsUser(team, userEmail) {
+ return normalizeString(team).includes(normalizeString(userEmail));
+ }
+
+ function stripTransientFields(workload) {
+ return {
+ workloadId: workload.workloadId,
+ name: workload.name,
+ description: workload.description,
+ consumptionStartDate: workload.consumptionStartDate,
+ workloadStatus: workload.workloadStatus,
+ opportunityForecastType: workload.opportunityForecastType,
+ opportunityForecastTypeGroup: workload.opportunityForecastTypeGroup,
+ opportunityId: workload.opportunityId,
+ opportunityNumber: workload.opportunityNumber,
+ adjustedACR: workload.adjustedACR,
+ rampMonths: workload.rampMonths,
+ forecast: workload.forecast,
+ customerId: workload.customerId,
+ customerName: workload.customerName,
+ actions: workload.actions,
+ serviceRequests: workload.serviceRequests,
+ hasSR: workload.hasSR,
+ hasAction: workload.hasAction,
+ };
+ }
+
+ function createExportPayload(
+ user,
+ customers,
+ pendingServiceRequests = [],
+ resourceCurrentUser = null
+ ) {
+ return {
+ generatedAt: new Date().toISOString(),
+ user,
+ resourceCurrentUser,
+ customers,
+ pendingServiceRequests,
+ };
+ }
+
+ function createDatasetSnapshot(exportPayload) {
+ const normalizedPayload = normalizeCachedExportPayload(exportPayload);
+
+ return {
+ user: normalizedPayload.user,
+ resourceCurrentUser: normalizedPayload.resourceCurrentUser,
+ customers: normalizedPayload.customers,
+ exportPayload: normalizedPayload,
+ summary: buildSummary(normalizedPayload),
+ };
+ }
+
+ function normalizeCachedExportPayload(exportPayload) {
+ const normalizedUser = {
+ userEmail: cleanString(exportPayload?.user?.userEmail),
+ name:
+ firstNonEmptyString([
+ exportPayload?.user?.name,
+ exportPayload?.user?.displayName,
+ exportPayload?.user?.fullName,
+ exportPayload?.user?.profile?.name,
+ exportPayload?.user?.userEmail,
+ ]) || "",
+ avatar: cleanString(
+ exportPayload?.user?.avatar || exportPayload?.user?.imageUrl
+ ),
+ administrator: Boolean(exportPayload?.user?.administrator),
+ };
+ const resourceCurrentUser = exportPayload?.resourceCurrentUser ?? null;
+ const normalizedCustomers = extractArray(exportPayload?.customers, [
+ "customers",
+ ])
+ .map((customer) => ({
+ customerId: cleanString(customer?.customerId || customer?.id),
+ name: cleanString(customer?.name),
+ workloads: extractArray(customer?.workloads, ["workloads"])
+ .map((workload) =>
+ stripTransientFields(normalizeWorkloadForCache(workload))
+ )
+ .filter((workload) => workload.workloadId),
+ }))
+ .filter((customer) => customer.customerId);
+ const pendingServiceRequests = normalizePendingServiceRequests(
+ extractArray(exportPayload?.pendingServiceRequests, [
+ "pendingServiceRequests",
+ "items",
+ "data",
+ "results",
+ ])
+ );
+
+ return {
+ generatedAt:
+ firstNonEmptyString([exportPayload?.generatedAt]) ||
+ new Date().toISOString(),
+ user: normalizedUser,
+ resourceCurrentUser,
+ customers: normalizedCustomers,
+ pendingServiceRequests,
+ };
+ }
+
+ function normalizeWorkloadForCache(workload) {
+ return {
+ workloadId: cleanString(workload?.workloadId),
+ name: cleanString(workload?.name),
+ description: cleanString(workload?.description),
+ consumptionStartDate: workload?.consumptionStartDate || "",
+ workloadStatus: cleanString(workload?.workloadStatus),
+ opportunityForecastType: cleanString(workload?.opportunityForecastType),
+ opportunityForecastTypeGroup:
+ cleanString(workload?.opportunityForecastTypeGroup) ||
+ normalizeForecastType(workload?.opportunityForecastType),
+ opportunityId: cleanString(workload?.opportunityId),
+ opportunityNumber: cleanString(workload?.opportunityNumber),
+ adjustedACR: toNumber(workload?.adjustedACR),
+ rampMonths: toNumber(workload?.rampMonths),
+ forecast: extractArray(workload?.forecast, ["forecast"]).map((entry) => ({
+ month: entry?.month || "",
+ year: entry?.year || "",
+ projectedConsumptionAmount: toNumber(entry?.projectedConsumptionAmount),
+ adjustedConsumptionAmount: toNumber(entry?.adjustedConsumptionAmount),
+ createdBy: entry?.createdBy || "",
+ createdDate: entry?.createdDate || "",
+ updatedBy: entry?.updatedBy || "",
+ updatedDate: entry?.updatedDate || "",
+ })),
+ customerId: cleanString(workload?.customerId),
+ customerName: cleanString(workload?.customerName),
+ actions: extractArray(workload?.actions, ["actions"]).map((action) => ({
+ owner: cleanString(action?.owner),
+ role: cleanString(action?.role),
+ name: cleanString(action?.name),
+ team: normalizeTeamValue(action?.team),
+ startDate: action?.startDate || "",
+ endDate: action?.endDate || "",
+ complexity: cleanString(action?.complexity),
+ })),
+ serviceRequests: extractArray(workload?.serviceRequests, [
+ "serviceRequests",
+ ]).map((serviceRequest) => ({
+ srNumber: cleanString(serviceRequest?.srNumber),
+ status: cleanString(serviceRequest?.status),
+ team: normalizeTeamValue(serviceRequest?.team),
+ })),
+ hasSR: Boolean(workload?.hasSR),
+ hasAction: Boolean(workload?.hasAction),
+ };
+ }
+
+ function buildSummary(exportPayload) {
+ const customers = exportPayload.customers || [];
+ const allWorkloads = customers.flatMap(
+ (customer) => customer.workloads || []
+ );
+ const eligibleWorkloads = allWorkloads
+ .filter((workload) =>
+ ALLOWED_STAGE_TYPES.includes(workload.opportunityForecastTypeGroup)
+ )
+ .sort((left, right) => right.adjustedACR - left.adjustedACR);
+ const totalEligibleAcr = eligibleWorkloads.reduce((sum, workload) => {
+ return sum + workload.adjustedACR;
+ }, 0);
+ const stageIndicators = ALLOWED_STAGE_TYPES.map((label) => {
+ const items = eligibleWorkloads.filter(
+ (workload) => workload.opportunityForecastTypeGroup === label
+ );
+
+ return {
+ label,
+ count: items.length,
+ totalAcr: items.reduce(
+ (sum, workload) => sum + workload.adjustedACR,
+ 0
+ ),
+ };
+ });
+ const srStatus = {
+ trueCount: eligibleWorkloads.filter((workload) => workload.hasSR).length,
+ falseCount: eligibleWorkloads.filter((workload) => !workload.hasSR)
+ .length,
+ };
+ const actionStatus = {
+ trueCount: eligibleWorkloads.filter((workload) => workload.hasAction)
+ .length,
+ falseCount: eligibleWorkloads.filter((workload) => !workload.hasAction)
+ .length,
+ };
+ const pendingServiceRequests = exportPayload.pendingServiceRequests || [];
+ const srHours = {
+ betweenZeroAndFour: pendingServiceRequests.filter((serviceRequest) => {
+ const hours = toNumber(serviceRequest.totalHoursWorked);
+ return hours >= 0 && hours <= 4;
+ }),
+ aboveFour: pendingServiceRequests.filter((serviceRequest) => {
+ return toNumber(serviceRequest.totalHoursWorked) > 4;
+ }),
+ };
+
+ return {
+ generatedAtLabel: formatDateTime(exportPayload.generatedAt),
+ totalCustomers: customers.length,
+ totalWorkloads: allWorkloads.length,
+ allWorkloads,
+ eligibleWorkloads,
+ totalEligibleAcr,
+ stageIndicators,
+ srStatus,
+ actionStatus,
+ pendingServiceRequests,
+ srHours,
+ };
+ }
+
+ return Object.freeze({
+ normalizeWorkload,
+ isWonWorkload,
+ normalizeForecastType,
+ normalizeTeamValue,
+ teamContainsUser,
+ stripTransientFields,
+ createExportPayload,
+ createDatasetSnapshot,
+ normalizeCachedExportPayload,
+ normalizeWorkloadForCache,
+ buildSummary,
+ });
+ }
+
+ globalThis.ArchPanelWorkloadDatasetDomain = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/repositories/cache-repository.js b/src/repositories/cache-repository.js
new file mode 100644
index 0000000..5ce9f68
--- /dev/null
+++ b/src/repositories/cache-repository.js
@@ -0,0 +1,102 @@
+(() => {
+ function create({
+ CACHE,
+ storageService,
+ normalizeCachedExportPayload,
+ cleanString,
+ }) {
+ async function readCachedDataset() {
+ const record = await storageService.readRecord(
+ CACHE,
+ CACHE.datasetKey,
+ "Unable to read the cached dataset."
+ );
+ const payload =
+ record?.exportPayload ||
+ record?.payload ||
+ record?.dataset?.exportPayload ||
+ record?.dataset ||
+ null;
+
+ return payload ? normalizeCachedExportPayload(payload) : null;
+ }
+
+ async function writeCachedDataset(exportPayload) {
+ const normalizedPayload = normalizeCachedExportPayload(exportPayload);
+
+ await storageService.writeRecord(
+ CACHE,
+ {
+ key: CACHE.datasetKey,
+ updatedAt: new Date().toISOString(),
+ exportPayload: normalizedPayload,
+ },
+ "Unable to write the cached dataset."
+ );
+ }
+
+ async function readCachedTaskTypePayload() {
+ const record = await storageService.readRecord(
+ CACHE,
+ CACHE.taskTypeKey,
+ "Unable to read the cached task types."
+ );
+
+ return record?.payload || null;
+ }
+
+ async function writeCachedTaskTypePayload(payload) {
+ await storageService.writeRecord(
+ CACHE,
+ {
+ key: CACHE.taskTypeKey,
+ updatedAt: new Date().toISOString(),
+ payload,
+ },
+ "Unable to write the cached task types."
+ );
+ }
+
+ async function writeCachedWorklistToken(tokenPayload) {
+ const token = cleanString(tokenPayload?.token);
+
+ if (!token) {
+ return;
+ }
+
+ await storageService.writeRecord(
+ CACHE,
+ {
+ key: CACHE.worklistTokenKey,
+ updatedAt: new Date().toISOString(),
+ token,
+ sourceUrl: cleanString(tokenPayload?.url),
+ },
+ "Unable to write the cached Worklist token."
+ );
+ }
+
+ async function readCachedWorklistToken() {
+ const record = await storageService.readRecord(
+ CACHE,
+ CACHE.worklistTokenKey,
+ "Unable to read the cached Worklist token."
+ );
+
+ return cleanString(record?.token);
+ }
+
+ return Object.freeze({
+ readCachedDataset,
+ writeCachedDataset,
+ readCachedTaskTypePayload,
+ writeCachedTaskTypePayload,
+ writeCachedWorklistToken,
+ readCachedWorklistToken,
+ });
+ }
+
+ globalThis.ArchPanelCacheRepository = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/repositories/comcip-repository.js b/src/repositories/comcip-repository.js
new file mode 100644
index 0000000..65a71c4
--- /dev/null
+++ b/src/repositories/comcip-repository.js
@@ -0,0 +1,438 @@
+(() => {
+ function create(dependencies) {
+ const {
+ COMCIP_REQUEST,
+ sendComcipRequest,
+ cleanString,
+ firstNonEmptyString,
+ extractArray,
+ getFieldValue,
+ toNumber,
+ formatDateForTimeEntryRequest,
+ } = dependencies;
+
+ async function fetchPendingServiceRequests() {
+ const response = await sendComcipRequest({
+ method: "POST",
+ url: COMCIP_REQUEST.url,
+ payload: createComcipServiceRequestPayload(),
+ timeoutMessage: "Timed out while requesting COMCIP data.",
+ });
+
+ assertComcipResponse(response, "COMCIP");
+
+ return normalizePendingServiceRequests(response.payload);
+ }
+
+ async function fetchResourceCurrentUser(userEmail) {
+ const normalizedUserEmail = cleanString(userEmail);
+
+ if (!normalizedUserEmail) {
+ throw new Error("Unable to request resource current user without userEmail.");
+ }
+
+ const url = `${COMCIP_REQUEST.resourceCurrentUserUrl}?emailAddress=${encodeURIComponent(
+ normalizedUserEmail
+ )}`;
+
+ return fetchComcipPayload(url, "resourceCurrentUser");
+ }
+
+ async function fetchTimeEntriesSummary(srNumber) {
+ const url = `${COMCIP_REQUEST.timeEntriesSummaryUrl}?srNumber=${encodeURIComponent(
+ srNumber
+ )}`;
+ const response = await sendGet(url);
+
+ assertComcipResponse(response, "timeEntriesSummary");
+
+ return response;
+ }
+
+ 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) {
+ const activityRequestUrl = buildActivityRequestUrl(srNumbers);
+ const activityPayload = await fetchComcipPayload(
+ activityRequestUrl,
+ "activityRequest"
+ );
+
+ return {
+ url: activityRequestUrl,
+ payload: activityPayload,
+ };
+ }
+
+ 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 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 encodeActivityRequestParam(value) {
+ return encodeURIComponent(String(value || ""))
+ .replace(/'/g, "%27")
+ .replace(/\(/g, "%28")
+ .replace(/\)/g, "%29");
+ }
+
+ function fetchTaskTypePayload() {
+ return fetchComcipPayload(COMCIP_REQUEST.taskTypeUrl, "taskTypeRequest");
+ }
+
+ async function mutateComcipPayload(method, url, payload, requestName) {
+ const response = await sendComcipRequest({
+ method,
+ url,
+ payload,
+ timeoutMessage: `Timed out while executing ${requestName}.`,
+ });
+
+ assertComcipResponse(response, requestName);
+
+ return response.payload;
+ }
+
+ async function fetchComcipPayload(url, requestName) {
+ const response = await sendGet(url);
+
+ assertComcipResponse(response, requestName);
+
+ return response.payload;
+ }
+
+ function sendGet(url) {
+ return sendComcipRequest({
+ method: "GET",
+ url,
+ payload: null,
+ timeoutMessage: "Timed out while requesting COMCIP data.",
+ });
+ }
+
+ function assertComcipResponse(response, requestName) {
+ if (response?.ok) {
+ return;
+ }
+
+ 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}).`
+ }`
+ );
+ }
+
+ function createComcipServiceRequestPayload() {
+ return {
+ entity: "CRMServiceRequest",
+ offset: 0,
+ limit: 100,
+ onlyData: true,
+ language: "US",
+ fields: [
+ "SrNumber",
+ "Title",
+ "LastUpdateDate",
+ "CreationDate",
+ "PrimaryPillarNew_c",
+ "PrimaryPillarNew_c_localizedValue",
+ "CountryText_c",
+ "OpportunityName_Id_c.OptyId",
+ "OpportunityName_Id_c.Name",
+ "OpportunityName_Id_c.OptyNumber",
+ "OpportunityName_Id_c.OpportunityType_c",
+ "OpportunityName_Id_c.StatusCode",
+ "OpportunityName_Id_c.EffectiveDate",
+ "OpportunityName_Id_c.StatusCode_localizedValue",
+ "OpportunityName_Id_c.PrimaryRevenue.WinProb",
+ "OpportunityName_Id_c.PrimaryRevenue.RevnAmount",
+ "OpportunityName_Id_c.PrimaryRevenue.CrmConversionRate",
+ "OpportunityName_Id_c.WorkloadAmountUSDWorkspace_c",
+ "CustomerAccount.PartyId",
+ "CustomerAccount.PartyUniqueName",
+ "CustomerAccount.PartyNumber",
+ "CustomerAccount.NamedFlag",
+ "StatusCd",
+ "StatusCd_localizedValue",
+ "ServiceNew_Id_c",
+ "CustomerDeadline_c",
+ "Owner.PartyId",
+ "Owner.PartyName",
+ "Owner.PrimaryEmail.EmailAddress",
+ "Owner.ResourceEmailAddress",
+ "ReportedByPartyId.PrimaryEmail.EmailAddress",
+ "Queue.QueueId",
+ "Queue.QueueName",
+ "ServiceRequest_Id_SRToInternalSR",
+ "ReportedHours_c",
+ "ProblemDescription",
+ "AccountRegistryId_c",
+ ],
+ sort: [
+ {
+ attribute: "LastUpdateDate",
+ direction: "descending",
+ },
+ ],
+ q: {
+ op: "$and",
+ criteria: [
+ {
+ op: "$eq",
+ attribute: "RecordSet",
+ value: "ORA_SVC_MY_SUBORD_SR_TEAM_SRS",
+ },
+ {
+ op: "$in",
+ attribute: "StatusCd",
+ values: ["SVC_PENDING_DELIVERY"],
+ },
+ ],
+ },
+ aggregations: {
+ StatusCd: {
+ ignore: true,
+ terms: {
+ attribute: "StatusCd",
+ includeTerms: [
+ "SVC_PENDING_ACCEPTANCE",
+ "SVC_PENDING_DELIVERY",
+ "SVC_PENDING_RESOURCING",
+ "SVC_SE_MGR_CLOSED",
+ "SVC_REJECT",
+ ],
+ excludeTerms: ["SVC_PENDING_DISPATCHER", "ORA_SVC_NEW", "SVC_REJECT"],
+ minBucketCount: 1,
+ maxNumberOfBuckets: 5,
+ other: false,
+ missing: false,
+ localize: true,
+ },
+ },
+ Queue: createTermsAggregation("Queue", 10),
+ CustomerAccount: createTermsAggregation("CustomerAccount", 5),
+ OpportunityStatus: createTermsAggregation(
+ "OpportunityName_Id_c.StatusCode",
+ 6
+ ),
+ Service: createTermsAggregation("ServiceNew_Id_c", 5),
+ Country: createTermsAggregation("CountryText_c", 5),
+ Pillar: createTermsAggregation("PrimaryPillarNew_c", 5),
+ WinProb: {
+ ignore: true,
+ range: {
+ attribute: "OpportunityName_Id_c.PrimaryRevenue.WinProb",
+ ranges: [
+ { key: "0-10 % SQL", from: 0, to: 11 },
+ { key: "20-30 % Pipeline", from: 20, to: 31 },
+ { key: "40-50 % Upside", from: 40, to: 51 },
+ { key: "60-90 % Forecast", from: 60, to: 91 },
+ { key: "100 % Won", from: 100, to: 101 },
+ ],
+ },
+ },
+ Amount: {
+ ignore: true,
+ range: {
+ attribute: "OpportunityName_Id_c.PrimaryRevenue.RevnAmount",
+ ranges: [
+ { key: "0-10,000", from: 0, to: 10000 },
+ { key: "10,000-50,000", from: 10000, to: 50000 },
+ { key: "50,000-100,000", from: 50000, to: 100000 },
+ { key: ">=100,000", from: 100000 },
+ ],
+ },
+ },
+ OpportunityName_Id_c: createTermsAggregation("OpportunityName_Id_c", 5),
+ },
+ };
+ }
+
+ function createTermsAggregation(attribute, maxNumberOfBuckets) {
+ return {
+ ignore: true,
+ terms: {
+ attribute,
+ includeTerms: [],
+ minBucketCount: 1,
+ maxNumberOfBuckets,
+ other: false,
+ missing: false,
+ localize: true,
+ },
+ };
+ }
+
+ function normalizePendingServiceRequests(payload) {
+ const items = Array.isArray(payload)
+ ? payload
+ : extractArray(payload, ["items", "data", "results", "content"]);
+
+ return items
+ .map((item) => normalizePendingServiceRequest(item))
+ .filter((item) => item.srNumber || item.customerName || item.lastUpdateDate)
+ .sort((left, right) => {
+ return (
+ new Date(right.lastUpdateDate || 0).getTime() -
+ new Date(left.lastUpdateDate || 0).getTime()
+ );
+ });
+ }
+
+ function normalizePendingServiceRequest(item) {
+ return {
+ customerName: cleanString(
+ getFieldValue(item, [
+ "CustomerAccount.PartyUniqueName",
+ "CustomerAccount.PartyName",
+ "CustomerAccount.Name",
+ "customerName",
+ ])
+ ),
+ lastUpdateDate: cleanString(
+ getFieldValue(item, ["LastUpdateDate", "lastUpdateDate"])
+ ),
+ srNumber: cleanString(getFieldValue(item, ["SrNumber", "srNumber"])),
+ queueName: cleanString(
+ getFieldValue(item, ["Queue.QueueName", "QueueName", "queueName"])
+ ),
+ requestedBy: cleanString(
+ getFieldValue(item, [
+ "ReportedByPartyId.PrimaryEmail.EmailAddress",
+ "ReportedByPartyId.EmailAddress",
+ "requestedBy",
+ ])
+ ),
+ creationDate: cleanString(
+ getFieldValue(item, ["CreationDate", "creationDate"])
+ ),
+ opportunityNumber: cleanString(
+ getFieldValue(item, [
+ "OpportunityName_Id_c.OptyNumber",
+ "OptyNumber",
+ "opportunityNumber",
+ ])
+ ),
+ srStatusLabel: cleanString(
+ getFieldValue(item, [
+ "StatusCd_localizedValue",
+ "StatusCd",
+ "srStatusLabel",
+ ])
+ ),
+ opportunityStatusLabel: cleanString(
+ getFieldValue(item, [
+ "OpportunityName_Id_c.StatusCode_localizedValue",
+ "OpportunityName_Id_c.StatusCode",
+ "opportunityStatusLabel",
+ ])
+ ),
+ serviceName: cleanString(
+ getFieldValue(item, [
+ "ServiceNew_Id_c.RecordName",
+ "ServiceNew_Id_c",
+ "serviceName",
+ ])
+ ),
+ country: cleanString(getFieldValue(item, ["CountryText_c", "country"])),
+ totalHoursWorked: toNumber(
+ getFieldValue(item, ["ReportedHours_c", "totalHoursWorked"])
+ ),
+ customerDeadline: cleanString(
+ getFieldValue(item, ["CustomerDeadline_c", "customerDeadline"])
+ ),
+ opportunityValue: toNumber(
+ getFieldValue(item, [
+ "WorkloadAmountUSDWorkspace_c",
+ "OpportunityName_Id_c.WorkloadAmountUSDWorkspace_c",
+ "OpportunityName_Id_c.PrimaryRevenue.RevnAmount",
+ "opportunityValue",
+ ])
+ ),
+ pillarLabel: cleanString(
+ getFieldValue(item, [
+ "PrimaryPillarNew_c_localizedValue",
+ "PrimaryPillarNew_c",
+ "pillarLabel",
+ ])
+ ),
+ };
+ }
+
+ return {
+ fetchPendingServiceRequests,
+ fetchResourceCurrentUser,
+ fetchTimeEntriesSummary,
+ fetchInfoWeekRequest,
+ fetchActiveServiceRequests,
+ buildTimeEntryWeekUrl,
+ fetchTimeEntryWeek,
+ fetchActivityRequest,
+ fetchTaskTypePayload,
+ mutateComcipPayload,
+ normalizePendingServiceRequests,
+ };
+ }
+
+ globalThis.ArchPanelComcipRepository = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/repositories/workbench-repository.js b/src/repositories/workbench-repository.js
new file mode 100644
index 0000000..c0c311b
--- /dev/null
+++ b/src/repositories/workbench-repository.js
@@ -0,0 +1,292 @@
+(() => {
+ function create(dependencies) {
+ const {
+ TARGET_ORIGIN,
+ API_ENDPOINTS,
+ fetchJson,
+ extractArray,
+ cleanString,
+ firstNonEmptyString,
+ getFieldValue,
+ toNumber,
+ normalizeWorkload,
+ isWonWorkload,
+ normalizeTeamValue,
+ } = dependencies;
+
+ async function fetchWorklistCurrentUser(worklistToken) {
+ const normalizedToken = cleanString(worklistToken);
+
+ if (!normalizedToken) {
+ throw new Error(
+ "Unable to resolve Worklist token. Reload the FuseWelcome page so the Worklist iframe token can be captured before refreshing data."
+ );
+ }
+
+ const url = new URL(API_ENDPOINTS.worklistCurrentUser);
+ url.search = new URLSearchParams({
+ source: "saasui",
+ _: String(Date.now()),
+ token: normalizedToken,
+ }).toString();
+
+ const payload = await fetchJson(url.toString(), { forceExtension: true });
+ const userPayload = normalizeWorklistUserPayload(payload);
+ const firstName = cleanString(
+ getFieldValue(userPayload, ["FirstName", "firstName", "User.FirstName"])
+ );
+ const lastName = cleanString(
+ getFieldValue(userPayload, ["LastName", "lastName", "User.LastName"])
+ );
+ const userEmail = cleanString(
+ getFieldValue(userPayload, [
+ "Email",
+ "email",
+ "EmailAddress",
+ "emailAddress",
+ "User.Email",
+ "User.email",
+ "User.EmailAddress",
+ ])
+ );
+ const displayName = firstNonEmptyString([
+ `${firstName} ${lastName}`.trim(),
+ userPayload?.DisplayName,
+ userPayload?.displayName,
+ userEmail,
+ ]);
+
+ if (!userEmail) {
+ const userKeys =
+ userPayload && typeof userPayload === "object"
+ ? Object.keys(userPayload).join(", ")
+ : "";
+ throw new Error(
+ `Unable to resolve userEmail from Worklist user response. Available top-level keys: ${Object.keys(
+ payload || {}
+ ).join(", ")}${userKeys ? ` | User keys: ${userKeys}` : ""}`
+ );
+ }
+
+ return {
+ userEmail,
+ name: displayName || userEmail,
+ avatar: cleanString(
+ getFieldValue(userPayload, ["ImageURL", "imageUrl", "User.ImageURL"])
+ ),
+ administrator: Boolean(
+ getFieldValue(userPayload, [
+ "Administrator",
+ "administrator",
+ "User.Administrator",
+ ])
+ ),
+ };
+ }
+
+ function normalizeWorklistUserPayload(payload) {
+ const candidate = payload?.User ?? payload?.user ?? payload;
+
+ if (Array.isArray(candidate)) {
+ return candidate[0] || {};
+ }
+
+ if (candidate?.User) {
+ return normalizeWorklistUserPayload(candidate);
+ }
+
+ return candidate || {};
+ }
+
+ async function fetchAllCustomers(userEmail) {
+ const limit = 49;
+ let offset = 0;
+ const customers = [];
+ const seen = new Set();
+
+ while (true) {
+ const url = new URL(API_ENDPOINTS.customerSummary, TARGET_ORIGIN);
+ url.search = new URLSearchParams({
+ emailAddress: userEmail,
+ projectionPeriod: "ALL",
+ recordSet: "IAM_ON_SR_TEAM",
+ growth: "All",
+ projectedGrowth: "All",
+ attainmentBand: "All",
+ projectedConsumption: "All",
+ customerStatus: "All",
+ workloadStatus: "All",
+ lastProjectionUpdate: "All",
+ revenueForecastType: "All",
+ customerProgramCode: "All",
+ planType: "All",
+ workLoadLivePeriod: "All",
+ activeConsumptionOnly: "false",
+ favorite: "false",
+ workloadDeleted: "false",
+ awsFlag: "N",
+ googleFlag: "N",
+ azureFlag: "N",
+ excludePayAsYouGoOnly: "false",
+ projectionReview: "N",
+ pillarFilter: "dpOci",
+ sortBy: "CUSTOMER_NAME",
+ sortOrder: "ASC",
+ limit: String(limit),
+ offset: String(offset),
+ }).toString();
+
+ const payload = await fetchJson(url.toString());
+ const items = extractArray(payload, [
+ "items",
+ "customers",
+ "content",
+ "results",
+ "records",
+ "data",
+ ]);
+ const mapped = items
+ .map((item) => ({
+ customerId: cleanString(item?.id),
+ name: item?.name || "",
+ }))
+ .filter((item) => item.customerId);
+
+ for (const item of mapped) {
+ if (!seen.has(item.customerId)) {
+ seen.add(item.customerId);
+ customers.push(item);
+ }
+ }
+
+ if (mapped.length < limit) {
+ break;
+ }
+
+ offset += limit;
+ }
+
+ return customers;
+ }
+
+ async function fetchCustomerWorkloads(customer) {
+ const url = new URL(
+ `${API_ENDPOINTS.customerWorkloads}/${encodeURIComponent(
+ customer.customerId
+ )}/workloads`,
+ TARGET_ORIGIN
+ );
+ url.search = new URLSearchParams({
+ workloadStatus: "Pipeline,Implementation,Live",
+ opportunityForecastType: "All",
+ includeForEstimates: "ALL",
+ financialYear: "ROLLING_4_QTRS",
+ }).toString();
+
+ const payload = await fetchJson(url.toString());
+ const items = extractArray(payload, [
+ "items",
+ "workloads",
+ "content",
+ "results",
+ "data",
+ ]);
+
+ return items
+ .map((item) => normalizeWorkload(item))
+ .filter(Boolean)
+ .filter((workload) => !isWonWorkload(workload));
+ }
+
+ async function fetchWorkloadActions(workloadId) {
+ const url = new URL(API_ENDPOINTS.workloadActions, TARGET_ORIGIN);
+ url.search = new URLSearchParams({
+ parentType: "WORKLOAD",
+ parentId: workloadId,
+ }).toString();
+
+ const payload = await fetchJson(url.toString());
+ const items = extractArray(payload, [
+ "items",
+ "actions",
+ "content",
+ "results",
+ "data",
+ ]);
+
+ return items.map((item) => ({
+ owner: item?.owner || "",
+ role: item?.role || "",
+ name: item?.name || "",
+ team: normalizeTeamValue(item?.team),
+ startDate: item?.startDate || "",
+ endDate: item?.endDate || "",
+ complexity: item?.complexity || "",
+ }));
+ }
+
+ async function fetchWorkloadServiceRequests(workloadId) {
+ const url = new URL(
+ `${API_ENDPOINTS.workloadServiceRequests}/${encodeURIComponent(
+ workloadId
+ )}/serviceRequests`,
+ TARGET_ORIGIN
+ );
+
+ const payload = await fetchJson(url.toString());
+ const items = extractArray(payload, [
+ "items",
+ "serviceRequests",
+ "content",
+ "results",
+ "data",
+ ]);
+
+ return items.map((item) => ({
+ srNumber: item?.srNumber || "",
+ status: item?.status || "",
+ team: normalizeTeamValue(item?.team),
+ }));
+ }
+
+ async function createWorkloadAction(payload) {
+ return fetchJson(API_ENDPOINTS.workloadActions, {
+ method: "POST",
+ headers: {
+ "content-type": "application/json; charset=UTF-8",
+ },
+ body: JSON.stringify(payload),
+ });
+ }
+
+ async function updateWorkloadForecast(workloadId, payload) {
+ const url = `${API_ENDPOINTS.workloadServiceRequests}/${encodeURIComponent(
+ workloadId
+ )}/forecast`;
+
+ return fetchJson(url, {
+ method: "PATCH",
+ omitAuthHeaders: true,
+ headers: {
+ accept: "*/*",
+ "content-type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
+ }
+
+ return {
+ fetchWorklistCurrentUser,
+ fetchAllCustomers,
+ fetchCustomerWorkloads,
+ fetchWorkloadActions,
+ fetchWorkloadServiceRequests,
+ createWorkloadAction,
+ updateWorkloadForecast,
+ };
+ }
+
+ globalThis.ArchPanelWorkbenchRepository = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/services/chrome-service.js b/src/services/chrome-service.js
new file mode 100644
index 0000000..37bd89b
--- /dev/null
+++ b/src/services/chrome-service.js
@@ -0,0 +1,25 @@
+(() => {
+ function sendMessage(message) {
+ return new Promise((resolve, reject) => {
+ chrome.runtime.sendMessage(message, (response) => {
+ const runtimeError = chrome.runtime.lastError;
+
+ if (runtimeError) {
+ reject(new Error(runtimeError.message));
+ return;
+ }
+
+ resolve(response);
+ });
+ });
+ }
+
+ function getExtensionUrl(path) {
+ return chrome.runtime.getURL(path);
+ }
+
+ globalThis.ArchPanelChromeService = Object.freeze({
+ sendMessage,
+ getExtensionUrl,
+ });
+})();
diff --git a/src/services/preferences-service.js b/src/services/preferences-service.js
new file mode 100644
index 0000000..2c6d37e
--- /dev/null
+++ b/src/services/preferences-service.js
@@ -0,0 +1,122 @@
+(() => {
+ function create({
+ STORAGE_KEYS,
+ getDateInputValue,
+ parseDatePreservingDateOnly,
+ }) {
+ function getStoredTheme() {
+ try {
+ const storedTheme = window.localStorage.getItem(STORAGE_KEYS.theme);
+
+ return storedTheme === "dark" ? "dark" : "light";
+ } catch {
+ return "light";
+ }
+ }
+
+ function setStoredTheme(theme) {
+ try {
+ window.localStorage.setItem(STORAGE_KEYS.theme, theme);
+ } catch {
+ // Keep in-memory UI state when browser storage is unavailable.
+ }
+ }
+
+ function getStoredSidebarCollapsed() {
+ try {
+ return (
+ window.localStorage.getItem(STORAGE_KEYS.sidebarCollapsed) === "true"
+ );
+ } catch {
+ return false;
+ }
+ }
+
+ function setStoredSidebarCollapsed(value) {
+ try {
+ window.localStorage.setItem(
+ STORAGE_KEYS.sidebarCollapsed,
+ String(Boolean(value))
+ );
+ } catch {
+ // Keep in-memory UI state when browser storage is unavailable.
+ }
+ }
+
+ function getStoredCalendarView() {
+ try {
+ return window.localStorage.getItem(STORAGE_KEYS.calendarView) === "week"
+ ? "week"
+ : "month";
+ } catch {
+ return "month";
+ }
+ }
+
+ function setStoredCalendarView(value) {
+ try {
+ window.localStorage.setItem(
+ STORAGE_KEYS.calendarView,
+ value === "week" ? "week" : "month"
+ );
+ } catch {
+ // Keep in-memory UI state when browser storage is unavailable.
+ }
+ }
+
+ function getStoredTimeManagementSelectedDate() {
+ try {
+ const storedDate = window.localStorage.getItem(
+ STORAGE_KEYS.timeManagementSelectedDate
+ );
+
+ return isValidDateInputValue(storedDate)
+ ? storedDate
+ : getDateInputValue(new Date());
+ } catch {
+ return getDateInputValue(new Date());
+ }
+ }
+
+ function setStoredTimeManagementSelectedDate(value) {
+ if (!isValidDateInputValue(value)) {
+ return;
+ }
+
+ try {
+ window.localStorage.setItem(
+ STORAGE_KEYS.timeManagementSelectedDate,
+ value
+ );
+ } catch {
+ // Keep in-memory selected date when browser storage is unavailable.
+ }
+ }
+
+ function isValidDateInputValue(value) {
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(value || ""))) {
+ return false;
+ }
+
+ const parsedDate = parseDatePreservingDateOnly(value);
+
+ return !Number.isNaN(parsedDate.getTime());
+ }
+
+ return Object.freeze({
+ getStoredTheme,
+ setStoredTheme,
+ getStoredSidebarCollapsed,
+ setStoredSidebarCollapsed,
+ getStoredCalendarView,
+ setStoredCalendarView,
+ getStoredTimeManagementSelectedDate,
+ setStoredTimeManagementSelectedDate,
+ isValidDateInputValue,
+ });
+ }
+
+ globalThis.ArchPanelPreferencesService = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/services/storage-service.js b/src/services/storage-service.js
new file mode 100644
index 0000000..4cad5b1
--- /dev/null
+++ b/src/services/storage-service.js
@@ -0,0 +1,95 @@
+(() => {
+ function openDatabase(cacheConfig) {
+ return new Promise((resolve, reject) => {
+ if (!globalThis.indexedDB) {
+ reject(new Error("IndexedDB is not available in this browser context."));
+ return;
+ }
+
+ const request = globalThis.indexedDB.open(
+ cacheConfig.dbName,
+ cacheConfig.dbVersion
+ );
+
+ request.onupgradeneeded = () => {
+ const database = request.result;
+
+ if (!database.objectStoreNames.contains(cacheConfig.storeName)) {
+ database.createObjectStore(cacheConfig.storeName, { keyPath: "key" });
+ }
+ };
+
+ request.onsuccess = () => {
+ resolve(request.result);
+ };
+
+ request.onerror = () => {
+ reject(request.error || new Error("Unable to open IndexedDB cache."));
+ };
+ });
+ }
+
+ async function withStore(cacheConfig, mode, callback) {
+ const database = await openDatabase(cacheConfig);
+
+ return new Promise((resolve, reject) => {
+ const transaction = database.transaction(cacheConfig.storeName, mode);
+ const store = transaction.objectStore(cacheConfig.storeName);
+
+ transaction.oncomplete = () => {
+ database.close();
+ };
+
+ transaction.onerror = () => {
+ const error =
+ transaction.error || new Error("IndexedDB transaction failed.");
+ database.close();
+ reject(error);
+ };
+
+ transaction.onabort = () => {
+ const error =
+ transaction.error || new Error("IndexedDB transaction was aborted.");
+ database.close();
+ reject(error);
+ };
+
+ callback(store, resolve, reject);
+ });
+ }
+
+ function readRecord(cacheConfig, key, errorMessage) {
+ return withStore(cacheConfig, "readonly", (store, resolve, reject) => {
+ const request = store.get(key);
+
+ request.onsuccess = () => {
+ resolve(request.result || null);
+ };
+
+ request.onerror = () => {
+ reject(request.error || new Error(errorMessage || "Unable to read cache."));
+ };
+ });
+ }
+
+ function writeRecord(cacheConfig, record, errorMessage) {
+ return withStore(cacheConfig, "readwrite", (store, resolve, reject) => {
+ const request = store.put(record);
+
+ request.onsuccess = () => {
+ resolve();
+ };
+
+ request.onerror = () => {
+ reject(request.error || new Error(errorMessage || "Unable to write cache."));
+ };
+ });
+ }
+
+ globalThis.ArchPanelStorageService = Object.freeze({
+ openDatabase,
+ withStore,
+ readRecord,
+ writeRecord,
+ });
+})();
diff --git a/src/services/transport-service.js b/src/services/transport-service.js
new file mode 100644
index 0000000..a9ffb02
--- /dev/null
+++ b/src/services/transport-service.js
@@ -0,0 +1,709 @@
+(() => {
+ function create({
+ TARGET_ORIGIN,
+ TARGET_APP_URL,
+ IDS,
+ BRIDGE,
+ COMCIP_REQUEST,
+ COMCIP_TIME_ENTRY_FRAME_ID,
+ chromeService,
+ cleanString,
+ firstNonEmptyString,
+ isHcmWelcomePage,
+ getSessionAuthHeaders,
+ setSessionAuthHeaders,
+ }) {
+ let requestCounter = 0;
+ let bridgeReadyPromise = null;
+ const pendingBridgeRequests = new Map();
+
+ function ensurePageBridge() {
+ if (bridgeReadyPromise) {
+ return bridgeReadyPromise;
+ }
+
+ bridgeReadyPromise = new Promise((resolve) => {
+ const existingScript = document.getElementById(BRIDGE.scriptId);
+
+ if (existingScript) {
+ resolve();
+ return;
+ }
+
+ const script = document.createElement("script");
+ script.id = BRIDGE.scriptId;
+ script.src = chromeService.getExtensionUrl("page-bridge.js");
+ script.async = false;
+ script.onload = () => {
+ script.remove();
+ resolve();
+ };
+ script.onerror = () => {
+ resolve();
+ };
+
+ (document.head || document.documentElement).appendChild(script);
+ });
+
+ return bridgeReadyPromise;
+ }
+
+ 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.")
+ );
+ }, 130000);
+
+ chromeService
+ .sendMessage({
+ type: "ARCH_PANEL_COMCIP_REQUEST",
+ method,
+ url,
+ payload,
+ useTimeEntryFrame,
+ headers: {
+ "accept-language": navigator.language || "pt-BR",
+ "x-vb-application-version": COMCIP_REQUEST.appVersion,
+ },
+ })
+ .then((response) => {
+ window.clearTimeout(timeout);
+ resolve(response || null);
+ })
+ .catch((error) => {
+ window.clearTimeout(timeout);
+ reject(error);
+ });
+ });
+ }
+
+ 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);
+ });
+ }
+
+ async function fetchJson(url, options = {}) {
+ if (options.forceExtension) {
+ return fetchJsonViaExtension(url, options);
+ }
+
+ if (shouldUseSpaFrameFetch(url) && !options.forceExtension) {
+ return fetchJsonViaSpaFrame(url, options);
+ }
+
+ if (shouldUseExtensionFetch(url) && !options.forcePageBridge) {
+ return fetchJsonViaExtension(url, options);
+ }
+
+ await ensurePageBridge();
+
+ const requestId = `${Date.now()}-${++requestCounter}`;
+ const requestUrl = toBridgeRequestUrl(url);
+
+ const response = await new Promise((resolve, reject) => {
+ const timeout = window.setTimeout(() => {
+ pendingBridgeRequests.delete(requestId);
+ reject(new Error(`Timed out while requesting ${requestUrl}`));
+ }, 60000);
+
+ pendingBridgeRequests.set(requestId, {
+ resolve,
+ reject,
+ timeout,
+ });
+
+ window.postMessage(
+ {
+ source: BRIDGE.source,
+ type: BRIDGE.fetchType,
+ requestId,
+ url: requestUrl,
+ options: {
+ method: options.method || "GET",
+ transport: options.transport || "fetch",
+ referrer: options.referrer,
+ headers: {
+ accept: "application/json, text/plain, */*",
+ ...(options.headers || {}),
+ },
+ body: options.body,
+ },
+ },
+ window.location.origin
+ );
+ });
+
+ 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,
+ ]);
+ const debugHeaderKeys = Array.isArray(response.debug?.headerKeys)
+ ? response.debug.headerKeys.join(", ")
+ : "";
+ const debugScopeKeys = Array.isArray(response.debug?.scopeKeys)
+ ? response.debug.scopeKeys.join(", ")
+ : "";
+ const debugParts = [
+ debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
+ debugScopeKeys ? `scopes: ${debugScopeKeys}` : "",
+ ].filter(Boolean);
+ const debugSuffix =
+ debugParts.length > 0 ? ` | ${debugParts.join(" | ")}` : "";
+
+ throw new Error(
+ `${
+ payloadMessage ||
+ response.error ||
+ `Request failed (${response.status}) for ${requestUrl}`
+ }${debugSuffix}`
+ );
+ }
+
+ return response.payload;
+ }
+
+ function shouldUseExtensionFetch(url) {
+ const normalized = resolveRequestUrl(url);
+
+ if (!normalized) {
+ return false;
+ }
+
+ try {
+ const parsedUrl = new URL(normalized, window.location.origin);
+ return parsedUrl.origin !== window.location.origin;
+ } catch {
+ return false;
+ }
+ }
+
+ function shouldUseSpaFrameFetch(url) {
+ if (!isHcmWelcomePage()) {
+ return false;
+ }
+
+ const normalized = resolveRequestUrl(url);
+
+ if (!normalized) {
+ return false;
+ }
+
+ try {
+ return new URL(normalized).origin === TARGET_ORIGIN;
+ } catch {
+ return false;
+ }
+ }
+
+ async function fetchJsonViaSpaFrame(url, options = {}) {
+ const requestUrl = resolveRequestUrl(url);
+ const bridgeHeaders = await requestBridgeHeaders({
+ url: requestUrl,
+ headers: options.headers,
+ body: options.body,
+ includeAllAuth: true,
+ }).catch(() => ({}));
+
+ await ensureSpaFrame();
+
+ const response = await chromeService.sendMessage({
+ type: "ARCH_PANEL_SPA_FRAME_FETCH",
+ url: requestUrl,
+ options: {
+ method: options.method || "GET",
+ referrer: options.referrer,
+ headers: buildExtensionFetchHeaders(
+ requestUrl,
+ options,
+ bridgeHeaders || {}
+ ),
+ body: options.body,
+ },
+ });
+
+ if (!response || typeof response !== "object") {
+ throw new Error(`No response received for ${requestUrl}`);
+ }
+
+ if (
+ !response.ok &&
+ response.status === 0 &&
+ /SPA Workbench frame did not return a result/i.test(
+ cleanString(response.error)
+ )
+ ) {
+ throw new Error(
+ response.error || "SPA Workbench iframe did not return a result."
+ );
+ }
+
+ 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,
+ ]);
+ const debugHeaderKeys = Array.isArray(response.debug?.headerKeys)
+ ? response.debug.headerKeys.join(", ")
+ : "";
+ const debugFrameUrl = cleanString(response.debug?.frameUrl);
+ const debugUrl = cleanString(response.debug?.url);
+ const debugParts = [
+ debugUrl ? `url: ${debugUrl}` : "",
+ debugFrameUrl ? `frame: ${debugFrameUrl}` : "",
+ debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
+ ].filter(Boolean);
+ const debugSuffix =
+ debugParts.length > 0 ? ` | ${debugParts.join(" | ")}` : "";
+
+ throw new Error(
+ `${
+ payloadMessage ||
+ response.error ||
+ `Request failed (${response.status}) for ${requestUrl}`
+ }${debugSuffix}`
+ );
+ }
+
+ return response.payload;
+ }
+
+ function ensureSpaFrame() {
+ const frameHost = ensureSpaFrameHost();
+ const existingFrame = document.getElementById(IDS.spaFrame);
+ const frameUrl = TARGET_APP_URL;
+
+ if (existingFrame instanceof HTMLIFrameElement) {
+ if (existingFrame.parentElement !== frameHost) {
+ frameHost.appendChild(existingFrame);
+ }
+
+ if (shouldNavigateSpaFrame(existingFrame, frameUrl)) {
+ navigateSpaFrame(existingFrame, frameUrl);
+ }
+
+ return waitForFrameLoad(existingFrame);
+ }
+
+ const frame = document.createElement("iframe");
+ frame.id = IDS.spaFrame;
+ frame.name = IDS.spaFrame;
+ frame.dataset.archPanelFrame = "spa-workbench";
+ frame.setAttribute("aria-hidden", "true");
+ frame.setAttribute("referrerpolicy", "no-referrer-when-downgrade");
+ frame.tabIndex = -1;
+ Object.assign(frame.style, {
+ position: "absolute",
+ width: "1440px",
+ height: "1000px",
+ opacity: "0",
+ pointerEvents: "none",
+ border: "0",
+ left: "0",
+ top: "0",
+ });
+
+ frameHost.appendChild(frame);
+ navigateSpaFrame(frame, frameUrl);
+
+ return waitForFrameLoad(frame);
+ }
+
+ function ensureSpaFrameHost() {
+ const existingHost = document.getElementById(IDS.spaFrameHost);
+
+ if (existingHost) {
+ const desiredParent = document.body || document.documentElement;
+
+ if (desiredParent && existingHost.parentElement !== desiredParent) {
+ desiredParent.appendChild(existingHost);
+ }
+
+ Object.assign(existingHost.style, getSpaFrameHostStyle());
+
+ return existingHost;
+ }
+
+ const host = document.createElement("div");
+ host.id = IDS.spaFrameHost;
+ host.setAttribute("aria-hidden", "true");
+ Object.assign(host.style, getSpaFrameHostStyle());
+
+ (document.body || document.documentElement).appendChild(host);
+
+ return host;
+ }
+
+ function getSpaFrameHostStyle() {
+ return {
+ position: "fixed",
+ width: "1440px",
+ height: "1000px",
+ overflow: "hidden",
+ opacity: "0",
+ pointerEvents: "none",
+ border: "0",
+ left: "-20000px",
+ top: "0",
+ zIndex: "0",
+ };
+ }
+
+ function shouldNavigateSpaFrame(frame, frameUrl) {
+ const currentSrc = cleanString(frame.getAttribute("src") || frame.src);
+
+ if (!currentSrc || currentSrc === "about:blank") {
+ return true;
+ }
+
+ try {
+ return new URL(currentSrc, window.location.href).toString() !== frameUrl;
+ } catch {
+ return currentSrc !== frameUrl;
+ }
+ }
+
+ function navigateSpaFrame(frame, frameUrl) {
+ frame.dataset.archPanelLoaded = "false";
+ frame.src = frameUrl;
+ }
+
+ function waitForFrameLoad(frame) {
+ if (frame.dataset.archPanelLoaded === "true") {
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve) => {
+ const timeout = window.setTimeout(() => {
+ resolve();
+ }, 12000);
+
+ frame.addEventListener(
+ "load",
+ () => {
+ frame.dataset.archPanelLoaded = "true";
+ window.clearTimeout(timeout);
+ window.setTimeout(resolve, 1200);
+ },
+ { once: true }
+ );
+ });
+ }
+
+ async function fetchJsonViaExtension(url, options = {}) {
+ const requestUrl = resolveRequestUrl(url);
+ const bridgeHeaders = await requestBridgeHeaders({
+ url: requestUrl,
+ headers: options.headers,
+ body: options.body,
+ includeAllAuth: true,
+ });
+ const response = await chromeService.sendMessage({
+ type: "ARCH_PANEL_EXTENSION_FETCH",
+ url: requestUrl,
+ options: {
+ method: options.method || "GET",
+ referrer: options.referrer,
+ headers: buildExtensionFetchHeaders(
+ requestUrl,
+ options,
+ bridgeHeaders || {}
+ ),
+ body: options.body,
+ },
+ });
+
+ if (!response || typeof response !== "object") {
+ throw new Error(`No response received for ${requestUrl}`);
+ }
+
+ 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,
+ ]);
+ const debugHeaderKeys = Array.isArray(response.debug?.headerKeys)
+ ? response.debug.headerKeys.join(", ")
+ : "";
+ const debugUrl = cleanString(response.debug?.url);
+ const debugParts = [
+ debugUrl ? `url: ${debugUrl}` : "",
+ debugHeaderKeys ? `captured headers: ${debugHeaderKeys}` : "",
+ ].filter(Boolean);
+ const debugSuffix =
+ debugParts.length > 0 ? ` | ${debugParts.join(" | ")}` : "";
+
+ throw new Error(
+ `${
+ payloadMessage ||
+ response.error ||
+ `Request failed (${response.status}) for ${requestUrl}`
+ }${debugSuffix}`
+ );
+ }
+
+ return response.payload;
+ }
+
+ function buildExtensionFetchHeaders(requestUrl, options = {}, bridgeHeaders = {}) {
+ const combinedHeaders = {
+ ...(bridgeHeaders || {}),
+ ...(getSessionAuthHeaders() || {}),
+ accept: "application/json, text/plain, */*",
+ ...(options.headers || {}),
+ };
+
+ return options.omitAuthHeaders
+ ? stripAuthHeaders(combinedHeaders)
+ : combinedHeaders;
+ }
+
+ function stripAuthHeaders(headers = {}) {
+ const sanitizedHeaders = {};
+
+ for (const [key, value] of Object.entries(headers || {})) {
+ const normalizedKey = cleanString(key).toLowerCase();
+
+ if (
+ normalizedKey === "authorization" ||
+ normalizedKey === "x-id-token" ||
+ normalizedKey === "spa-ts-authorization" ||
+ normalizedKey.endsWith("-authorization")
+ ) {
+ continue;
+ }
+
+ sanitizedHeaders[key] = value;
+ }
+
+ return sanitizedHeaders;
+ }
+
+ async function requestBridgeHeaders(options = {}) {
+ await ensurePageBridge();
+
+ const requestId = `${Date.now()}-${++requestCounter}`;
+
+ return new Promise((resolve, reject) => {
+ const timeout = window.setTimeout(() => {
+ pendingBridgeRequests.delete(requestId);
+ reject(new Error("Timed out while requesting bridge headers"));
+ }, 30000);
+
+ pendingBridgeRequests.set(requestId, {
+ resolve,
+ reject,
+ timeout,
+ });
+
+ window.postMessage(
+ {
+ source: BRIDGE.source,
+ type: BRIDGE.headersType,
+ requestId,
+ options,
+ },
+ window.location.origin
+ );
+ });
+ }
+
+ function toBridgeRequestUrl(url) {
+ const normalized = resolveRequestUrl(url);
+
+ if (!normalized) {
+ return normalized;
+ }
+
+ try {
+ const parsedUrl = new URL(normalized, window.location.origin);
+
+ if (parsedUrl.origin === window.location.origin) {
+ return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
+ }
+
+ return parsedUrl.toString();
+ } catch {
+ return normalized;
+ }
+ }
+
+ function resolveRequestUrl(url) {
+ const normalized = cleanString(url);
+
+ if (!normalized) {
+ return "";
+ }
+
+ try {
+ const baseOrigin =
+ isHcmWelcomePage() && normalized.startsWith("/")
+ ? TARGET_ORIGIN
+ : window.location.origin;
+
+ return new URL(normalized, baseOrigin).toString();
+ } catch {
+ return normalized;
+ }
+ }
+
+ function handleBridgeMessage(event) {
+ const message = event.data;
+
+ if (!message || message.source !== BRIDGE.source) {
+ return;
+ }
+
+ if (message.type === BRIDGE.workbenchAuthHeadersType) {
+ rememberWorkbenchAuthHeaders(message.headers);
+ return;
+ }
+
+ if (event.source !== window) {
+ return;
+ }
+
+ if (message.type === BRIDGE.readyType) {
+ return;
+ }
+
+ if (
+ message.type !== BRIDGE.resultType &&
+ message.type !== BRIDGE.headersResultType
+ ) {
+ return;
+ }
+
+ const pending = pendingBridgeRequests.get(message.requestId);
+
+ if (!pending) {
+ return;
+ }
+
+ pendingBridgeRequests.delete(message.requestId);
+ window.clearTimeout(pending.timeout);
+ pending.resolve(
+ message.type === BRIDGE.headersResultType ? message.headers || {} : message
+ );
+ }
+
+ function rememberWorkbenchAuthHeaders(headers) {
+ if (!headers || typeof headers !== "object") {
+ return;
+ }
+
+ const nextHeaders = {
+ ...(getSessionAuthHeaders() || {}),
+ };
+
+ for (const [key, value] of Object.entries(headers)) {
+ const normalizedKey = cleanString(key).toLowerCase();
+ const normalizedValue = cleanString(value);
+
+ if (!normalizedKey || !normalizedValue) {
+ continue;
+ }
+
+ nextHeaders[normalizedKey] = normalizedValue;
+ }
+
+ setSessionAuthHeaders(nextHeaders);
+ }
+
+ return Object.freeze({
+ ensurePageBridge,
+ fetchJson,
+ sendComcipRequestMessage,
+ ensureSpaFrame,
+ handleBridgeMessage,
+ });
+ }
+
+ globalThis.ArchPanelTransportService = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/shared/config.js b/src/shared/config.js
new file mode 100644
index 0000000..72f2f86
--- /dev/null
+++ b/src/shared/config.js
@@ -0,0 +1,189 @@
+(() => {
+ const TARGET_ORIGIN = "https://spa.oracle.com";
+ const TARGET_PATHNAME =
+ "/oalcrm/web/api/g2m-consumer-application/ui/index.html";
+ const TARGET_PARAM = "ojr";
+ const TARGET_PARAM_VALUE = "workload_workbench";
+ const TARGET_APP_URL =
+ "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench";
+ const HCM_ORIGIN = "https://eeho.fa.us2.oraclecloud.com";
+ const HCM_WELCOME_PATHNAME = "/hcmUI/faces/FuseWelcome";
+ const HCM_MY_INFORMATION_APPS_GROUP_ID = "yourapps_groupNode_my_information";
+ const WORKLIST_ORIGIN = "https://worklist.oracle.com";
+ const WORKLIST_SAASUI_PATHNAME = "/oalapp/pub/worklist/saasui/index.html";
+ const USER_BUTTON_LABEL = "[[displayUser().fullName]]";
+ const ALLOWED_STAGE_TYPES = ["SQL", "Pipeline", "Upside", "Forecast"];
+
+ const SELECTORS = {
+ header: ".oj-oal-ux-global-header.oj-oal-ux-global-header-pillar-cx",
+ functions: ".oj-flex.oj-oal-ux-global-header-functions",
+ };
+
+ const IDS = {
+ mount: "arch-panel-extension-mount",
+ button: "arch-panel-extension-button",
+ style: "arch-panel-extension-style",
+ overlay: "arch-panel-extension-overlay",
+ dialog: "arch-panel-extension-dialog",
+ hcmTile: "arch-panel-extension-hcm-tile",
+ spaFrame: "arch-panel-extension-spa-frame",
+ spaFrameHost: "arch-panel-extension-spa-frame-host",
+ };
+
+ const CLASSES = {
+ modalOpen: "arch-panel-extension-modal-open",
+ overlayOpen: "is-open",
+ };
+
+ const STORAGE_KEYS = {
+ theme: "arch-panel-extension-theme",
+ sidebarCollapsed: "arch-panel-extension-sidebar-collapsed",
+ calendarView: "arch-panel-extension-calendar-view",
+ timeManagementSelectedDate: "arch-panel-extension-time-management-selected-date",
+ };
+
+ const CACHE = {
+ dbName: "arch-panel-extension-db",
+ dbVersion: 1,
+ storeName: "datasets",
+ datasetKey: "workload-dashboard",
+ taskTypeKey: "time-management-task-types",
+ worklistTokenKey: "worklist-saasui-token",
+ };
+
+ const DETAIL_SORT_KEYS = {
+ opportunityNumber: "opportunityNumber",
+ customerName: "customerName",
+ workload: "workload",
+ rampMonths: "rampMonths",
+ adjustedACR: "adjustedACR",
+ opportunityForecastTypeGroup: "opportunityForecastTypeGroup",
+ hasSR: "hasSR",
+ hasAction: "hasAction",
+ };
+
+ const BRIDGE = {
+ source: "arch-panel-extension",
+ fetchType: "ARCH_PANEL_FETCH",
+ resultType: "ARCH_PANEL_FETCH_RESULT",
+ headersType: "ARCH_PANEL_HEADERS",
+ headersResultType: "ARCH_PANEL_HEADERS_RESULT",
+ workbenchAuthHeadersType: "ARCH_PANEL_WORKBENCH_AUTH_HEADERS",
+ readyType: "ARCH_PANEL_BRIDGE_READY",
+ scriptId: "arch-panel-extension-page-bridge",
+ };
+
+ const API_ENDPOINTS = {
+ currentUser: "/oalcrm/web/api/identity-management/users/current",
+ worklistCurrentUser:
+ "https://worklist.oracle.com/oalapp/pub/wlp/svc/user-service/user",
+ customerSummary:
+ "/oalcrm/web/api/provider-proxy/wwb-provider/service/elastic/customers/listSummary",
+ customerWorkloads:
+ "/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/customers",
+ workloadActions:
+ "/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/actions",
+ workloadServiceRequests:
+ "/oalcrm/web/api/provider-proxy/wwb-provider/service/workbench-proxy/wwb/mgmt/workloads",
+ };
+
+ const COMCIP_REQUEST = {
+ url:
+ "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/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",
+ timeEntriesSummaryUrl:
+ "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 ACTION_TEAM_OPTIONS = [
+ {
+ value: "DM_AUTONOMOUS",
+ label: "Data Platform - Autonomous Database",
+ },
+ {
+ value: "CI_COMPUTE",
+ label: "Cloud Infrastructure - Compute including HPC",
+ },
+ {
+ value: "APP_DEV_AI_CLOUD_SERVICES",
+ label: "App Dev - AI Cloud Services",
+ },
+ {
+ value: "DEV_CLOUD_NATIVE_APP",
+ label: "App Dev - Cloud Native",
+ },
+ {
+ value: "CI_BIG_COMPUTE_HPC",
+ label: "Cloud Infrastructure - AI Infra/GPU",
+ },
+ {
+ value: "CI_MULTI_CLOUD",
+ label: "Cloud Infrastructure - Multi-Cloud",
+ },
+ {
+ value: "CI_STORAGE",
+ label: "Cloud Infrastructure - Storage",
+ },
+ {
+ value: "CI_VMWARE",
+ label: "Cloud Infrastructure - VMware",
+ },
+ {
+ value: "DALH_DESIGN",
+ label: "Data Platform - Analytical Data Platform/Lakehouse",
+ },
+ {
+ value: "DATA_INTEGRATION",
+ label: "Data Platform - Data Integration",
+ },
+ {
+ value: "DM_UPGRADE",
+ label: "Data Platform - Modernise",
+ },
+ ];
+
+ const ACTION_COMPLEXITY_OPTIONS = ["LOW", "MEDIUM", "HIGH", "VERY_HIGH"];
+
+ globalThis.ArchPanelConfig = Object.freeze({
+ TARGET_ORIGIN,
+ TARGET_PATHNAME,
+ TARGET_PARAM,
+ TARGET_PARAM_VALUE,
+ TARGET_APP_URL,
+ HCM_ORIGIN,
+ HCM_WELCOME_PATHNAME,
+ HCM_MY_INFORMATION_APPS_GROUP_ID,
+ WORKLIST_ORIGIN,
+ WORKLIST_SAASUI_PATHNAME,
+ USER_BUTTON_LABEL,
+ ALLOWED_STAGE_TYPES,
+ SELECTORS,
+ IDS,
+ CLASSES,
+ STORAGE_KEYS,
+ CACHE,
+ DETAIL_SORT_KEYS,
+ BRIDGE,
+ API_ENDPOINTS,
+ COMCIP_REQUEST,
+ COMCIP_TIME_ENTRY_FRAME_ID,
+ ACTION_TEAM_OPTIONS,
+ ACTION_COMPLEXITY_OPTIONS,
+ });
+})();
diff --git a/src/shared/utils.js b/src/shared/utils.js
new file mode 100644
index 0000000..d739ab6
--- /dev/null
+++ b/src/shared/utils.js
@@ -0,0 +1,463 @@
+(() => {
+ function normalizeText(value) {
+ return String(value || "").replace(/\s+/g, " ").trim();
+ }
+
+ function cleanString(value) {
+ return normalizeText(value);
+ }
+
+ function firstNonEmptyString(values) {
+ for (const value of values) {
+ const normalized = cleanString(value);
+
+ if (normalized) {
+ return normalized;
+ }
+ }
+
+ return "";
+ }
+
+ function normalizeString(value) {
+ return normalizeText(value).toUpperCase();
+ }
+
+ function toNumber(value) {
+ if (typeof value === "number" && Number.isFinite(value)) {
+ return value;
+ }
+
+ const numeric = Number.parseFloat(String(value ?? "").replace(/,/g, ""));
+ return Number.isFinite(numeric) ? numeric : 0;
+ }
+
+ function toIsoDateString(value) {
+ const normalized = cleanString(value);
+
+ if (!normalized) {
+ return "";
+ }
+
+ const date = new Date(`${normalized}T12:00:00.000Z`);
+
+ return Number.isNaN(date.getTime()) ? normalized : date.toISOString();
+ }
+
+ function extractArray(value, candidateKeys = []) {
+ if (Array.isArray(value)) {
+ return value;
+ }
+
+ if (!value || typeof value !== "object") {
+ return [];
+ }
+
+ for (const key of candidateKeys) {
+ if (Array.isArray(value[key])) {
+ return value[key];
+ }
+ }
+
+ for (const nestedValue of Object.values(value)) {
+ if (Array.isArray(nestedValue)) {
+ return nestedValue;
+ }
+
+ if (nestedValue && typeof nestedValue === "object") {
+ const nestedArray = extractArray(nestedValue, candidateKeys);
+
+ if (nestedArray.length > 0) {
+ return nestedArray;
+ }
+ }
+ }
+
+ return [];
+ }
+
+ async function mapWithConcurrency(items, concurrency, mapper) {
+ const results = new Array(items.length);
+ let nextIndex = 0;
+
+ async function worker() {
+ while (nextIndex < items.length) {
+ const currentIndex = nextIndex;
+ nextIndex += 1;
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
+ }
+ }
+
+ const workerCount = Math.min(concurrency, items.length || 1);
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
+
+ return results;
+ }
+
+ function getFieldValue(source, paths) {
+ if (!source || typeof source !== "object") {
+ return "";
+ }
+
+ for (const path of paths) {
+ if (Object.prototype.hasOwnProperty.call(source, path)) {
+ return source[path];
+ }
+
+ const parts = String(path).split(".");
+ let current = source;
+
+ for (const part of parts) {
+ if (!current || typeof current !== "object") {
+ current = "";
+ break;
+ }
+
+ current = current[part];
+ }
+
+ if (current !== undefined && current !== null && current !== "") {
+ return current;
+ }
+ }
+
+ return "";
+ }
+
+ function flattenDisplayRecord(value, prefix = "", output = []) {
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
+ if (prefix) {
+ output.push([prefix, value]);
+ }
+
+ return output;
+ }
+
+ for (const [key, nestedValue] of Object.entries(value)) {
+ const nextKey = prefix ? `${prefix}.${key}` : key;
+
+ if (
+ nestedValue &&
+ typeof nestedValue === "object" &&
+ !Array.isArray(nestedValue)
+ ) {
+ flattenDisplayRecord(nestedValue, nextKey, output);
+ } else {
+ output.push([nextKey, nestedValue]);
+ }
+ }
+
+ return output;
+ }
+
+ function isVisible(element) {
+ const style = window.getComputedStyle(element);
+ const rect = element.getBoundingClientRect();
+
+ return (
+ style.display !== "none" &&
+ style.visibility !== "hidden" &&
+ Number.parseFloat(style.opacity || "1") > 0 &&
+ rect.width > 0 &&
+ rect.height > 0
+ );
+ }
+
+ function formatWholeNumber(value) {
+ return new Intl.NumberFormat("en-US", {
+ maximumFractionDigits: 0,
+ }).format(value);
+ }
+
+ function formatCurrency(value) {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ maximumFractionDigits: 0,
+ }).format(value || 0);
+ }
+
+ function formatHours(value) {
+ return new Intl.NumberFormat("pt-BR", {
+ maximumFractionDigits: 2,
+ }).format(toNumber(value));
+ }
+
+ function formatWeekNumber(value) {
+ return String(toNumber(value)).padStart(2, "0");
+ }
+
+ function formatOperationalWeekLabel(weekInfo) {
+ if (!weekInfo) {
+ return "";
+ }
+
+ return `FY${formatWeekNumber(weekInfo.fiscalYear % 100)}-Q${
+ weekInfo.quarter
+ }-W${formatWeekNumber(weekInfo.weekNumber)}`;
+ }
+
+ function formatPercent(value) {
+ return new Intl.NumberFormat("en-US", {
+ maximumFractionDigits: 1,
+ }).format(value || 0) + "%";
+ }
+
+ function formatProgressPercent(value) {
+ return new Intl.NumberFormat("en-US", {
+ maximumFractionDigits: 0,
+ }).format(value || 0) + "%";
+ }
+
+ function formatDate(value) {
+ if (!value) {
+ return "";
+ }
+
+ const date = parseDatePreservingDateOnly(value);
+
+ if (Number.isNaN(date.getTime())) {
+ return String(value);
+ }
+
+ return new Intl.DateTimeFormat("en-US", {
+ dateStyle: "medium",
+ }).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 formatDateInputDisplay(value) {
+ const date = parseDatePreservingDateOnly(value);
+
+ if (Number.isNaN(date.getTime())) {
+ return cleanString(value);
+ }
+
+ const day = String(date.getDate()).padStart(2, "0");
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const year = date.getFullYear();
+
+ return `${day}/${month}/${year}`;
+ }
+
+ 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 "";
+ }
+
+ const date = new Date(value);
+
+ if (Number.isNaN(date.getTime())) {
+ return String(value);
+ }
+
+ return new Intl.DateTimeFormat("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ }).format(date);
+ }
+
+ function getLocalDateKey(value) {
+ if (!value) {
+ return "";
+ }
+
+ 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 getOperationalWeekInfo(value) {
+ const date = parseDatePreservingDateOnly(value);
+
+ if (Number.isNaN(date.getTime())) {
+ return null;
+ }
+
+ const operationalYearStart = new Date(date.getFullYear(), 5, 1);
+
+ if (date < operationalYearStart) {
+ operationalYearStart.setFullYear(operationalYearStart.getFullYear() - 1);
+ }
+
+ const normalizedDate = new Date(
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate()
+ );
+ const normalizedStart = new Date(
+ operationalYearStart.getFullYear(),
+ operationalYearStart.getMonth(),
+ operationalYearStart.getDate()
+ );
+ const daysSinceStart = Math.floor(
+ (normalizedDate.getTime() - normalizedStart.getTime()) / 86400000
+ );
+ const monthsSinceStart =
+ (normalizedDate.getFullYear() - normalizedStart.getFullYear()) * 12 +
+ normalizedDate.getMonth() -
+ normalizedStart.getMonth();
+
+ return {
+ fiscalYear: operationalYearStart.getFullYear() + 1,
+ quarter: Math.floor(monthsSinceStart / 3) + 1,
+ weekNumber: Math.floor(daysSinceStart / 7) + 1,
+ };
+ }
+
+ function getCalendarRowWeekDate(rowStartDate) {
+ const weekDate = new Date(rowStartDate);
+
+ if (weekDate.getDay() === 0) {
+ weekDate.setDate(weekDate.getDate() + 1);
+ }
+
+ return weekDate;
+ }
+
+ function parseDatePreservingDateOnly(value) {
+ if (value instanceof Date) {
+ return value;
+ }
+
+ if (typeof value === "string") {
+ const dateOnlyMatch = value.trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
+
+ if (dateOnlyMatch) {
+ return new Date(
+ Number(dateOnlyMatch[1]),
+ Number(dateOnlyMatch[2]) - 1,
+ Number(dateOnlyMatch[3])
+ );
+ }
+ }
+
+ return new Date(value);
+ }
+
+ function getErrorMessage(error) {
+ if (error instanceof Error) {
+ return error.message;
+ }
+
+ return "Unexpected error while fetching live data.";
+ }
+
+ function escapeHtml(value) {
+ return String(value ?? "")
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+
+ function cssEscape(value) {
+ if (window.CSS?.escape) {
+ return window.CSS.escape(String(value || ""));
+ }
+
+ return String(value || "").replace(/["\\]/g, "\\$&");
+ }
+
+ globalThis.ArchPanelUtils = Object.freeze({
+ normalizeText,
+ cleanString,
+ firstNonEmptyString,
+ normalizeString,
+ toNumber,
+ toIsoDateString,
+ extractArray,
+ mapWithConcurrency,
+ getFieldValue,
+ flattenDisplayRecord,
+ isVisible,
+ formatWholeNumber,
+ formatCurrency,
+ formatHours,
+ formatWeekNumber,
+ formatOperationalWeekLabel,
+ formatPercent,
+ formatProgressPercent,
+ formatDate,
+ getDateInputValue,
+ formatDateInputDisplay,
+ formatDateForTimeEntryRequest,
+ parseTimeEntryDate,
+ formatDateTime,
+ getLocalDateKey,
+ getOperationalWeekInfo,
+ getCalendarRowWeekDate,
+ parseDatePreservingDateOnly,
+ getErrorMessage,
+ escapeHtml,
+ cssEscape,
+ });
+})();
diff --git a/src/views/dashboard-view.js b/src/views/dashboard-view.js
new file mode 100644
index 0000000..48d53ef
--- /dev/null
+++ b/src/views/dashboard-view.js
@@ -0,0 +1,714 @@
+(() => {
+ function create(dependencies) {
+ const {
+ appState,
+ buildWorkloadStartCalendar,
+ hasRampForecastMismatch,
+ renderOpportunityLink,
+ renderWorkloadLink,
+ escapeHtml,
+ cleanString,
+ formatWholeNumber,
+ formatCurrency,
+ formatPercent,
+ formatProgressPercent,
+ formatDate,
+ } = dependencies;
+
+ function renderMainContent() {
+ if (appState.status === "loading") {
+ return `
+
+
+ Loading data
+ Building the workload dataset
+ ${escapeHtml(appState.loadingMessage)}
+
+
+ Overall progress
+
+
+
+
+ ${formatProgressPercent(appState.loadingProgress || 0)}
+
+
+
+
+
+ `;
+ }
+
+ if (appState.status === "error") {
+ return `
+
+
+ Request failed
+ Unable to assemble the dashboard
+ ${escapeHtml(appState.errorMessage)}
+
+
+ Retry
+
+
+
+
+ `;
+ }
+
+ if (!appState.dataset) {
+ return `
+
+
+ Ready
+ Start the live data extraction
+
+ No cached snapshot is available yet. Use Refresh data to fetch the current user, customers, workloads, actions and service requests.
+
+
+
+ Refresh data
+
+
+
+
+ `;
+ }
+
+ const { user, summary } = appState.dataset;
+
+ return `
+
+
+
+
+ ${renderMetricCard("Total clients", formatWholeNumber(summary.totalCustomers), "Loaded from listSummary")}
+ ${renderMetricCard("Total workloads", formatWholeNumber(summary.totalWorkloads), "Excluding Won workloads")}
+ ${renderMetricCard("Total (ACR)", formatCurrency(summary.totalEligibleAcr), "Across eligible workloads")}
+ ${renderMetricCard("Pending SRs", formatWholeNumber(summary.pendingServiceRequests.length), "SVC_PENDING_DELIVERY")}
+
+
+
+ ${summary.stageIndicators.map((indicator) => renderStageIndicator(indicator)).join("")}
+
+
+
+ ${renderBooleanIndicator(
+ "Status de SRs",
+ "Workloads com SR",
+ summary.srStatus.trueCount,
+ "Workloads sem SR",
+ summary.srStatus.falseCount,
+ "sr"
+ )}
+ ${renderBooleanIndicator(
+ "Status de Consumption Plan/action",
+ "Workloads com action",
+ summary.actionStatus.trueCount,
+ "Workloads sem action",
+ summary.actionStatus.falseCount,
+ "action"
+ )}
+
+
+ ${renderSrHoursAndCalendarSection(summary)}
+
+
+ `;
+ }
+
+ function renderCurrentUserSidebar(user) {
+ const displayName = cleanString(user?.name || user?.userEmail);
+ const userEmail = cleanString(user?.userEmail);
+ const avatar = cleanString(user?.avatar);
+ const initials = getUserInitials(displayName || userEmail);
+
+ return `
+
+ ${
+ avatar
+ ? `
`
+ : `
${escapeHtml(initials)} `
+ }
+
+
+ `;
+ }
+
+ function getUserInitials(value) {
+ const parts = cleanString(value)
+ .split(/\s+/)
+ .filter(Boolean)
+ .slice(0, 2);
+
+ if (!parts.length) {
+ return "U";
+ }
+
+ return parts.map((part) => part.charAt(0).toUpperCase()).join("");
+ }
+
+ function getExtensionVersion() {
+ try {
+ return chrome.runtime.getManifest()?.version || "0.0.0";
+ } catch {
+ return "0.0.0";
+ }
+ }
+
+ function renderMetricCard(label, value, detail) {
+ return `
+
+ ${escapeHtml(label)}
+ ${escapeHtml(value)}
+ ${escapeHtml(detail)}
+
+ `;
+ }
+
+ function renderStageIndicator(indicator) {
+ return `
+
+ ${escapeHtml(indicator.label)}
+ ${formatWholeNumber(indicator.count)}
+ Total (ACR): ${formatCurrency(indicator.totalAcr)}
+
+ `;
+ }
+
+ function renderBooleanIndicator(
+ title,
+ trueLabel,
+ trueCount,
+ falseLabel,
+ falseCount,
+ scope
+ ) {
+ const total = trueCount + falseCount;
+ const truePercent = total > 0 ? (trueCount / total) * 100 : 0;
+ const falsePercent = total > 0 ? (falseCount / total) * 100 : 0;
+
+ return `
+
+
+
+
+
+
+
+
+ ${escapeHtml(trueLabel)}: ${formatPercent(truePercent)}
+ ${escapeHtml(falseLabel)}: ${formatPercent(falsePercent)}
+
+
+
+
+ ${escapeHtml(trueLabel)}
+ ${formatWholeNumber(trueCount)}
+ ${formatPercent(truePercent)}
+
+
+ ${escapeHtml(falseLabel)}
+ ${formatWholeNumber(falseCount)}
+ ${formatPercent(falsePercent)}
+
+
+
+ `;
+ }
+
+ function renderSrHoursAndCalendarSection(summary) {
+ const calendar = buildWorkloadStartCalendar(
+ summary.allWorkloads,
+ appState.calendarYear,
+ appState.calendarMonth,
+ appState.calendarView,
+ appState.calendarAnchorDateKey
+ );
+
+ return `
+
+ ${renderSrHoursIndicatorCard(summary)}
+ ${renderWorkloadStartCalendar(calendar)}
+
+ `;
+ }
+
+ function renderSrHoursIndicatorCard(summary) {
+ const zeroToFourCount = summary.srHours.betweenZeroAndFour.length;
+ const aboveFourCount = summary.srHours.aboveFour.length;
+ const total = zeroToFourCount + aboveFourCount;
+ const zeroToFourPercent = total > 0 ? (zeroToFourCount / total) * 100 : 0;
+ const aboveFourPercent = total > 0 ? (aboveFourCount / total) * 100 : 0;
+
+ return `
+
+
+
+
+
+
+
+
+ SRs 0-4h: ${formatPercent(zeroToFourPercent)}
+ SRs >4h: ${formatPercent(aboveFourPercent)}
+
+
+
+ ${renderSrHoursIndicator(
+ "SRs 0-4h",
+ formatPercent(zeroToFourPercent),
+ zeroToFourCount,
+ "0-4"
+ )}
+ ${renderSrHoursIndicator(
+ "SRs >4h",
+ formatPercent(aboveFourPercent),
+ aboveFourCount,
+ "above-4"
+ )}
+
+
+ `;
+ }
+
+ function renderSrHoursIndicator(label, detail, count, value) {
+ return `
+
+ ${escapeHtml(label)}
+ ${formatWholeNumber(count)}
+ ${escapeHtml(detail)}
+
+ `;
+ }
+
+ function renderWorkloadStartCalendar(calendar) {
+ return `
+
+
+
+ ${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
+ .map((day) => `${day} `)
+ .join("")}
+
+
+ ${calendar.days
+ .map((day) => renderCalendarDay(day, calendar.view))
+ .join("")}
+
+
+ `;
+ }
+
+ function renderCalendarDay(day, view = "month") {
+ if (view === "week") {
+ return renderCalendarWeekDay(day);
+ }
+
+ const firstWorkload = day.workloads[0];
+ const calendarWorkloadLabel =
+ firstWorkload?.description || firstWorkload?.name || "Workload";
+ const className = [
+ "arch-panel-extension-calendar-day",
+ day.isCurrentMonth ? "" : "is-muted",
+ day.isToday ? "is-today" : "",
+ day.workloads.length ? "has-workload" : "",
+ ]
+ .filter(Boolean)
+ .join(" ");
+
+ return `
+
+ ${day.dayNumber}
+ ${
+ day.isWeekStart
+ ? `${escapeHtml(
+ day.operationalWeekLabel
+ )} `
+ : ""
+ }
+ ${
+ day.workloads.length
+ ? `${escapeHtml(
+ calendarWorkloadLabel
+ )} ${formatWholeNumber(
+ day.workloads.length
+ )} `
+ : ""
+ }
+
+ `;
+ }
+
+ function renderCalendarWeekDay(day) {
+ const className = [
+ "arch-panel-extension-calendar-day",
+ "is-week-day",
+ day.isToday ? "is-today" : "",
+ day.workloads.length ? "has-workload" : "",
+ ]
+ .filter(Boolean)
+ .join(" ");
+ const title = day.workloads
+ .map((workload) => workload.description || workload.name)
+ .filter(Boolean)
+ .join(" | ");
+
+ return `
+
+
+ ${day.dayNumber}
+ ${
+ day.isWeekStart
+ ? `${escapeHtml(
+ day.operationalWeekLabel
+ )} `
+ : ""
+ }
+
+
+ ${
+ day.workloads.length
+ ? day.workloads.map((workload) => renderCalendarWeekWorkload(workload)).join("")
+ : `No starts `
+ }
+
+
+ `;
+ }
+
+ function renderCalendarWeekWorkload(workload) {
+ const label = workload.description || workload.name || "Workload";
+ const customer = workload.customerName || "";
+
+ return `
+
+ ${escapeHtml(label)}
+ ${
+ customer
+ ? `${escapeHtml(
+ customer
+ )} `
+ : ""
+ }
+
+ `;
+ }
+
+ function renderWorkloadTable(workloads) {
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Opportunity
+ Customer
+ Workload
+ ACR
+ Type
+ Tem SR?
+ Tem Action?
+
+
+
+ ${workloads.map((workload) => renderWorkloadTableRow(workload)).join("")}
+
+
+
+ `;
+ }
+
+ function renderWorkloadTableRow(
+ workload,
+ includeConsumptionStartDate = false,
+ includeRampMonths = false
+ ) {
+ const hasRampMismatch =
+ includeRampMonths && hasRampForecastMismatch(workload);
+
+ return `
+
+ ${renderOpportunityLink(workload)}
+ ${escapeHtml(workload.customerName || "")}
+ ${renderWorkloadLink(workload, hasRampMismatch)}
+ ${
+ includeConsumptionStartDate
+ ? `${escapeHtml(formatDate(workload.consumptionStartDate) || "-")} `
+ : ""
+ }
+ ${
+ includeRampMonths
+ ? `${renderRampMonthsButton(workload)} `
+ : ""
+ }
+ ${formatCurrency(workload.adjustedACR)}
+ ${escapeHtml(workload.opportunityForecastTypeGroup)}
+ ${renderBooleanBadge(workload.hasSR)}
+ ${renderHasActionCell(workload)}
+
+ `;
+ }
+
+ function renderRampMonthsButton(workload) {
+ return `
+
+ ${formatWholeNumber(workload.rampMonths || 0)}
+
+ `;
+ }
+
+ function renderBooleanBadge(value) {
+ const isTrue = Boolean(value);
+ const variantClass = isTrue ? "is-success" : "is-danger";
+ const label = isTrue ? "Sim" : "Não";
+
+ return `
+
+ ${label}
+
+ `;
+ }
+
+ function renderHasActionCell(workload) {
+ if (workload.hasAction) {
+ return renderBooleanBadge(true);
+ }
+
+ return `
+
+ ${renderBooleanBadge(false)}
+
+ ➕
+
+
+ `;
+ }
+
+ function renderInsightRow(label, value, detail) {
+ return `
+
+
+
${escapeHtml(label)}
+
${escapeHtml(detail)}
+
+
${escapeHtml(value)}
+
+ `;
+ }
+
+
+ return {
+ renderMainContent,
+ renderWorkloadTableRow,
+ };
+ }
+
+ globalThis.ArchPanelDashboardView = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/views/manage-time-view.js b/src/views/manage-time-view.js
new file mode 100644
index 0000000..5418654
--- /dev/null
+++ b/src/views/manage-time-view.js
@@ -0,0 +1,633 @@
+(() => {
+ function create(dependencies) {
+ const {
+ appState,
+ escapeHtml,
+ cleanString,
+ normalizeString,
+ formatDateInputDisplay,
+ getDateInputValue,
+ formatHours,
+ getWeekStartDate,
+ getLocalDateKey,
+ getTimeManagementDayKey,
+ isTimeManagementSaveEnabled,
+ getTimeManagementRows,
+ getTimeManagementActivityOptions,
+ findTimeManagementServiceRequestByNumber,
+ formatTimeManagementServiceRequestLabel,
+ isNonServiceRequestSrNumber,
+ findOptionValue,
+ findOptionLabel,
+ getTimeManagementTaskTypeOptions,
+ formatTimeManagementHourPayloadValue,
+ } = dependencies;
+
+ function renderTimeManagementModal() {
+ const modal = appState.timeManagementModal;
+
+ if (!modal) {
+ return "";
+ }
+
+ const selectedDate = modal.selectedDate || getDateInputValue(new Date());
+ const selectedDateLabel = formatDateInputDisplay(selectedDate);
+ const canSaveTimeManagement = isTimeManagementSaveEnabled(modal);
+ const isSavingTimeManagement = Boolean(modal.isSaving);
+
+ return `
+
+
+
+
+
Horas apontadas
+
Manage time
+
+
+
+
+
+ ${renderTimeManagementBody(modal)}
+
+
+
+ ${
+ modal.saveErrorMessage
+ ? `
${escapeHtml(modal.saveErrorMessage)}
`
+ : ""
+ }
+
+ Fechar
+
+
+
+
+ `;
+ }
+
+ 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.length
+ ? rows.map((row, index) => renderTimeManagementEntryRow(modal, row, index)).join("")
+ : `
+ Nenhum apontamento de hora neste período
+
`
+ }
+
+
+ `;
+ }
+
+ 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 isHighlighted =
+ cleanString(modal?.highlightRowId) &&
+ cleanString(modal?.highlightRowId) === rowId;
+ const selectedSr = cleanString(row?.selectedSrNumber);
+ const activityOptions = getTimeManagementActivityOptions(modal, selectedSr);
+ const selectedSrOption = findTimeManagementServiceRequestByNumber(
+ modal,
+ selectedSr
+ );
+ const isNonServiceRequest = isNonServiceRequestSrNumber(selectedSr);
+ const selectedSrLabel =
+ selectedSrOption?.label ||
+ formatTimeManagementServiceRequestLabel(selectedSrOption) ||
+ 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 `
+
+
+
+ ${renderSearchableCombobox({
+ field: "sr",
+ rowId,
+ selectedValue: selectedSr,
+ value: selectedSrLabel,
+ placeholder: "Search SR",
+ options: modal.serviceRequests || [],
+ valueKey: "srNumber",
+ labelKey: "label",
+ fallbackLabelKey: "srTitle",
+ disabled: isNonServiceRequest,
+ })}
+
+
+ ${renderSearchableCombobox({
+ field: "activity",
+ rowId,
+ selectedValue: selectedActivityValue,
+ value: selectedActivityLabel,
+ placeholder: "Search activity",
+ options: activityOptions,
+ valueKey: "value",
+ labelKey: "label",
+ metaKey: "value",
+ disabled: isNonServiceRequest,
+ })}
+
+
+ ${renderSearchableCombobox({
+ field: "taskType",
+ rowId,
+ selectedValue: selectedTaskTypeValue,
+ value: selectedTaskTypeLabel,
+ placeholder: "Search task type",
+ options: taskTypeOptions,
+ valueKey: "value",
+ labelKey: "label",
+ })}
+
+ ${(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 renderTimeManagementHourIndicators(modal) {
+ if (modal?.status === "loading") {
+ return `
+
+ ${Array.from({ length: 4 }, () => `
+
+
+
+
+ `).join("")}
+
+ `;
+ }
+
+ return `
+
+ ${buildTimeManagementHourIndicators(modal)
+ .map(
+ (indicator) => `
+
+ ${escapeHtml(indicator.label)}
+ ${escapeHtml(formatHours(indicator.value))}
+
+ `
+ )
+ .join("")}
+
+ `;
+ }
+
+ function buildTimeManagementHourIndicators(modal) {
+ const totals = getTimeManagementRows(modal).reduce(
+ (accumulator, row) => {
+ const rowTotal = getTimeManagementRowTotalHours(row);
+ const category = getTimeManagementHourCategory(modal, row);
+
+ accumulator[category] += rowTotal;
+ accumulator.total += rowTotal;
+
+ return accumulator;
+ },
+ {
+ service: 0,
+ nonService: 0,
+ timeOff: 0,
+ total: 0,
+ }
+ );
+
+ return [
+ { label: "Service", value: totals.service },
+ { label: "Non Service", value: totals.nonService },
+ { label: "Time Off", value: totals.timeOff },
+ { label: "Total hours", value: totals.total },
+ ];
+ }
+
+ function getTimeManagementRowTotalHours(row) {
+ return Object.values(row?.dayHours || {}).reduce(
+ (sum, value) => sum + formatTimeManagementHourPayloadValue(value),
+ 0
+ );
+ }
+
+ function getTimeManagementHourCategory(modal, row) {
+ if (isTimeManagementTimeOffRow(modal, row)) {
+ return "timeOff";
+ }
+
+ return isNonServiceRequestSrNumber(row?.selectedSrNumber)
+ ? "nonService"
+ : "service";
+ }
+
+ function isTimeManagementTimeOffRow(modal, row) {
+ const taskTypeValue = cleanString(row?.selectedTaskTypeValue);
+ const taskType = findTimeManagementTaskTypeByValue(modal, taskTypeValue);
+ const candidates = [
+ taskTypeValue,
+ taskType?.label,
+ taskType?.tags,
+ ].map((value) => normalizeString(value));
+
+ return candidates.some(
+ (value) =>
+ value === "TIME_OFF" ||
+ value === "TIMEOFF" ||
+ value.includes("TIME_OFF") ||
+ value.includes("TIME OFF")
+ );
+ }
+
+ function findTimeManagementTaskTypeByValue(modal, value) {
+ const normalizedValue = normalizeString(value);
+
+ if (!normalizedValue) {
+ return null;
+ }
+
+ return (modal?.taskTypes || []).find(
+ (item) => normalizeString(item?.value) === normalizedValue
+ ) || null;
+ }
+
+ function renderTimeManagementDayInput(
+ day,
+ rowId = "",
+ dayHours = {}
+ ) {
+ const dateKey = getTimeManagementDayKey(day);
+ const value = cleanString(dayHours?.[dateKey]);
+
+ return `
+
+
+
+ `;
+ }
+
+ return {
+ renderTimeManagementModal,
+ };
+ }
+
+ globalThis.ArchPanelManageTimeView = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/views/render-helpers.js b/src/views/render-helpers.js
new file mode 100644
index 0000000..a1de7c4
--- /dev/null
+++ b/src/views/render-helpers.js
@@ -0,0 +1,213 @@
+(() => {
+ function create({
+ appState,
+ escapeHtml,
+ cleanString,
+ formatWholeNumber,
+ formatCurrency,
+ formatDate,
+ formatHours,
+ }) {
+ function renderServiceRequestDetailTable(items) {
+ return `
+
+
+
+
+ Customer
+ SR Number
+ Queue
+ Service
+ Pillar
+ Oppty value
+ Horas apontadas
+ Customer deadline
+ Oppty Status
+
+
+
+ ${items
+ .map(
+ (serviceRequest) => `
+
+ ${escapeHtml(serviceRequest.customerName || "-")}
+ ${renderServiceRequestLink(serviceRequest)}
+ ${escapeHtml(serviceRequest.queueName || "-")}
+ ${escapeHtml(serviceRequest.serviceName || "-")}
+ ${escapeHtml(serviceRequest.pillarLabel || "-")}
+ ${formatCurrency(serviceRequest.opportunityValue)}
+ ${renderReportedHoursLink(serviceRequest)}
+ ${escapeHtml(formatDate(serviceRequest.customerDeadline) || "-")}
+ ${escapeHtml(serviceRequest.opportunityStatusLabel || "-")}
+
+ `
+ )
+ .join("")}
+
+
+
+ `;
+ }
+
+ function renderSortButton(label, sortKey, isNumeric = false) {
+ const currentKey = appState.detailModal?.sortKey || "";
+ const currentDirection = appState.detailModal?.sortDirection || "asc";
+ const indicator =
+ currentKey === sortKey ? (currentDirection === "asc" ? " ↑" : " ↓") : "";
+
+ return `
+
+ ${escapeHtml(label)}${indicator}
+
+ `;
+ }
+
+ function renderOpportunityLink(workload) {
+ const label = workload.opportunityNumber || "Opportunity";
+
+ if (!workload.opportunityId || !workload.opportunityNumber) {
+ return escapeHtml(label);
+ }
+
+ const href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?id=${encodeURIComponent(
+ workload.opportunityId
+ )}&puid=${encodeURIComponent(workload.opportunityNumber)}`;
+
+ return `${escapeHtml(label)} `;
+ }
+
+ function renderServiceRequestLink(serviceRequest) {
+ const srNumber = cleanString(serviceRequest?.srNumber);
+
+ if (!srNumber) {
+ return "-";
+ }
+
+ const href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=${encodeURIComponent(
+ srNumber
+ )}`;
+
+ return `${escapeHtml(srNumber)} `;
+ }
+
+ function renderReportedHoursLink(serviceRequest) {
+ const srNumber = cleanString(serviceRequest?.srNumber);
+ const label = formatHours(serviceRequest?.totalHoursWorked);
+
+ if (!srNumber) {
+ return escapeHtml(label);
+ }
+
+ return `
+
+ ${escapeHtml(label)}
+
+ `;
+ }
+
+ function renderWorkloadLink(workload, showRampAlert = false) {
+ const description = workload.description ? ` | ${workload.description}` : "";
+ const label = `${workload.name || "Unnamed workload"}${description}`;
+
+ if (!workload.workloadId) {
+ return `${escapeHtml(label)}${renderRampAlertButton(
+ workload,
+ showRampAlert
+ )}`;
+ }
+
+ const href = `https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench/workload_details;workloadId=${encodeURIComponent(
+ workload.workloadId
+ )}`;
+
+ return `
+
+ ${escapeHtml(label)}
+ ${renderRampAlertButton(workload, showRampAlert)}
+
+ `;
+ }
+
+ function renderRampAlertButton(workload, showRampAlert) {
+ if (!showRampAlert || !workload.workloadId) {
+ return "";
+ }
+
+ return `
+
+ !
+
+ `;
+ }
+
+ function getHeaderNote() {
+ if (appState.status === "loading") {
+ return appState.loadingMessage || "Loading";
+ }
+
+ if (appState.status === "error") {
+ return "Extraction failed";
+ }
+
+ if (appState.dataset) {
+ return `${appState.dataset.user.userEmail} - ${formatWholeNumber(
+ appState.dataset.summary.eligibleWorkloads.length
+ )} eligible workloads`;
+ }
+
+ return "Ready";
+ }
+
+ function getHeaderSummary() {
+ if (appState.status === "loading") {
+ return appState.loadingMessage || "Loading";
+ }
+
+ if (appState.status === "error") {
+ return "Extraction failed";
+ }
+
+ if (appState.dataset) {
+ return `${appState.dataset.user.userEmail} | ${formatWholeNumber(
+ appState.dataset.summary.eligibleWorkloads.length
+ )} eligible workloads`;
+ }
+
+ return "Ready";
+ }
+
+ return Object.freeze({
+ renderServiceRequestDetailTable,
+ renderSortButton,
+ renderOpportunityLink,
+ renderWorkloadLink,
+ getHeaderNote,
+ getHeaderSummary,
+ });
+ }
+
+ globalThis.ArchPanelRenderHelpers = Object.freeze({
+ create,
+ });
+})();
diff --git a/src/views/workbench-modals-view.js b/src/views/workbench-modals-view.js
new file mode 100644
index 0000000..97540c5
--- /dev/null
+++ b/src/views/workbench-modals-view.js
@@ -0,0 +1,518 @@
+(() => {
+ function create(dependencies) {
+ const {
+ appState,
+ DETAIL_SORT_KEYS,
+ ACTION_TEAM_OPTIONS,
+ ACTION_COMPLEXITY_OPTIONS,
+ getSortedDetailItems,
+ renderServiceRequestDetailTable,
+ renderSortButton,
+ renderWorkloadTableRow,
+ findWorkloadById,
+ renderOpportunityLink,
+ groupTimeEntriesByResourceName,
+ getTimeEntryTimeSpent,
+ getTimeEntryActivity,
+ getTimeEntryTaskType,
+ isNonZeroAmount,
+ escapeHtml,
+ cleanString,
+ formatWholeNumber,
+ formatCurrency,
+ formatDate,
+ formatHours,
+ } = dependencies;
+
+ function renderDetailModal() {
+ if (!appState.detailModal) {
+ return "";
+ }
+
+ const isServiceRequestDetail = appState.detailModal.type === "serviceRequests";
+ const detailItems = isServiceRequestDetail
+ ? appState.detailModal.items || []
+ : getSortedDetailItems();
+
+ return `
+
+
+
+
+ ${
+ isServiceRequestDetail
+ ? `${formatWholeNumber(detailItems.length)} SRs `
+ : `${formatWholeNumber(detailItems.length)} workloads
+
+ Total (ACR): ${formatCurrency(appState.detailModal.totalAcr)} `
+ }
+
+ ${
+ isServiceRequestDetail
+ ? renderServiceRequestDetailTable(detailItems)
+ : renderDetailTable(
+ detailItems,
+ appState.detailModal.scope === "calendar-day"
+ )
+ }
+
+ ${
+ isServiceRequestDetail
+ ? `
+ ⏱️ Manage time
+ `
+ : ""
+ }
+
+ Fechar
+
+
+
+
+ `;
+ }
+
+ function renderActionFormModal() {
+ if (!appState.actionFormModal || !appState.dataset) {
+ return "";
+ }
+
+ const workload = findWorkloadById(appState.actionFormModal.workloadId);
+ const userEmail = appState.dataset.user?.userEmail || "";
+
+ if (!workload) {
+ return "";
+ }
+
+ const modalError = cleanString(appState.actionFormModal.errorMessage);
+ const isSubmitting = Boolean(appState.actionFormModal.isSubmitting);
+
+ return `
+
+
+
+
+
Create workload action
+
Criar action
+
+
+ Fechar
+
+
+
+
+ ${renderOpportunityLink(workload)}
+
+ ${escapeHtml(workload.customerName || "")}
+
+ ${escapeHtml(workload.name || "Unnamed workload")}
+
+
+ ${modalError ? `
${escapeHtml(modalError)}
` : ""}
+
+
+
+
+ `;
+ }
+
+ function renderRampComparisonModal() {
+ const modal = appState.rampComparisonModal;
+
+ if (!modal) {
+ return "";
+ }
+
+ const rows = modal.rows || [];
+ const title = `${modal.workload.customerName || "Unknown customer"} | ${
+ modal.workload.name || "Unnamed workload"
+ }${
+ modal.workload.description ? ` | ${modal.workload.description}` : ""
+ }`;
+
+ return `
+
+
+
+
+
Ramp forecast comparison
+
${escapeHtml(title)}
+
+
+
+ Ramp months: ${formatWholeNumber(modal.workload.rampMonths || 0)}
+
+ Adjusted ACR: ${formatCurrency(modal.workload.adjustedACR)}
+
+ Consumption start: ${escapeHtml(
+ formatDate(modal.workload.consumptionStartDate) || "-"
+ )}
+
+ ${renderRampComparisonPivotTable(rows)}
+
+
+ Atualizar pelo Calculated ramp
+
+
+ Fechar
+
+
+
+
+ `;
+ }
+
+ function renderRampComparisonPivotTable(rows) {
+ if (!rows.length) {
+ return `Nenhum forecast encontrado para comparar.
`;
+ }
+
+ return `
+
+
+
+
+ Period
+ ${rows.map((row) => `${escapeHtml(row.periodLabel)} `).join("")}
+
+
+ Current adjusted
+ ${rows
+ .map(
+ (row) =>
+ `${formatCurrency(row.currentValue)} `
+ )
+ .join("")}
+
+
+ Calculated ramp
+ ${rows
+ .map(
+ (row) =>
+ `${formatCurrency(row.calculatedValue)} `
+ )
+ .join("")}
+
+
+ Delta
+ ${rows
+ .map(
+ (row) =>
+ `${formatCurrency(row.delta)} `
+ )
+ .join("")}
+
+
+
+
+ `;
+ }
+
+ function renderForecastUpdateConfirmModal() {
+ const modal = appState.forecastUpdateConfirmModal;
+
+ if (!modal) {
+ return "";
+ }
+
+ return `
+
+
+
+
+
Confirmar atualização
+
Atualizar forecast pelo Calculated ramp?
+
+
+
+ Esta ação enviará ${formatWholeNumber(
+ modal.payload.length
+ )} meses calculados para o workload atual. Os valores atuais do forecast serão substituídos pelos valores da linha Calculated ramp.
+
+ ${
+ modal.errorMessage
+ ? `
${escapeHtml(
+ modal.errorMessage
+ )}
`
+ : ""
+ }
+
+
+ Cancelar
+
+
+ ${modal.isSubmitting ? "Atualizando..." : "Confirmar atualização"}
+
+
+
+
+ `;
+ }
+
+ function renderTimeEntriesDrawer() {
+ const drawer = appState.timeEntriesDrawer;
+
+ if (!drawer) {
+ return "";
+ }
+
+ const items = Array.isArray(drawer.items) ? drawer.items : [];
+
+ return `
+
+
+
+
Horas apontadas
+
SR ${escapeHtml(drawer.srNumber || "-")}
+
+
+ ${renderTimeEntriesDrawerBody(drawer, items)}
+
+
+ `;
+ }
+
+ function renderTimeEntriesDrawerBody(drawer, items) {
+ if (drawer.status === "loading") {
+ return `
+
+
Carregando
+
Buscando dados
+
Consultando o resumo de time entries na origem COMCIP.
+
+ `;
+ }
+
+ if (drawer.status === "error") {
+ return `
+
+
Erro na requisição
+
${escapeHtml(drawer.errorMessage || "Não foi possível carregar os apontamentos.")}
+
+ Tentar novamente
+
+
+ `;
+ }
+
+ const groupedItems = groupTimeEntriesByResourceName(items);
+
+ return `
+
+ ${formatWholeNumber(items.length)} registros
+ ${formatWholeNumber(groupedItems.length)} resources
+
+ ${
+ items.length
+ ? groupedItems.map((group) => renderTimeEntryGroup(group)).join("")
+ : `Nenhum registro retornado para esta SR.
`
+ }
+ `;
+ }
+
+ function renderTimeEntryGroup(group) {
+ const totalTimeSpent = group.items.reduce((sum, item) => {
+ return sum + getTimeEntryTimeSpent(item);
+ }, 0);
+
+ return `
+
+
+
+
resourceName
+
${escapeHtml(group.resourceName)}
+
+ ${formatHours(totalTimeSpent)} total
+
+
+
+
+
+ Activity
+ Task type
+ Time spent
+
+
+
+ ${group.items.map((item) => renderTimeEntryRow(item)).join("")}
+
+
+
+ Total
+ ${formatHours(totalTimeSpent)}
+
+
+
+
+
+ `;
+ }
+
+ function renderTimeEntryRow(item) {
+ return `
+
+ ${escapeHtml(getTimeEntryActivity(item) || "-")}
+ ${escapeHtml(getTimeEntryTaskType(item) || "-")}
+ ${formatHours(getTimeEntryTimeSpent(item))}
+
+ `;
+ }
+
+ function renderDetailTable(items, includeConsumptionStartDate = false) {
+ return `
+
+
+
+
+ ${renderSortButton("Opportunity", DETAIL_SORT_KEYS.opportunityNumber)}
+ ${renderSortButton("Customer", DETAIL_SORT_KEYS.customerName)}
+ ${renderSortButton("Workload", DETAIL_SORT_KEYS.workload)}
+ ${includeConsumptionStartDate ? "consumptionStartDate " : ""}
+ ${renderSortButton("Ramp months", DETAIL_SORT_KEYS.rampMonths, true)}
+ ${renderSortButton("ACR", DETAIL_SORT_KEYS.adjustedACR, true)}
+ ${renderSortButton("Type", DETAIL_SORT_KEYS.opportunityForecastTypeGroup)}
+ ${renderSortButton("Tem SR?", DETAIL_SORT_KEYS.hasSR)}
+ ${renderSortButton("Tem Action?", DETAIL_SORT_KEYS.hasAction)}
+
+
+
+ ${items
+ .map((workload) =>
+ renderWorkloadTableRow(workload, includeConsumptionStartDate, true)
+ )
+ .join("")}
+
+
+
+ `;
+ }
+
+
+ return {
+ renderDetailModal,
+ renderActionFormModal,
+ renderRampComparisonModal,
+ renderForecastUpdateConfirmModal,
+ renderTimeEntriesDrawer,
+ };
+ }
+
+ globalThis.ArchPanelWorkbenchModalsView = Object.freeze({
+ create,
+ });
+})();
diff --git a/tools/bump-extension-version.ps1 b/tools/bump-extension-version.ps1
index b1c5d20..5627487 100644
--- a/tools/bump-extension-version.ps1
+++ b/tools/bump-extension-version.ps1
@@ -61,5 +61,6 @@ $updatedManifest = [regex]::Replace(
1
)
-Set-Content -LiteralPath $manifestPath -Value $updatedManifest -Encoding UTF8
+$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
+[System.IO.File]::WriteAllText($manifestPath, $updatedManifest, $utf8NoBom)
Write-Output "Extension version updated: $currentVersion -> $nextVersion"