(function () { "use strict"; const redwoodStyles = "\n .opportunities-extension-scroll-lock {\n overflow: hidden !important;\n }\n\n .opportunities-extension-modal,\n .opportunities-extension-modal * {\n box-sizing: border-box;\n font-family: \"Oracle Sans\", Arial, Helvetica, sans-serif;\n }\n\n .opportunities-extension-modal {\n position: fixed;\n inset: 0;\n z-index: 2147483647;\n background: #f5f4f2;\n color: #000000;\n }\n\n .opportunities-extension-modal[hidden] {\n display: none !important;\n }\n\n .opportunities-extension-shell {\n height: 100vh;\n min-height: 0;\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n background: #f5f4f2;\n }\n\n .opportunities-extension-header {\n position: relative;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 24px;\n padding: 28px 40px 22px;\n border-bottom: 1px solid #dedbd7;\n background:\n linear-gradient(90deg, #8f624a 0 11%, #c74634 11% 23%, #6f5a7f 23% 36%, #00758f 36% 51%, #d4b06a 51% 63%, transparent 63% 100%) top left / 100% 6px no-repeat,\n #ffffff;\n }\n\n .opportunities-extension-header::before {\n position: absolute;\n left: 40px;\n bottom: -1px;\n width: 64px;\n height: 3px;\n background: #00758f;\n content: \"\";\n }\n\n .opportunities-extension-header h1 {\n margin: 0;\n color: #000000;\n font-size: 24px;\n font-weight: 700;\n line-height: 1.2;\n letter-spacing: 0;\n }\n\n .opportunities-extension-header p {\n margin: 0;\n color: #5f5a55;\n font-size: 14px;\n line-height: 1.4;\n }\n\n .opportunities-extension-subtitle-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin-top: 6px;\n }\n\n .opportunities-extension-auth-badge {\n display: inline-flex;\n align-items: center;\n min-height: 22px;\n padding: 2px 8px;\n border-radius: 999px;\n color: #ffffff;\n font-size: 12px;\n font-weight: 700;\n line-height: 1.2;\n }\n\n .opportunities-extension-auth-badge-authenticated {\n background: #3f6f17;\n }\n\n .opportunities-extension-auth-badge-unauthenticated {\n background: #c5331f;\n }\n\n .opportunities-extension-auth-badge-pending {\n background: #6f5a7f;\n }\n\n .opportunities-extension-products-progress {\n display: inline-flex;\n align-items: center;\n min-height: 22px;\n padding: 2px 8px;\n border-radius: 999px;\n background: #e8f4f6;\n color: #006d7a;\n font-size: 12px;\n font-weight: 700;\n line-height: 1.2;\n }\n\n .opportunities-extension-products-progress[hidden] {\n display: none !important;\n }\n\n .opportunities-extension-products-cache-timestamp {\n display: inline-flex;\n align-items: center;\n min-height: 22px;\n padding: 2px 8px;\n border-radius: 999px;\n background: #f0eeeb;\n color: #5f5a55;\n font-size: 11px;\n font-weight: 600;\n line-height: 1.2;\n }\n\n .opportunities-extension-products-cache-timestamp[hidden] {\n display: none !important;\n }\n\n .opportunities-extension-products-progress[data-state=\"running\"]::before {\n width: 12px;\n height: 12px;\n margin-right: 6px;\n border: 2px solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n content: \"\";\n animation: opportunities-extension-spin .8s linear infinite;\n }\n\n .opportunities-extension-products-progress[data-state=\"complete\"] {\n background: #e2f2e5;\n color: #356d19;\n }\n\n .opportunities-extension-products-progress[data-state=\"complete-with-errors\"],\n .opportunities-extension-products-progress[data-state=\"error\"] {\n background: #f9e3e1;\n color: #a52b1c;\n }\n\n @keyframes opportunities-extension-spin {\n to { transform: rotate(360deg); }\n }\n\n .opportunities-extension-icon-button {\n width: 36px;\n height: 36px;\n flex: 0 0 36px;\n border: 1px solid transparent;\n border-radius: 4px;\n background: transparent;\n color: #312d2a;\n font-size: 18px;\n font-weight: 600;\n line-height: 1;\n cursor: pointer;\n }\n\n .opportunities-extension-header-actions {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n }\n\n .opportunities-extension-icon-button svg {\n width: 18px;\n height: 18px;\n vertical-align: middle;\n }\n\n .opportunities-extension-icon-button .opportunities-extension-settings-icon {\n width: 20px;\n height: 20px;\n }\n\n .opportunities-extension-icon-button:hover,\n .opportunities-extension-icon-button:focus {\n border-color: #b8b2ad;\n background: #f5f4f2;\n outline: none;\n }\n\n .opportunities-extension-body {\n display: grid;\n min-height: 0;\n grid-template-rows: auto auto minmax(0, 1fr);\n gap: 16px;\n padding: 22px 40px 36px;\n background: #f5f4f2;\n overflow: hidden;\n }\n\n .opportunities-extension-form {\n display: flex;\n align-items: end;\n flex-wrap: wrap;\n gap: 24px;\n min-height: 82px;\n max-width: none;\n margin: 0;\n padding: 14px 16px;\n border: 1px solid #dedbd7;\n border-radius: 4px;\n background: #ffffff;\n }\n\n .opportunities-extension-field {\n display: grid;\n max-width: max-content;\n min-width: 270px;\n width: max-content;\n gap: 6px;\n color: #312d2a;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n }\n\n .opportunities-extension-stage-dashboard {\n display: grid;\n grid-template-columns: repeat(6, minmax(0, 1fr));\n gap: 12px;\n }\n\n .opportunities-extension-stage-dashboard-card {\n display: grid;\n gap: 6px;\n min-width: 0;\n padding: 12px 16px;\n border: 1px solid #dedbd7;\n border-top: 3px solid #5f5a55;\n border-radius: 4px;\n background: #ffffff;\n }\n\n .opportunities-extension-stage-dashboard-card span {\n color: #5f5a55;\n font-size: 11px;\n font-weight: 700;\n line-height: 1.2;\n }\n\n .opportunities-extension-stage-dashboard-value {\n overflow: hidden;\n color: #312d2a;\n font-size: 22px;\n font-weight: 700;\n line-height: 1.2;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .opportunities-extension-stage-dashboard-card[data-stage=\"SQL\"] {\n border-top-color: #008aa6;\n }\n\n .opportunities-extension-total-dashboard-card {\n border-top-color: #312d2a;\n }\n\n .opportunities-extension-stage-dashboard-card[data-stage=\"PIPELINE\"] {\n border-top-color: #6f6863;\n }\n\n .opportunities-extension-stage-dashboard-card[data-stage=\"UPSIDE\"] {\n border-top-color: #c87b00;\n }\n\n .opportunities-extension-stage-dashboard-card[data-stage=\"FORECAST\"] {\n border-top-color: #755b8b;\n }\n\n .opportunities-extension-stage-dashboard-card[data-stage=\"WON\"] {\n border-top-color: #4b7f22;\n }\n\n .opportunities-extension-stage-filter {\n display: grid;\n grid-template-rows: auto 44px;\n gap: 6px;\n min-width: 0;\n }\n\n .opportunities-extension-stage-filter-label {\n color: #312d2a;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n }\n\n .opportunities-extension-stage-filter-controls {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n }\n\n .opportunities-extension-stage-filter-button {\n min-height: 36px;\n padding: 6px 12px;\n border: 1px solid currentColor;\n border-radius: 18px;\n background: #ffffff;\n color: #5f5a55;\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0;\n line-height: 1.2;\n cursor: pointer;\n }\n\n .opportunities-extension-stage-filter-button[data-stage=\"SQL\"] {\n color: #006b84;\n }\n\n .opportunities-extension-stage-filter-button[data-stage=\"PIPELINE\"] {\n color: #5f5a55;\n }\n\n .opportunities-extension-stage-filter-button[data-stage=\"UPSIDE\"] {\n color: #945400;\n }\n\n .opportunities-extension-stage-filter-button[data-stage=\"FORECAST\"] {\n color: #624e74;\n }\n\n .opportunities-extension-stage-filter-button[data-stage=\"WON\"] {\n color: #3f6f17;\n }\n\n .opportunities-extension-stage-filter-button[aria-pressed=\"true\"][data-stage=\"SQL\"] {\n border-color: #006b84;\n background: #006b84;\n color: #ffffff;\n }\n\n .opportunities-extension-stage-filter-button[aria-pressed=\"true\"][data-stage=\"PIPELINE\"] {\n border-color: #5f5a55;\n background: #5f5a55;\n color: #ffffff;\n }\n\n .opportunities-extension-stage-filter-button[aria-pressed=\"true\"][data-stage=\"UPSIDE\"] {\n border-color: #945400;\n background: #945400;\n color: #ffffff;\n }\n\n .opportunities-extension-stage-filter-button[aria-pressed=\"true\"][data-stage=\"FORECAST\"] {\n border-color: #624e74;\n background: #624e74;\n color: #ffffff;\n }\n\n .opportunities-extension-stage-filter-button[aria-pressed=\"true\"][data-stage=\"WON\"] {\n border-color: #3f6f17;\n background: #3f6f17;\n color: #ffffff;\n }\n\n .opportunities-extension-stage-filter-button:focus-visible {\n outline: 2px solid #00758f;\n outline-offset: 2px;\n }\n\n .opportunities-extension-status-filter {\n display: grid;\n grid-template-rows: auto 44px;\n gap: 6px;\n min-width: 0;\n }\n\n .opportunities-extension-status-filter-label {\n color: #312d2a;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n }\n\n .opportunities-extension-status-filter-controls {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n }\n\n .opportunities-extension-status-filter-button {\n min-height: 36px;\n padding: 6px 12px;\n border: 1px solid currentColor;\n border-radius: 18px;\n background: #ffffff;\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0;\n line-height: 1.2;\n cursor: pointer;\n }\n\n .opportunities-extension-status-filter-button[data-status=\"OPEN\"] {\n color: #197a3d;\n }\n\n .opportunities-extension-status-filter-button[data-status=\"CLOSED\"] {\n color: #4b5563;\n }\n\n .opportunities-extension-status-filter-button[data-status=\"LOST\"] {\n color: #b13b34;\n }\n\n .opportunities-extension-status-filter-button[data-status=\"WON\"] {\n color: #1f5f99;\n }\n\n .opportunities-extension-status-filter-button[aria-pressed=\"true\"][data-status=\"OPEN\"] {\n border-color: #197a3d;\n background: #197a3d;\n color: #ffffff;\n }\n\n .opportunities-extension-status-filter-button[aria-pressed=\"true\"][data-status=\"CLOSED\"] {\n border-color: #4b5563;\n background: #4b5563;\n color: #ffffff;\n }\n\n .opportunities-extension-status-filter-button[aria-pressed=\"true\"][data-status=\"LOST\"] {\n border-color: #b13b34;\n background: #b13b34;\n color: #ffffff;\n }\n\n .opportunities-extension-status-filter-button[aria-pressed=\"true\"][data-status=\"WON\"] {\n border-color: #1f5f99;\n background: #1f5f99;\n color: #ffffff;\n }\n\n .opportunities-extension-status-filter-button:focus-visible {\n outline: 2px solid #00758f;\n outline-offset: 2px;\n }\n\n .opportunities-extension-customer-filter {\n position: relative;\n display: grid;\n grid-template-rows: auto 44px;\n gap: 6px;\n min-width: 220px;\n }\n\n .opportunities-extension-customer-filter-label {\n color: #312d2a;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n }\n\n .opportunities-extension-customer-filter-trigger {\n position: relative;\n min-width: 220px;\n min-height: 44px;\n padding: 10px 38px 10px 12px;\n border: 1px solid #b8b2ad;\n border-radius: 3px;\n background: #ffffff;\n color: #312d2a;\n font-size: 14px;\n line-height: 1.3;\n text-align: left;\n cursor: pointer;\n }\n\n .opportunities-extension-customer-filter-trigger::after {\n position: absolute;\n top: 50%;\n right: 16px;\n width: 0;\n height: 0;\n border-top: 6px solid #312d2a;\n border-right: 5px solid transparent;\n border-left: 5px solid transparent;\n content: \"\";\n pointer-events: none;\n transform: translateY(-50%);\n }\n\n .opportunities-extension-customer-filter-trigger:disabled {\n cursor: not-allowed;\n background: #f5f4f2;\n color: #7d7772;\n }\n\n .opportunities-extension-customer-filter-trigger:focus-visible {\n border-color: #312d2a;\n box-shadow: 0 0 0 1px #312d2a;\n outline: none;\n }\n\n .opportunities-extension-customer-filter-panel {\n position: absolute;\n top: calc(100% + 8px);\n left: 0;\n z-index: 4;\n display: grid;\n width: min(360px, calc(100vw - 80px));\n gap: 10px;\n padding: 12px;\n border: 1px solid #8f8a85;\n border-radius: 4px;\n background: #ffffff;\n box-shadow: 0 4px 12px rgba(0, 0, 0, .18);\n }\n\n .opportunities-extension-customer-filter-panel[hidden] {\n display: none !important;\n }\n\n .opportunities-extension-customer-filter-search {\n width: 100%;\n min-height: 40px;\n padding: 8px 10px;\n border: 1px solid #b8b2ad;\n border-radius: 3px;\n background: #ffffff;\n color: #312d2a;\n font-size: 14px;\n }\n\n .opportunities-extension-customer-filter-search:focus {\n border-color: #312d2a;\n box-shadow: 0 0 0 1px #312d2a;\n outline: none;\n }\n\n .opportunities-extension-customer-filter-select-all,\n .opportunities-extension-customer-filter-option {\n display: flex;\n align-items: center;\n gap: 8px;\n color: #312d2a;\n font-size: 13px;\n line-height: 1.35;\n }\n\n .opportunities-extension-customer-filter-select-all {\n min-height: 32px;\n padding-bottom: 8px;\n border-bottom: 1px solid #dedbd7;\n font-weight: 700;\n }\n\n .opportunities-extension-customer-filter-panel input[type=\"checkbox\"] {\n width: 16px;\n height: 16px;\n flex: 0 0 16px;\n accent-color: #00758f;\n }\n\n .opportunities-extension-customer-filter-list {\n display: block;\n max-height: 250px;\n overflow: auto;\n }\n\n .opportunities-extension-customer-filter-option {\n display: grid;\n grid-template-columns: 16px minmax(0, 1fr);\n align-items: start;\n min-height: 0;\n height: auto !important;\n padding: 8px 4px;\n cursor: pointer;\n }\n\n .opportunities-extension-customer-filter-option input[type=\"checkbox\"] {\n margin-top: 1px;\n }\n\n .opportunities-extension-customer-filter-option span {\n display: block;\n min-width: 0;\n line-height: 18px;\n overflow-wrap: anywhere;\n white-space: normal;\n }\n\n .opportunities-extension-customer-filter-option:hover {\n background: #f5f4f2;\n }\n\n .opportunities-extension-customer-filter-empty {\n margin: 4px 0;\n color: #5f5a55;\n font-size: 13px;\n }\n\n .opportunities-extension-table-search-field {\n display: grid;\n grid-template-rows: auto 44px;\n gap: 6px;\n width: 260px;\n color: #312d2a;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n }\n\n .opportunities-extension-table-search-field input {\n width: 100%;\n min-height: 44px;\n padding: 10px 12px;\n border: 1px solid #b8b2ad;\n border-radius: 3px;\n background: #ffffff;\n color: #312d2a;\n font-size: 14px;\n line-height: 1.3;\n }\n\n .opportunities-extension-table-search-field input:focus {\n border-color: #312d2a;\n box-shadow: 0 0 0 1px #312d2a;\n outline: none;\n }\n\n .opportunities-extension-select-wrap {\n position: relative;\n display: block;\n width: 270px;\n max-width: 100%;\n }\n\n .opportunities-extension-select-wrap::after {\n position: absolute;\n top: 50%;\n right: 16px;\n width: 0;\n height: 0;\n border-left: 5px solid transparent;\n border-right: 5px solid transparent;\n border-top: 6px solid #000000;\n content: \"\";\n pointer-events: none;\n transform: translateY(-50%);\n }\n\n .opportunities-extension-field select {\n width: 100%;\n min-width: 0;\n max-width: 100%;\n min-height: 44px;\n box-sizing: border-box;\n appearance: none;\n border: 1px solid #b8b2ad;\n border-radius: 3px;\n background: #ffffff;\n color: #000000;\n font-size: 14px;\n line-height: 1.3;\n padding: 11px 44px 11px 12px;\n }\n\n .opportunities-extension-field select:focus {\n border-color: #312d2a;\n box-shadow: 0 0 0 1px #312d2a;\n outline: none;\n }\n\n .opportunities-extension-table-surface {\n min-height: 0;\n height: 100%;\n border: 1px solid #dedbd7;\n border-radius: 4px;\n background: #ffffff;\n overflow-x: auto;\n overflow-y: auto;\n }\n\n .opportunities-extension-table {\n width: 100%;\n min-width: 0;\n border-collapse: collapse;\n table-layout: fixed;\n color: #312d2a;\n font-size: 13px;\n line-height: 1.35;\n }\n\n .opportunities-extension-table thead {\n background: #faf9f8;\n }\n\n .opportunities-extension-table th {\n position: sticky;\n top: 0;\n z-index: 1;\n height: 44px;\n border-bottom: 1px solid #b8b2ad;\n background: #faf9f8;\n color: #312d2a;\n font-size: 12px;\n font-weight: 700;\n text-align: left;\n white-space: nowrap;\n }\n\n .opportunities-extension-table td {\n min-height: 48px;\n padding: 12px 16px;\n border-bottom: 1px solid #ebe8e5;\n vertical-align: middle;\n overflow-wrap: anywhere;\n }\n\n .opportunities-extension-table th:nth-child(1) {\n width: 16%;\n }\n\n .opportunities-extension-table th:nth-child(2) {\n width: 8%;\n }\n\n .opportunities-extension-table th:nth-child(3) {\n width: 18%;\n }\n\n .opportunities-extension-table th:nth-child(4) {\n width: 11%;\n }\n\n .opportunities-extension-table th:nth-child(5) {\n width: 8%;\n }\n\n .opportunities-extension-table th:nth-child(6) {\n width: 8%;\n }\n\n .opportunities-extension-table th:nth-child(7) {\n width: 7%;\n }\n\n .opportunities-extension-table th:nth-child(8) {\n width: 6%;\n }\n\n .opportunities-extension-table th:nth-child(9) {\n width: 7%;\n }\n\n .opportunities-extension-table th:nth-child(10) {\n width: 7%;\n }\n\n .opportunities-extension-table th:nth-child(11) {\n width: 4%;\n }\n\n .opportunities-extension-table td:nth-child(2),\n .opportunities-extension-table td:nth-child(5),\n .opportunities-extension-table td:nth-child(6),\n .opportunities-extension-table td:nth-child(8) {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .opportunities-extension-table td:nth-child(9) {\n padding-right: 6px;\n padding-left: 6px;\n white-space: nowrap;\n overflow: visible;\n }\n\n .opportunities-extension-table td:nth-child(7) {\n padding-right: 6px;\n padding-left: 6px;\n white-space: nowrap;\n overflow: visible;\n }\n\n .opportunities-extension-table td:nth-child(10) {\n padding-right: 6px;\n padding-left: 6px;\n font-size: 12px;\n white-space: nowrap;\n }\n\n .opportunities-extension-table td:nth-child(11) {\n padding-right: 6px;\n padding-left: 6px;\n text-align: center;\n white-space: nowrap;\n }\n\n .opportunities-extension-table tbody tr:hover {\n background: #f7f6f4;\n }\n\n .opportunities-extension-table tbody tr:last-child td {\n border-bottom: 0;\n }\n\n .opportunities-extension-table a {\n color: #006b84;\n text-decoration: underline;\n text-decoration-thickness: 1px;\n text-underline-offset: 2px;\n }\n\n .opportunities-extension-table a:hover {\n color: #004f63;\n }\n\n .opportunities-extension-opty-number-content {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n }\n\n .opportunities-extension-name-content {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n }\n\n .opportunities-extension-name-content > a {\n min-width: 0;\n }\n\n .opportunities-extension-actions-content {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 4px;\n white-space: nowrap;\n }\n\n .opportunities-extension-products-count {\n display: inline-grid;\n width: auto;\n min-width: 20px;\n height: 20px;\n place-items: center;\n flex: 0 0 auto;\n box-sizing: border-box;\n padding: 0 3px;\n border: 1px solid #7ca7b2;\n border-radius: 999px;\n background: #e8f4f6;\n color: #006d7a;\n font-size: 10px;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n vertical-align: middle;\n cursor: pointer;\n }\n\n .opportunities-extension-products-count:hover,\n .opportunities-extension-products-count:focus-visible,\n .opportunities-extension-products-count[aria-expanded=\"true\"] {\n border-color: #00758f;\n background: #00758f;\n color: #ffffff;\n outline: none;\n }\n\n .opportunities-extension-products-count-error {\n border-color: #d8887e;\n background: #f9e3e1;\n color: #a52b1c;\n }\n\n .opportunities-extension-products-detail-row:hover {\n background: transparent !important;\n }\n\n .opportunities-extension-products-detail-cell {\n padding: 0 !important;\n }\n\n .opportunities-extension-products-detail {\n padding: 14px 20px 18px;\n border-bottom: 1px solid #c9c5c1;\n background: #f0eeeb;\n }\n\n .opportunities-extension-products-detail-heading {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 10px;\n color: #312d2a;\n }\n\n .opportunities-extension-ai-summary {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n margin: 0 0 12px;\n padding: 12px 16px;\n border: 1px solid #dedbd7;\n border-radius: 6px;\n background: #ffffff;\n color: #312d2a;\n font-size: 13px;\n line-height: 1.5;\n text-align: left;\n white-space: normal;\n overflow-wrap: anywhere;\n }\n\n .opportunities-extension-ai-summary strong {\n display: block;\n font-size: 13px;\n font-weight: 600;\n }\n\n .opportunities-extension-ai-summary p {\n margin: 4px 0 0;\n font: inherit;\n white-space: pre-wrap;\n }\n\n .opportunities-extension-ai-summary-error {\n color: #b42318;\n border-color: #d9928b;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-ai-summary {\n background: #292827;\n color: #f6f4f2;\n border-color: #5f5a55;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-ai-summary-error {\n color: #ffb4ab;\n border-color: #a45c55;\n }\n\n .opportunities-extension-products-detail-heading strong {\n font-size: 13px;\n }\n\n .opportunities-extension-products-detail-heading span {\n color: #5f5a55;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n }\n\n .opportunities-extension-products-table {\n width: 100%;\n table-layout: fixed;\n border-collapse: collapse;\n border: 1px solid #dedbd7;\n background: #ffffff;\n font-size: 12px;\n }\n\n .opportunities-extension-products-table th,\n .opportunities-extension-products-table td {\n position: static;\n height: auto;\n padding: 8px 10px;\n border-bottom: 1px solid #ebe8e5;\n background: transparent;\n overflow-wrap: anywhere;\n vertical-align: middle;\n white-space: normal;\n }\n\n .opportunities-extension-products-table th {\n background: #faf9f8;\n font-size: 11px;\n }\n\n .opportunities-extension-products-table th:nth-child(1) { width: 16%; }\n .opportunities-extension-products-table th:nth-child(2) { width: 16%; }\n .opportunities-extension-products-table th:nth-child(3) { width: 8%; }\n .opportunities-extension-products-table th:nth-child(4) { width: 10%; }\n .opportunities-extension-products-table th:nth-child(5) { width: 8%; }\n .opportunities-extension-products-table th:nth-child(6) { width: 8%; }\n .opportunities-extension-products-table th:nth-child(7) { width: 7%; }\n .opportunities-extension-products-table th:nth-child(8) { width: 10%; }\n .opportunities-extension-products-table th:nth-child(9) { width: 12%; }\n .opportunities-extension-products-table th:nth-child(10) { width: 5%; }\n\n .opportunities-extension-products-empty {\n margin: 0;\n padding: 10px 0;\n color: #5f5a55;\n font-size: 13px;\n }\n\n .opportunities-extension-copy-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n width: 24px;\n height: 24px;\n padding: 3px;\n border: 0;\n border-radius: 3px;\n background: transparent;\n color: #00758f;\n cursor: pointer;\n }\n\n .opportunities-extension-copy-button svg {\n width: 16px;\n height: 16px;\n fill: currentColor;\n }\n\n .opportunities-extension-copy-button:hover,\n .opportunities-extension-copy-button:focus-visible {\n background: #e8f4f6;\n color: #004f63;\n outline: none;\n }\n\n .opportunities-extension-action-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n padding: 5px;\n border: 1px solid transparent;\n border-radius: 4px;\n background: transparent;\n color: #00758f;\n cursor: pointer;\n }\n\n .opportunities-extension-action-button svg {\n width: 21px;\n height: 21px;\n }\n\n .opportunities-extension-action-button:hover,\n .opportunities-extension-action-button:focus-visible {\n border-color: #7ca7b2;\n background: #e8f4f6;\n color: #004f63;\n outline: none;\n }\n\n .opportunities-extension-action-button:disabled {\n cursor: wait;\n opacity: .72;\n }\n\n .opportunities-extension-action-button-loading svg {\n animation: opportunities-extension-pulse 1s ease-in-out infinite;\n }\n\n @keyframes opportunities-extension-pulse {\n 50% { opacity: .35; }\n }\n\n .opportunities-extension-copy-toast {\n position: fixed;\n right: 28px;\n bottom: 28px;\n z-index: 3;\n max-width: min(360px, calc(100vw - 56px));\n padding: 12px 16px;\n border-radius: 4px;\n background: #2f6f18;\n box-shadow: 0 4px 12px rgba(0, 0, 0, .18);\n color: #ffffff;\n font-size: 13px;\n font-weight: 700;\n }\n\n .opportunities-extension-copy-toast-error {\n background: #a52b1c;\n }\n\n .opportunities-extension-copy-toast[hidden] {\n display: none !important;\n }\n\n .opportunities-extension-stage-badge {\n display: inline-flex;\n align-items: center;\n min-height: 24px;\n padding: 3px 8px;\n border-radius: 12px;\n background: #ebe8e5;\n color: #312d2a;\n font-size: 11px;\n font-weight: 700;\n line-height: 1.2;\n white-space: nowrap;\n }\n\n .opportunities-extension-stage-badge[data-stage=\"SQL\"] {\n background: #d9f0f5;\n color: #006b84;\n }\n\n .opportunities-extension-stage-badge[data-stage=\"PIPELINE\"] {\n background: #ebe8e5;\n color: #5f5a55;\n }\n\n .opportunities-extension-stage-badge[data-stage=\"UPSIDE\"] {\n background: #fff0d8;\n color: #945400;\n }\n\n .opportunities-extension-stage-badge[data-stage=\"FORECAST\"] {\n background: #eee8f5;\n color: #624e74;\n }\n\n .opportunities-extension-stage-badge[data-stage=\"WON\"] {\n background: #e5f1d9;\n color: #3f6f17;\n }\n\n .opportunities-extension-status-badge {\n display: inline-flex;\n align-items: center;\n min-height: 24px;\n padding: 3px 8px;\n border-radius: 12px;\n background: #ebe8e5;\n color: #312d2a;\n font-size: 11px;\n font-weight: 700;\n line-height: 1.2;\n white-space: nowrap;\n }\n\n .opportunities-extension-status-badge[data-status=\"OPEN\"] {\n background: #e2f2e5;\n color: #197a3d;\n }\n\n .opportunities-extension-status-badge[data-status=\"CLOSED\"] {\n background: #e5e7eb;\n color: #4b5563;\n }\n\n .opportunities-extension-status-badge[data-status=\"LOST\"] {\n background: #f9e3e1;\n color: #b13b34;\n }\n\n .opportunities-extension-status-badge[data-status=\"WON\"] {\n background: #e3edf8;\n color: #1f5f99;\n }\n\n .opportunities-extension-sort-button {\n position: relative;\n display: inline-flex;\n align-items: center;\n width: 100%;\n min-height: 44px;\n padding: 8px 42px 8px 16px;\n border: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n font-weight: inherit;\n letter-spacing: 0;\n line-height: 1.2;\n text-align: left;\n white-space: normal;\n cursor: pointer;\n }\n\n .opportunities-extension-table-header-label {\n display: inline-flex;\n align-items: center;\n min-height: 44px;\n padding: 8px 6px;\n }\n\n .opportunities-extension-sort-button::after {\n position: absolute;\n right: 15px;\n color: #5f5a55;\n content: \"↕\";\n font-size: 16px;\n font-weight: 400;\n }\n\n .opportunities-extension-sort-button[data-sort-direction=\"ascending\"]::after {\n color: #00758f;\n content: \"↑\";\n }\n\n .opportunities-extension-sort-button[data-sort-direction=\"descending\"]::after {\n color: #00758f;\n content: \"↓\";\n }\n\n .opportunities-extension-sort-button:hover {\n background: #f0eeeb;\n }\n\n .opportunities-extension-sort-button:focus-visible {\n outline: 2px solid #00758f;\n outline-offset: -2px;\n }\n\n .opportunities-extension-table-status {\n height: 152px;\n color: #5f5a55;\n font-size: 14px;\n text-align: center;\n }\n\n .opportunities-extension-visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n }\n\n .opportunities-extension-preferences-modal {\n position: absolute;\n inset: 0;\n z-index: 3;\n display: grid;\n place-items: center;\n padding: 24px;\n background: rgba(0, 0, 0, .42);\n }\n\n .opportunities-extension-preferences-modal[hidden] {\n display: none !important;\n }\n\n .opportunities-extension-account-plans-modal {\n position: absolute;\n inset: 0;\n z-index: 4;\n display: grid;\n place-items: center;\n padding: 12px;\n background: rgba(0, 0, 0, .42);\n }\n\n .opportunities-extension-account-plans-modal[hidden] {\n display: none !important;\n }\n\n .opportunities-extension-account-plans-dialog {\n display: grid;\n width: min(1180px, 100%);\n height: min(760px, calc(100vh - 24px));\n min-width: 0;\n grid-template-rows: auto minmax(0, 1fr);\n grid-template-columns: minmax(0, 1fr);\n overflow: hidden;\n border: 1px solid #8f8a85;\n border-radius: 4px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(0, 0, 0, .24);\n color: #312d2a;\n }\n\n .opportunities-extension-account-plans-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n padding: 14px 20px;\n border-bottom: 1px solid #dedbd7;\n }\n\n .opportunities-extension-account-plans-header h2 {\n margin: 0;\n font-size: 20px;\n line-height: 1.2;\n }\n\n .opportunities-extension-account-plans-header p {\n margin: 5px 0 0;\n color: #5f5a55;\n font-size: 13px;\n line-height: 1.35;\n }\n\n .opportunities-extension-account-plans-content {\n display: grid;\n min-width: 0;\n grid-template-rows: auto minmax(0, 1fr);\n grid-template-columns: minmax(0, 1fr);\n min-height: 0;\n overflow: hidden;\n }\n\n .opportunities-extension-account-plans-content > .opportunities-extension-account-plans-skeleton {\n grid-row: 1 / -1;\n }\n\n .opportunities-extension-account-plans-tabs-navigation {\n position: relative;\n min-width: 0;\n border-bottom: 1px solid #dedbd7;\n }\n\n .opportunities-extension-account-plans-tabs-navigation .opportunities-extension-account-plans-tab-navigation-button[hidden] {\n display: none;\n }\n\n .opportunities-extension-account-plans-tabs {\n display: flex;\n min-width: 0;\n gap: 4px;\n overflow-x: auto;\n padding: 8px 20px 0;\n scrollbar-width: none;\n }\n\n .opportunities-extension-account-plans-tabs::-webkit-scrollbar {\n display: none;\n }\n\n .opportunities-extension-account-plans-tab-navigation-button {\n display: grid;\n position: absolute;\n z-index: 1;\n top: 50%;\n width: 32px;\n height: 32px;\n place-items: center;\n padding: 0;\n border: 1px solid #8f8a85;\n border-radius: 4px;\n background: #ffffff;\n color: #312d2a;\n box-shadow: 0 2px 6px rgba(0, 0, 0, .16);\n transform: translateY(-50%);\n cursor: pointer;\n }\n\n .opportunities-extension-account-plans-tab-navigation-button:first-child {\n left: 8px;\n }\n\n .opportunities-extension-account-plans-tab-navigation-button:last-child {\n right: 8px;\n }\n\n .opportunities-extension-account-plans-tab-navigation-button:hover:not(:disabled) {\n border-color: #00758f;\n background: #f0eeeb;\n color: #005e73;\n }\n\n .opportunities-extension-account-plans-tab-navigation-button:disabled {\n border-color: #dedbd7;\n color: #b8b2ad;\n cursor: default;\n }\n\n .opportunities-extension-account-plans-tab-navigation-button:focus-visible {\n outline: 2px solid #00758f;\n outline-offset: 2px;\n }\n\n .opportunities-extension-account-plans-tab-navigation-button svg {\n width: 18px;\n height: 18px;\n }\n\n .opportunities-extension-account-plans-panels {\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n }\n\n .opportunities-extension-account-plan-tab {\n min-height: 38px;\n flex: 0 0 auto;\n padding: 7px 12px;\n border: 0;\n border-bottom: 3px solid transparent;\n background: transparent;\n color: #5f5a55;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n }\n\n .opportunities-extension-account-plan-tab:hover {\n background: #f0eeeb;\n color: #312d2a;\n }\n\n .opportunities-extension-account-plan-tab[aria-selected=\"true\"] {\n border-bottom-color: #00758f;\n color: #006d7a;\n }\n\n .opportunities-extension-account-plan-tab:focus-visible {\n outline: 2px solid #00758f;\n outline-offset: -2px;\n }\n\n .opportunities-extension-account-plan-panel {\n display: grid;\n height: 100%;\n min-height: 0;\n grid-template-rows: auto minmax(0, 1fr);\n box-sizing: border-box;\n padding: 16px 20px;\n }\n\n .opportunities-extension-account-plan-panel[hidden] {\n display: none !important;\n }\n\n .opportunities-extension-account-plan-details {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 0 24px;\n margin: 0;\n }\n\n .opportunities-extension-account-plan-details > div {\n display: grid;\n grid-template-columns: minmax(104px, 38%) minmax(0, 1fr);\n gap: 10px;\n min-height: 42px;\n align-items: center;\n padding: 7px 0;\n border-bottom: 1px solid #ebe8e5;\n }\n\n .opportunities-extension-account-plan-details dt {\n color: #5f5a55;\n font-size: 12px;\n font-weight: 700;\n }\n\n .opportunities-extension-account-plan-details dd {\n min-width: 0;\n margin: 0;\n color: #312d2a;\n font-size: 13px;\n overflow-wrap: anywhere;\n }\n\n .opportunities-extension-consumption-trend {\n display: grid;\n min-height: 0;\n grid-template-rows: auto minmax(0, 1fr);\n margin-top: 14px;\n padding-top: 12px;\n border-top: 1px solid #dedbd7;\n }\n\n .opportunities-extension-consumption-trend-header {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 24px;\n }\n\n .opportunities-extension-consumption-periods {\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n .opportunities-extension-consumption-periods button {\n min-height: 38px;\n padding: 7px 14px;\n border: 1px solid transparent;\n border-radius: 4px;\n background: transparent;\n color: #312d2a;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.2;\n cursor: pointer;\n }\n\n .opportunities-extension-consumption-periods button:hover {\n background: #f0eeeb;\n }\n\n .opportunities-extension-consumption-periods button[aria-pressed=\"true\"] {\n border-color: #00758f;\n background: #e7f6f8;\n color: #005e73;\n }\n\n .opportunities-extension-consumption-periods button:focus-visible,\n .opportunities-extension-consumption-navigation-button:focus-visible {\n outline: 2px solid #00758f;\n outline-offset: 2px;\n }\n\n .opportunities-extension-consumption-chart {\n display: grid;\n min-height: 0;\n grid-template-rows: auto minmax(0, 1fr);\n margin-top: 4px;\n }\n\n .opportunities-extension-consumption-navigation {\n display: grid;\n grid-template-columns: 36px minmax(130px, auto) 36px;\n align-items: center;\n justify-content: center;\n gap: 8px;\n min-height: 40px;\n margin-bottom: 2px;\n }\n\n .opportunities-extension-consumption-navigation strong {\n color: #312d2a;\n font-size: 13px;\n text-align: center;\n }\n\n .opportunities-extension-consumption-navigation-button {\n display: grid;\n width: 36px;\n height: 36px;\n place-items: center;\n padding: 0;\n border: 1px solid #8f8a85;\n border-radius: 4px;\n background: #ffffff;\n color: #312d2a;\n cursor: pointer;\n }\n\n .opportunities-extension-consumption-navigation-button:hover:not(:disabled) {\n border-color: #00758f;\n background: #f0eeeb;\n color: #005e73;\n }\n\n .opportunities-extension-consumption-navigation-button:disabled {\n border-color: #dedbd7;\n color: #b8b2ad;\n cursor: default;\n }\n\n .opportunities-extension-consumption-navigation-button svg {\n width: 18px;\n height: 18px;\n }\n\n .opportunities-extension-consumption-figure {\n position: relative;\n min-width: 0;\n min-height: 0;\n margin: 0;\n scrollbar-width: none;\n }\n\n .opportunities-extension-consumption-figure::-webkit-scrollbar {\n display: none;\n }\n\n .opportunities-extension-consumption-visual {\n display: grid;\n height: 100%;\n min-height: 0;\n grid-template-columns: minmax(0, 1fr) 176px;\n align-items: center;\n gap: 16px;\n }\n\n .opportunities-extension-consumption-svg {\n display: block;\n width: 100%;\n height: 300px;\n min-height: 0;\n overflow: visible;\n }\n\n .opportunities-extension-consumption-svg .consumption-grid-line {\n stroke: #dedbd7;\n stroke-width: 1;\n shape-rendering: crispEdges;\n }\n\n .opportunities-extension-consumption-svg .consumption-axis-label,\n .opportunities-extension-consumption-svg .consumption-x-label {\n fill: #5f5a55;\n font-family: Arial, sans-serif;\n font-size: 12px;\n }\n\n .opportunities-extension-consumption-svg .consumption-value-label {\n fill: #312d2a;\n font-family: Arial, sans-serif;\n font-size: 12px;\n font-weight: 700;\n }\n\n .opportunities-extension-consumption-svg .consumption-bar-funded {\n fill: #b77888;\n }\n\n .opportunities-extension-consumption-svg .consumption-bar-overage {\n fill: #de7907;\n }\n\n .opportunities-extension-consumption-svg .consumption-average-line {\n stroke: #de7907;\n stroke-width: 1.5;\n stroke-dasharray: 6 6;\n }\n\n .opportunities-extension-consumption-svg .consumption-bar-group {\n cursor: default;\n outline: none;\n }\n\n .opportunities-extension-consumption-svg .consumption-bar-group rect {\n transition: filter .16s ease, opacity .16s ease, stroke .16s ease;\n }\n\n .opportunities-extension-consumption-svg .consumption-bar-group:hover rect,\n .opportunities-extension-consumption-svg .consumption-bar-group:focus rect {\n filter: brightness(1.06);\n stroke: #312d2a;\n stroke-width: 1.5;\n }\n\n .opportunities-extension-consumption-svg .consumption-animated-label {\n opacity: 0;\n animation: opportunities-extension-consumption-label-in .24s ease-out forwards;\n }\n\n .opportunities-extension-consumption-svg .consumption-animated-average {\n opacity: 0;\n animation: opportunities-extension-consumption-average-in .35s .32s ease-out forwards;\n }\n\n @keyframes opportunities-extension-consumption-label-in {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n @keyframes opportunities-extension-consumption-average-in {\n from { opacity: 0; }\n to { opacity: 1; }\n }\n\n .opportunities-extension-consumption-legend {\n display: grid;\n align-content: center;\n gap: 4px;\n }\n\n .opportunities-extension-consumption-legend button {\n display: grid;\n min-width: 0;\n grid-template-columns: 16px minmax(0, 1fr);\n align-items: center;\n gap: 8px;\n padding: 6px 4px;\n border: 0;\n border-radius: 4px;\n background: transparent;\n color: #312d2a;\n font-size: 12px;\n line-height: 1.25;\n text-align: left;\n cursor: pointer;\n }\n\n .opportunities-extension-consumption-legend button:hover {\n background: #f0eeeb;\n }\n\n .opportunities-extension-consumption-legend button:focus-visible {\n outline: 2px solid #00758f;\n outline-offset: 1px;\n }\n\n .opportunities-extension-consumption-legend button[aria-pressed=\"false\"] {\n color: #77726d;\n opacity: .55;\n }\n\n .opportunities-extension-consumption-legend-swatch {\n display: block;\n width: 12px;\n height: 12px;\n justify-self: center;\n }\n\n .opportunities-extension-consumption-legend-funded {\n background: #b77888;\n }\n\n .opportunities-extension-consumption-legend-overage {\n background: #de7907;\n }\n\n .opportunities-extension-consumption-legend-average {\n height: 0;\n border-top: 2px dashed #de7907;\n }\n\n .opportunities-extension-consumption-tooltip {\n position: absolute;\n z-index: 2;\n display: grid;\n min-width: 190px;\n max-width: 260px;\n gap: 4px;\n padding: 10px 12px;\n border: 1px solid #8f8a85;\n border-radius: 4px;\n background: #ffffff;\n box-shadow: 0 4px 12px rgba(0, 0, 0, .2);\n color: #312d2a;\n font-size: 12px;\n line-height: 1.3;\n pointer-events: none;\n }\n\n .opportunities-extension-consumption-tooltip[hidden] {\n display: none !important;\n }\n\n .opportunities-extension-consumption-tooltip strong {\n margin-bottom: 2px;\n font-size: 13px;\n }\n\n .opportunities-extension-consumption-chart-skeleton {\n height: 100%;\n min-height: 0;\n padding: 34px 62px 24px;\n box-sizing: border-box;\n }\n\n .opportunities-extension-consumption-chart-skeleton-plot {\n display: flex;\n height: 100%;\n min-height: 220px;\n align-items: flex-end;\n justify-content: space-around;\n gap: 12px;\n padding: 0 14px;\n border-bottom: 1px solid #dedbd7;\n border-left: 1px solid #dedbd7;\n }\n\n .opportunities-extension-consumption-chart-skeleton-plot span {\n width: min(38px, 6%);\n border-radius: 2px 2px 0 0;\n background: #dedbd7;\n animation: opportunities-extension-skeleton-pulse 1.15s ease-in-out infinite alternate;\n }\n\n .opportunities-extension-consumption-chart-empty,\n .opportunities-extension-consumption-chart-error {\n display: grid;\n min-height: 220px;\n place-items: center;\n margin: 0;\n color: #5f5a55;\n font-size: 14px;\n text-align: center;\n }\n\n .opportunities-extension-consumption-chart-error {\n color: #9f2d20;\n }\n\n .opportunities-extension-account-plan-status-indicator {\n display: inline-block;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #9b9691;\n box-shadow: 0 0 0 3px #efedeb;\n vertical-align: middle;\n }\n\n .opportunities-extension-account-plan-status-indicator[data-active=\"true\"] {\n background: #3f6f17;\n box-shadow: 0 0 0 3px #e2f2e5;\n }\n\n .opportunities-extension-account-plan-tab .opportunities-extension-account-plan-status-indicator {\n margin-right: 8px;\n }\n\n .opportunities-extension-account-plans-skeleton-tabs {\n display: flex;\n min-height: 46px;\n align-items: center;\n padding: 0 20px;\n border-bottom: 1px solid #dedbd7;\n }\n\n .opportunities-extension-account-plans-skeleton {\n display: grid;\n height: 100%;\n min-height: 0;\n grid-template-rows: auto minmax(0, 1fr);\n overflow: hidden;\n }\n\n .opportunities-extension-account-plans-skeleton-panel {\n padding: 16px 20px;\n }\n\n .opportunities-extension-account-plans-skeleton-tab {\n display: inline-flex;\n align-items: center;\n gap: 9px;\n min-height: 38px;\n padding: 0 12px;\n border-bottom: 3px solid #00758f;\n }\n\n .opportunities-extension-account-plans-skeleton-status {\n display: block;\n width: 10px;\n height: 10px;\n flex: 0 0 auto;\n border-radius: 50%;\n background: #dedbd7;\n box-shadow: 0 0 0 3px #efedeb;\n animation: opportunities-extension-skeleton-pulse 1.15s ease-in-out infinite alternate;\n }\n\n .opportunities-extension-account-plans-skeleton-details {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 0 24px;\n padding: 0;\n }\n\n .opportunities-extension-account-plans-skeleton-details > div {\n display: grid;\n grid-template-columns: minmax(104px, 38%) minmax(0, 1fr);\n align-items: center;\n gap: 10px;\n min-height: 42px;\n padding: 7px 0;\n border-bottom: 1px solid #ebe8e5;\n }\n\n .opportunities-extension-account-plans-skeleton-bar {\n display: block;\n height: 12px;\n border-radius: 3px;\n background: #dedbd7;\n animation: opportunities-extension-skeleton-pulse 1.15s ease-in-out infinite alternate;\n }\n\n .opportunities-extension-account-plans-skeleton-tabs .opportunities-extension-account-plans-skeleton-bar {\n height: 14px;\n }\n\n .opportunities-extension-account-plans-skeleton-label {\n width: 74%;\n height: 10px;\n }\n\n .opportunities-extension-account-plans-skeleton-trend {\n display: grid;\n min-height: 0;\n grid-template-rows: auto auto minmax(0, 1fr);\n margin: 14px 0 0;\n padding-top: 12px;\n border-top: 1px solid #dedbd7;\n }\n\n .opportunities-extension-account-plans-skeleton-trend-header {\n display: flex;\n justify-content: flex-end;\n min-height: 38px;\n }\n\n .opportunities-extension-account-plans-skeleton-periods {\n display: flex;\n align-items: center;\n gap: 16px;\n }\n\n .opportunities-extension-account-plans-skeleton-periods .opportunities-extension-account-plans-skeleton-bar {\n height: 12px;\n }\n\n .opportunities-extension-account-plans-skeleton-period-active {\n height: 38px !important;\n border: 1px solid #8cc8d3;\n background: #e7f6f8;\n }\n\n .opportunities-extension-account-plans-skeleton-navigation {\n display: grid;\n grid-template-columns: 32px 124px 32px;\n align-items: center;\n justify-content: center;\n gap: 8px;\n min-height: 54px;\n }\n\n .opportunities-extension-account-plans-skeleton-navigation .opportunities-extension-account-plans-skeleton-bar {\n justify-self: center;\n }\n\n .opportunities-extension-account-plans-skeleton-chart {\n display: grid;\n min-height: 0;\n grid-template-columns: minmax(0, 1fr) 150px;\n align-items: center;\n gap: 28px;\n padding: 12px 14px 0 62px;\n }\n\n .opportunities-extension-account-plans-skeleton-plot {\n display: flex;\n position: relative;\n height: 300px;\n min-height: 0;\n box-sizing: border-box;\n align-items: flex-end;\n justify-content: space-around;\n gap: 12px;\n padding: 0 14px 64px;\n background: repeating-linear-gradient(to bottom, transparent 0, transparent 58px, #ebe8e5 58px, #ebe8e5 59px) left top / 100% 236px no-repeat;\n }\n\n .opportunities-extension-account-plans-skeleton-plot::before,\n .opportunities-extension-account-plans-skeleton-plot::after {\n position: absolute;\n display: block;\n content: \"\";\n background: #dedbd7;\n }\n\n .opportunities-extension-account-plans-skeleton-plot::before {\n top: 0;\n bottom: 64px;\n left: 0;\n width: 1px;\n }\n\n .opportunities-extension-account-plans-skeleton-plot::after {\n right: 0;\n bottom: 64px;\n left: 0;\n height: 1px;\n }\n\n .opportunities-extension-account-plans-skeleton-plot .opportunities-extension-account-plans-skeleton-bar {\n width: min(38px, 7%);\n flex: 0 1 38px;\n border-radius: 2px 2px 0 0;\n }\n\n .opportunities-extension-account-plans-skeleton-legend {\n display: grid;\n align-content: center;\n gap: 18px;\n min-height: 0;\n }\n\n .opportunities-extension-account-plans-skeleton-legend .opportunities-extension-account-plans-skeleton-bar {\n height: 11px;\n }\n\n @keyframes opportunities-extension-skeleton-pulse {\n from { opacity: .48; }\n to { opacity: 1; }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .opportunities-extension-account-plans-skeleton-bar,\n .opportunities-extension-consumption-chart-skeleton-plot span,\n .opportunities-extension-consumption-svg .consumption-animated-label,\n .opportunities-extension-consumption-svg .consumption-animated-average {\n animation: none;\n opacity: 1;\n }\n }\n\n .opportunities-extension-account-plans-empty,\n .opportunities-extension-account-plans-error {\n margin: 0;\n padding: 28px 20px;\n color: #5f5a55;\n font-size: 14px;\n }\n\n .opportunities-extension-account-plans-error {\n color: #9f2d20;\n }\n\n .opportunities-extension-preferences-dialog {\n display: grid;\n width: min(680px, 100%);\n max-height: min(720px, calc(100vh - 48px));\n grid-template-rows: auto minmax(0, 1fr) auto;\n overflow: hidden;\n border: 1px solid #8f8a85;\n border-radius: 4px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(0, 0, 0, .24);\n color: #312d2a;\n }\n\n .opportunities-extension-preferences-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n padding: 18px 20px;\n border-bottom: 1px solid #dedbd7;\n }\n\n .opportunities-extension-preferences-header h2 {\n margin: 0;\n font-size: 20px;\n font-weight: 700;\n line-height: 1.2;\n }\n\n .opportunities-extension-preferences-content {\n min-height: 0;\n overflow: auto;\n padding: 20px;\n }\n\n .opportunities-extension-preferences-fieldset {\n min-width: 0;\n margin: 0;\n padding: 0;\n border: 0;\n }\n\n .opportunities-extension-preferences-fieldset legend {\n margin-bottom: 12px;\n padding: 0;\n color: #312d2a;\n font-size: 15px;\n font-weight: 700;\n line-height: 1.3;\n }\n\n .opportunities-extension-preferences-options {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 4px 20px;\n }\n\n .opportunities-extension-preferences-option {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n min-width: 0;\n padding: 9px 8px;\n border-radius: 4px;\n color: #312d2a;\n font-size: 14px;\n line-height: 1.35;\n cursor: pointer;\n }\n\n .opportunities-extension-preferences-option:hover {\n background: #f0eeeb;\n }\n\n .opportunities-extension-preferences-option input {\n width: 18px;\n height: 18px;\n flex: 0 0 18px;\n margin: 0;\n accent-color: #00758f;\n cursor: pointer;\n }\n\n .opportunities-extension-preferences-cache-field {\n display: grid;\n width: min(280px, 100%);\n gap: 6px;\n margin-top: 24px;\n color: #312d2a;\n font-size: 14px;\n font-weight: 700;\n }\n\n .opportunities-extension-preferences-cache-field select {\n width: 100%;\n height: 40px;\n padding: 0 36px 0 12px;\n border: 1px solid #8f8a85;\n border-radius: 4px;\n background: #ffffff;\n color: #312d2a;\n font-size: 14px;\n font-weight: 400;\n }\n\n .opportunities-extension-preferences-cache-field select:focus {\n border-color: #00758f;\n outline: 2px solid #bde7ee;\n outline-offset: 0;\n }\n\n .opportunities-extension-preferences-footer {\n display: flex;\n justify-content: flex-end;\n padding: 14px 20px;\n border-top: 1px solid #dedbd7;\n }\n\n .opportunities-extension-button {\n min-width: 88px;\n min-height: 36px;\n padding: 7px 16px;\n border: 1px solid transparent;\n border-radius: 4px;\n font-size: 14px;\n font-weight: 600;\n line-height: 1.2;\n cursor: pointer;\n }\n\n .opportunities-extension-button-primary {\n border-color: #00758f;\n background: #00758f;\n color: #ffffff;\n }\n\n .opportunities-extension-button-primary:hover,\n .opportunities-extension-button-primary:focus-visible {\n border-color: #005e73;\n background: #005e73;\n outline: none;\n }\n\n .opportunities-extension-debug-panel {\n position: fixed;\n right: 24px;\n bottom: 24px;\n z-index: 1;\n width: min(640px, calc(100vw - 48px));\n max-height: min(560px, calc(100vh - 120px));\n overflow: auto;\n border: 1px solid #8f8a85;\n border-radius: 4px;\n background: #ffffff;\n box-shadow: 0 2px 8px rgba(0, 0, 0, .16);\n padding: 16px;\n color: #000000;\n }\n\n .opportunities-extension-debug-panel[hidden] {\n display: none !important;\n }\n\n .opportunities-extension-debug-panel h2 {\n margin: 0;\n font-size: 18px;\n font-weight: 700;\n line-height: 1.25;\n }\n\n .opportunities-extension-debug-panel p {\n margin: 4px 0 0;\n color: #5f5a55;\n font-size: 13px;\n }\n\n .opportunities-extension-debug-panel-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 12px;\n }\n\n .opportunities-extension-debug-visibility-button {\n width: 34px;\n height: 34px;\n flex: 0 0 34px;\n color: #312d2a;\n }\n\n .opportunities-extension-debug-visibility-button svg {\n width: 20px;\n height: 20px;\n }\n\n .opportunities-extension-debug-list {\n display: grid;\n gap: 10px;\n }\n\n .opportunities-extension-debug-item {\n border: 1px solid #dedbd7;\n border-left-width: 4px;\n border-radius: 4px;\n background: #faf9f8;\n }\n\n .opportunities-extension-debug-item[open] {\n background: #ffffff;\n }\n\n .opportunities-extension-debug-item-success {\n border-left-color: #3f6f17;\n }\n\n .opportunities-extension-debug-item-success .opportunities-extension-debug-status {\n background: #e2f2e5;\n color: #356d19;\n }\n\n .opportunities-extension-debug-item-failed {\n border-left-color: #c5331f;\n }\n\n .opportunities-extension-debug-item-failed .opportunities-extension-debug-status {\n background: #f9e3e1;\n color: #a52b1c;\n }\n\n .opportunities-extension-debug-item-pending {\n border-left-color: #6f5a7f;\n }\n\n .opportunities-extension-debug-item-pending .opportunities-extension-debug-status {\n background: #eee8f2;\n color: #6f5a7f;\n }\n\n .opportunities-extension-debug-heading {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n min-height: 42px;\n padding: 9px 12px 9px 10px;\n cursor: pointer;\n list-style: none;\n }\n\n .opportunities-extension-debug-heading::-webkit-details-marker {\n display: none;\n }\n\n .opportunities-extension-debug-heading::before {\n content: \"⌄\";\n width: 14px;\n color: #5f5a55;\n font-size: 16px;\n line-height: 1;\n transform: rotate(-90deg);\n transition: transform .15s ease;\n }\n\n .opportunities-extension-debug-item[open] .opportunities-extension-debug-heading::before {\n transform: rotate(0deg);\n }\n\n .opportunities-extension-debug-summary-meta {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n }\n\n .opportunities-extension-debug-http-status,\n .opportunities-extension-debug-status {\n display: inline-flex;\n align-items: center;\n min-height: 22px;\n padding: 0 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 700;\n line-height: 1;\n text-transform: uppercase;\n }\n\n .opportunities-extension-debug-http-status {\n background: #e8f4f6;\n color: #006d7a;\n }\n\n .opportunities-extension-debug-status {\n background: #ebe8e5;\n color: #5f5a55;\n }\n\n .opportunities-extension-debug-heading strong {\n font-size: 13px;\n }\n\n .opportunities-extension-debug-item pre {\n margin: 0;\n padding: 8px 12px 12px;\n color: #312d2a;\n font-family: Consolas, \"Courier New\", monospace;\n font-size: 12px;\n line-height: 1.45;\n white-space: pre-wrap;\n word-break: break-word;\n }\n\n .opportunities-extension-debug-empty {\n padding: 14px;\n border: 1px dashed #b8b2ad;\n border-radius: 4px;\n color: #5f5a55;\n font-size: 13px;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] {\n background: #1f1f1f;\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-shell,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-body {\n background: #1f1f1f;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-header {\n border-bottom-color: #4e4a46;\n background:\n linear-gradient(90deg, #a8795d 0 11%, #dd5a47 11% 23%, #876e9b 23% 36%, #00a6be 36% 51%, #dfbd76 51% 63%, transparent 63% 100%) top left / 100% 6px no-repeat,\n #292827;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-header h1,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-dashboard-value,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-field,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-label,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-status-filter-label,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-label,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table-search-field {\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-header p,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-header p,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-dashboard-card span,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-empty,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-detail-heading span,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-empty,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-empty,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plan-details dt,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-panel p,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-empty {\n color: #c9c5c1;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-error {\n color: #ffb4a8;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-chart-error {\n color: #ffb4a8;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-skeleton-bar {\n background: #5b5753;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-skeleton-status {\n background: #5b5753;\n box-shadow: 0 0 0 3px #3b3937;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-skeleton-period-active {\n border-color: #3f7580;\n background: #23474e;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-chart-skeleton-plot span {\n background: #5b5753;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plan-status-indicator {\n background: #8f8a85;\n box-shadow: 0 0 0 3px #3b3937;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plan-status-indicator[data-active=\"true\"] {\n background: #7fb05b;\n box-shadow: 0 0 0 3px #314527;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-form,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-dashboard-card,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table-surface,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-panel,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-preferences-dialog,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-dialog,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-table,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-panel,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-item[open] {\n border-color: #4e4a46;\n background: #292827;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-trigger,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-search,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table-search-field input,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-field select,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-preferences-cache-field select,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-status-filter-button {\n border-color: #6a6560;\n background: #333130;\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-trigger::after {\n border-top-color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-select-wrap::after {\n border-top-color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-trigger:disabled {\n background: #292827;\n color: #918b86;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-search::placeholder,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table-search-field input::placeholder {\n color: #b5b0ab;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-select-all,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-option,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-preferences-header h2,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-header h2,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-preferences-fieldset legend,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-preferences-option,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-preferences-cache-field,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-detail-heading,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plan-details dd,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-heading,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-item pre,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-panel h2 {\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-navigation strong {\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-chart-empty {\n color: #c9c5c1;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-select-all,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table th {\n border-bottom-color: #4e4a46;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-preferences-header,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-preferences-footer,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-header,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-tabs-navigation,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-tabs,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plan-details > div,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-skeleton-tabs,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-skeleton-details > div {\n border-color: #4e4a46;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-trend,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-chart-skeleton-plot,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-skeleton-trend,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-skeleton-plot {\n border-color: #4e4a46;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-skeleton-plot {\n background: repeating-linear-gradient(to bottom, transparent 0, transparent 58px, #4e4a46 58px, #4e4a46 59px) left top / 100% 236px no-repeat;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-skeleton-plot::before,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-skeleton-plot::after {\n background: #4e4a46;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plan-tab {\n color: #c9c5c1;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plan-tab:hover {\n background: #3b3937;\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plan-tab[aria-selected=\"true\"] {\n border-bottom-color: #43c4d5;\n color: #91e4ed;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-periods button {\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-periods button:hover {\n background: #3b3937;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-periods button[aria-pressed=\"true\"] {\n border-color: #43c4d5;\n background: #173e43;\n color: #91e4ed;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-navigation-button,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-tab-navigation-button {\n border-color: #6a6560;\n background: #333130;\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-navigation-button:hover:not(:disabled),\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-tab-navigation-button:hover:not(:disabled) {\n border-color: #43c4d5;\n background: #3b3937;\n color: #91e4ed;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-navigation-button:disabled,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-account-plans-tab-navigation-button:disabled {\n border-color: #4e4a46;\n color: #746f6a;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-svg .consumption-grid-line {\n stroke: #4e4a46;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-svg .consumption-axis-label,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-svg .consumption-x-label {\n fill: #c9c5c1;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-svg .consumption-value-label {\n fill: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-svg .consumption-bar-funded {\n fill: #ca8798;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-svg .consumption-bar-overage {\n fill: #f09a32;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-svg .consumption-average-line {\n stroke: #f09a32;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-svg .consumption-bar-group:hover rect,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-svg .consumption-bar-group:focus rect {\n stroke: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-legend button {\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-legend button:hover {\n background: #3b3937;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-legend button[aria-pressed=\"false\"] {\n color: #a8a39e;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-legend-funded {\n background: #ca8798;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-legend-overage {\n background: #f09a32;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-legend-average {\n border-top-color: #f09a32;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-consumption-tooltip {\n border-color: #6a6560;\n background: #333130;\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-preferences-option:hover {\n background: #3b3937;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-preferences-option input {\n accent-color: #43c4d5;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-button-primary {\n border-color: #00a6be;\n background: #008aa6;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-button-primary:hover,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-button-primary:focus-visible {\n border-color: #43c4d5;\n background: #006d7a;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-filter-option:hover,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table tbody tr:hover,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-sort-button:hover,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-icon-button:hover,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-icon-button:focus {\n background: #3b3937;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table thead,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table th,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-table th {\n background: #333130;\n color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table td {\n border-bottom-color: #3f3c39;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-detail {\n border-bottom-color: #4e4a46;\n background: #252423;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-table th,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-table td {\n border-bottom-color: #3f3c39;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-count {\n border-color: #2f9bae;\n background: #1d4e55;\n color: #91e4ed;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-count:hover,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-count:focus-visible,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-count[aria-expanded=\"true\"] {\n border-color: #43c4d5;\n background: #008aa6;\n color: #ffffff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-progress[data-state=\"running\"] {\n background: #1d4e55;\n color: #91e4ed;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-progress[data-state=\"complete\"] {\n background: #244f2e;\n color: #a8dfb7;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-progress[data-state=\"complete-with-errors\"],\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-progress[data-state=\"error\"],\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-count-error {\n border-color: #b13b34;\n background: #5f2926;\n color: #ffb4ad;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-cache-timestamp {\n background: #3b3937;\n color: #c9c5c1;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table a,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-copy-button,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-button {\n color: #43c4d5;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-table a:hover,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-copy-button:hover,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-copy-button:focus-visible,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-button:hover,\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-button:focus-visible {\n background: #1d4e55;\n color: #91e4ed;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button[data-stage=\"SQL\"] {\n color: #43c4d5;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button[data-stage=\"PIPELINE\"] {\n color: #d1cbc6;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button[data-stage=\"UPSIDE\"] {\n color: #ffbf62;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button[data-stage=\"FORECAST\"] {\n color: #d2b7e9;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button[data-stage=\"WON\"] {\n color: #9bd46a;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-dashboard-card[data-stage=\"SQL\"] {\n border-top-color: #43c4d5;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-total-dashboard-card {\n border-top-color: #f6f4f2;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-dashboard-card[data-stage=\"PIPELINE\"] {\n border-top-color: #c0b9b3;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-dashboard-card[data-stage=\"UPSIDE\"] {\n border-top-color: #ffb84d;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-dashboard-card[data-stage=\"FORECAST\"] {\n border-top-color: #cba7e7;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-dashboard-card[data-stage=\"WON\"] {\n border-top-color: #92cf5b;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button[aria-pressed=\"true\"][data-stage=\"SQL\"] {\n border-color: #008aa6;\n background: #008aa6;\n color: #ffffff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button[aria-pressed=\"true\"][data-stage=\"PIPELINE\"] {\n border-color: #6f6863;\n background: #6f6863;\n color: #ffffff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button[aria-pressed=\"true\"][data-stage=\"UPSIDE\"] {\n border-color: #b96e00;\n background: #b96e00;\n color: #ffffff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button[aria-pressed=\"true\"][data-stage=\"FORECAST\"] {\n border-color: #755b8b;\n background: #755b8b;\n color: #ffffff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-stage-filter-button[aria-pressed=\"true\"][data-stage=\"WON\"] {\n border-color: #4b7f22;\n background: #4b7f22;\n color: #ffffff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-status-filter-button[data-status=\"OPEN\"] {\n color: #69c985;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-status-filter-button[data-status=\"CLOSED\"] {\n color: #c3cad3;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-status-filter-button[data-status=\"LOST\"] {\n color: #ff9d94;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-status-filter-button[data-status=\"WON\"] {\n color: #83bfff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-status-filter-button[aria-pressed=\"true\"][data-status=\"OPEN\"] {\n border-color: #197a3d;\n background: #197a3d;\n color: #ffffff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-status-filter-button[aria-pressed=\"true\"][data-status=\"CLOSED\"] {\n border-color: #4b5563;\n background: #4b5563;\n color: #ffffff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-status-filter-button[aria-pressed=\"true\"][data-status=\"LOST\"] {\n border-color: #b13b34;\n background: #b13b34;\n color: #ffffff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-status-filter-button[aria-pressed=\"true\"][data-status=\"WON\"] {\n border-color: #1f5f99;\n background: #1f5f99;\n color: #ffffff;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-item {\n border-color: #4e4a46;\n background: #252423;\n }\n\n .opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-icon-button {\n color: #f6f4f2;\n }\n\n @media (min-width: 1201px) and (max-width: 1450px) {\n .opportunities-extension-table th:nth-child(1) {\n width: 16%;\n }\n\n .opportunities-extension-table th:nth-child(3) {\n width: 18%;\n }\n\n .opportunities-extension-table th:nth-child(4) {\n width: 12%;\n }\n\n .opportunities-extension-table th:nth-child(6) {\n width: 7%;\n }\n\n .opportunities-extension-table th:nth-child(7) {\n width: 8%;\n }\n\n .opportunities-extension-table th:nth-child(10) {\n width: 10%;\n }\n }\n\n @media (max-width: 1200px) {\n .opportunities-extension-table {\n min-width: 1180px;\n }\n\n .opportunities-extension-table th:nth-child(1) {\n width: 15%;\n }\n\n .opportunities-extension-table th:nth-child(3) {\n width: 17%;\n }\n\n .opportunities-extension-table th:nth-child(4) {\n width: 13%;\n }\n\n .opportunities-extension-table th:nth-child(6) {\n width: 6%;\n }\n\n .opportunities-extension-table th:nth-child(7) {\n width: 9%;\n }\n\n .opportunities-extension-table th:nth-child(10) {\n width: 11%;\n }\n }\n\n @media (max-width: 700px) {\n .opportunities-extension-header {\n padding: 24px 20px 18px;\n }\n\n .opportunities-extension-header::before {\n left: 20px;\n }\n\n .opportunities-extension-header h1 {\n font-size: 22px;\n }\n\n .opportunities-extension-header p {\n font-size: 14px;\n }\n\n .opportunities-extension-body {\n padding: 16px 20px 24px;\n }\n\n .opportunities-extension-form {\n align-items: stretch;\n flex-wrap: wrap;\n gap: 14px;\n min-height: auto;\n padding: 12px;\n }\n\n .opportunities-extension-field {\n max-width: none;\n width: 100%;\n }\n\n .opportunities-extension-customer-filter {\n width: 100%;\n }\n\n .opportunities-extension-customer-filter-trigger {\n width: 100%;\n }\n\n .opportunities-extension-customer-filter-panel {\n width: min(360px, calc(100vw - 64px));\n }\n\n .opportunities-extension-table-search-field {\n width: 100%;\n }\n\n .opportunities-extension-stage-dashboard {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n\n .opportunities-extension-stage-dashboard-value {\n font-size: 18px;\n }\n\n .opportunities-extension-preferences-modal {\n padding: 12px;\n }\n\n .opportunities-extension-account-plans-modal {\n padding: 12px;\n }\n\n .opportunities-extension-preferences-dialog {\n max-height: calc(100vh - 24px);\n }\n\n .opportunities-extension-account-plans-dialog {\n height: calc(100vh - 24px);\n }\n\n .opportunities-extension-account-plan-panel {\n padding: 12px 14px;\n }\n\n .opportunities-extension-account-plan-details {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 0 14px;\n }\n\n .opportunities-extension-account-plan-details > div {\n grid-template-columns: 1fr;\n align-content: center;\n gap: 3px;\n min-height: 54px;\n padding: 5px 0;\n }\n\n .opportunities-extension-account-plans-skeleton-details {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 0 14px;\n padding: 0;\n }\n\n .opportunities-extension-account-plans-skeleton-details > div {\n grid-template-columns: 1fr;\n align-content: center;\n gap: 5px;\n min-height: 54px;\n padding: 5px 0;\n }\n\n .opportunities-extension-account-plans-skeleton-trend {\n margin: 14px 0 0;\n }\n\n .opportunities-extension-account-plans-skeleton-periods {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n align-items: center;\n gap: 6px;\n }\n\n .opportunities-extension-account-plans-skeleton-periods .opportunities-extension-account-plans-skeleton-bar {\n width: 100% !important;\n }\n\n .opportunities-extension-account-plans-skeleton-chart {\n grid-template-columns: minmax(0, 1fr);\n gap: 12px;\n padding: 12px 8px 0 34px;\n }\n\n .opportunities-extension-account-plans-skeleton-legend {\n display: flex;\n min-height: 24px;\n align-items: center;\n justify-content: center;\n gap: 12px;\n }\n\n .opportunities-extension-consumption-trend-header {\n align-items: stretch;\n }\n\n .opportunities-extension-consumption-periods {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: 3px;\n }\n\n .opportunities-extension-consumption-periods button {\n min-width: 0;\n padding: 7px 4px;\n font-size: 12px;\n }\n\n .opportunities-extension-consumption-chart {\n min-height: 0;\n }\n\n .opportunities-extension-consumption-svg {\n width: 760px;\n height: 245px;\n }\n\n .opportunities-extension-consumption-visual {\n width: 920px;\n grid-template-columns: 760px 144px;\n gap: 12px;\n }\n\n .opportunities-extension-consumption-figure {\n overflow-x: auto;\n overscroll-behavior-inline: contain;\n }\n\n .opportunities-extension-consumption-chart-skeleton {\n min-height: 0;\n padding: 24px 18px 18px;\n }\n\n .opportunities-extension-consumption-chart-skeleton-plot {\n min-height: 180px;\n gap: 4px;\n padding: 0 6px;\n }\n\n .opportunities-extension-preferences-options {\n grid-template-columns: 1fr;\n }\n\n .opportunities-extension-stage-filter-controls {\n gap: 6px;\n }\n\n .opportunities-extension-status-filter-controls {\n gap: 6px;\n }\n\n .opportunities-extension-table-surface {\n min-width: 0;\n min-height: calc(100vh - 230px);\n }\n\n .opportunities-extension-debug-panel {\n right: 12px;\n bottom: 12px;\n width: calc(100vw - 24px);\n max-height: calc(100vh - 96px);\n }\n }\n \n/* Full-page diagnostics, scoped to the extension and its current theme. */\n.opportunities-extension-modal .opportunities-extension-debug-panel {\n --debug-surface: #fff;\n --debug-code: #f5f6f7;\n --debug-text: #312d2a;\n --debug-muted: #5f5a55;\n --debug-border: #dedbd7;\n --debug-success: #22613b;\n --debug-success-bg: #e1f1e5;\n --debug-failed: #a12721;\n --debug-failed-bg: #fce8e6;\n --debug-pending: #805000;\n --debug-pending-bg: #fff1ce;\n --debug-info: #075e87;\n --debug-info-bg: #e5f1fa;\n --debug-key: #075e87;\n --debug-string: #22613b;\n --debug-number: #865000;\n --debug-literal: #73429b;\n position: fixed;\n inset: 0;\n z-index: 50;\n display: grid;\n grid-template-rows: auto auto minmax(0, 1fr);\n width: auto;\n height: auto;\n max-height: none;\n min-width: 0;\n box-sizing: border-box;\n overflow: hidden;\n margin: 0;\n padding: 20px;\n border: 0;\n border-radius: 0;\n background: var(--debug-surface);\n color: var(--debug-text);\n color-scheme: light;\n}\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-panel {\n --debug-surface: #292827;\n --debug-code: #222120;\n --debug-text: #f6f4f2;\n --debug-muted: #c9c5c1;\n --debug-border: #514c47;\n --debug-success: #a9e3bd;\n --debug-success-bg: #214d34;\n --debug-failed: #ffb4ab;\n --debug-failed-bg: #582c2a;\n --debug-pending: #ffe099;\n --debug-pending-bg: #59441e;\n --debug-info: #a8d9ff;\n --debug-info-bg: #233f52;\n --debug-key: #8ed6f0;\n --debug-string: #a9e3bd;\n --debug-number: #ffe099;\n --debug-literal: #d6b7f1;\n color: #f6f4f2;\n color-scheme: dark;\n}\n.opportunities-extension-debug-actions { display: flex; gap: 8px; flex: 0 0 auto; }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-list {\n overflow: auto;\n min-height: 0;\n min-width: 0;\n align-content: start;\n scrollbar-gutter: stable;\n padding: 2px 8px 12px 2px;\n}\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item {\n --debug-state: var(--debug-info);\n --debug-state-bg: var(--debug-info-bg);\n border-color: var(--debug-border);\n border-left-color: var(--debug-state);\n background: var(--debug-surface);\n min-width: 0;\n}\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-success { --debug-state: var(--debug-success); --debug-state-bg: var(--debug-success-bg); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-failed { --debug-state: var(--debug-failed); --debug-state-bg: var(--debug-failed-bg); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-pending { --debug-state: var(--debug-pending); --debug-state-bg: var(--debug-pending-bg); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-status {\n color: var(--debug-state);\n background: var(--debug-state-bg);\n}\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status,\n.opportunities-extension-debug-method {\n color: var(--debug-info);\n background: var(--debug-info-bg);\n}\n.opportunities-extension-debug-method {\n display: inline-flex;\n align-items: center;\n min-height: 22px;\n padding: 0 8px;\n border-radius: 4px;\n font: 700 11px/1.2 Consolas, monospace;\n}\n.opportunities-extension-debug-method[data-method=\"POST\"] { color: var(--debug-literal); background: var(--debug-code); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class=\"2\"] { color: var(--debug-success); background: var(--debug-success-bg); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class=\"4\"],\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class=\"5\"] { color: var(--debug-failed); background: var(--debug-failed-bg); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class=\"pending\"] { color: var(--debug-pending); background: var(--debug-pending-bg); }\n.opportunities-extension-debug-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; padding: 0 14px 16px; }\n.opportunities-extension-debug-block { min-width: 0; border-left: 2px solid var(--debug-info); padding-left: 10px; }\n.opportunities-extension-debug-block[data-direction=\"response\"] { border-left-color: var(--debug-success); }\n.opportunities-extension-modal .opportunities-extension-debug-block h3 { margin: 0 0 8px; color: var(--debug-info); font-size: 12px; }\n.opportunities-extension-modal .opportunities-extension-debug-block[data-direction=\"response\"] h3 { color: var(--debug-success); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item pre {\n min-width: 0;\n margin: 0;\n padding: 12px;\n max-height: 380px;\n overflow: auto;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n color: var(--debug-text);\n background: var(--debug-code);\n line-height: 1.6;\n tab-size: 2;\n}\n.opportunities-extension-debug-json-key { color: var(--debug-key); }\n.opportunities-extension-debug-json-string { color: var(--debug-string); }\n.opportunities-extension-debug-json-number { color: var(--debug-number); }\n.opportunities-extension-debug-json-literal { color: var(--debug-literal); }\n@media (max-width: 760px) {\n .opportunities-extension-modal .opportunities-extension-debug-panel { padding: 12px; }\n .opportunities-extension-debug-details { grid-template-columns: minmax(0, 1fr); }\n}\n.opportunities-extension-debug-tabs {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n margin-bottom: 12px;\n padding-bottom: 8px;\n border-bottom: 1px solid #dedbd7;\n}\n\n.opportunities-extension-debug-tabs button {\n max-width: 100%;\n min-height: 32px;\n padding: 6px 8px;\n border: 0;\n border-bottom: 2px solid transparent;\n border-radius: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 12px;\n line-height: 1.35;\n overflow-wrap: anywhere;\n cursor: pointer;\n}\n\n.opportunities-extension-debug-tabs button:hover {\n background: #f5f4f2;\n}\n\n.opportunities-extension-debug-tabs button[aria-selected=\"true\"] {\n border-bottom-color: #00758f;\n background: #e8f4f6;\n color: #006b84;\n font-weight: 700;\n}\n\n.opportunities-extension-debug-tabs button:focus-visible,\n.opportunities-extension-debug-list[role=\"tabpanel\"]:focus-visible {\n outline: 2px solid #00758f;\n outline-offset: -2px;\n}\n\n.opportunities-extension-debug-time {\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.opportunities-extension-debug-heading {\n flex-wrap: wrap;\n}\n\n.opportunities-extension-debug-heading strong {\n flex: 1 1 180px;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.opportunities-extension-debug-summary-meta {\n flex-wrap: wrap;\n justify-content: flex-end;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-tabs button {\n color: #f6f4f2;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-heading::before {\n color: #c9c5c1;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-tabs {\n border-bottom-color: #514c47;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-tabs button:hover {\n background: #383633;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-tabs button[aria-selected=\"true\"] {\n background: #173f48;\n border-bottom-color: #8cc8d3;\n color: #bde7ee;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-tabs button:focus-visible,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-list[role=\"tabpanel\"]:focus-visible {\n outline-color: #8cc8d3;\n}\n.opportunities-extension-debug-duration {\n color: #55514d;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-duration { color: #d6d2ce; }\n\n/* Compact filters use available CSS width, including Windows display scaling. */\n.opportunities-extension-products-progress,\n.opportunities-extension-auth-badge { opacity: 1; transition: opacity 300ms ease; }\n.opportunities-extension-products-progress[data-fading=\"true\"],\n.opportunities-extension-auth-badge[data-fading=\"true\"] { opacity: 0; }\n.opportunities-extension-auth-badge[hidden] { display: none; }\n@media (prefers-reduced-motion: reduce) {\n .opportunities-extension-products-progress,\n .opportunities-extension-auth-badge { transition: none; }\n}\n.opportunities-extension-crm-sr-badge {\n border: 1px solid #408254;\n color: #185b30;\n background: #e1f1e5;\n}\n.opportunities-extension-crm-sr-badge[data-state=\"missing\"] { color: #9f201b; background: #fde8e7; border-color: #c74640; }\n.opportunities-extension-crm-sr-badge[data-state=\"error\"] { color: #785000; background: #fff0cd; border-color: #987021; }\n.opportunities-extension-wl-actions-badge[data-state=\"loading\"] { color: #5f5a55; background: #eceae8; border-color: #8f8a85; }\n.opportunities-extension-crm-sr-badge:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-crm-sr-badge { color: #b8e4c6; background: #214d34; border-color: #6eac82; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-crm-sr-badge[data-state=\"missing\"] { color: #ffd9d5; background: #542824; border-color: #ef938b; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-crm-sr-badge[data-state=\"error\"] { color: #ffe0a3; background: #584219; border-color: #c99b4b; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-wl-actions-badge[data-state=\"loading\"] { color: #dedbd7; background: #484541; border-color: #8f8a85; }\n\n.opportunities-extension-form {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr)) max-content max-content;\n align-items: end;\n gap: 12px;\n min-height: 0;\n padding: 12px;\n}\n\n.opportunities-extension-form > *,\n.opportunities-extension-badge-filters > section {\n min-width: 0;\n width: 100%;\n max-width: none;\n grid-template-rows: auto 32px;\n gap: 4px;\n font-size: 12px;\n}\n\n.opportunities-extension-form > * > span:first-child,\n.opportunities-extension-badge-filters > section > span:first-child {\n font-size: 12px;\n line-height: 16px;\n}\n\n.opportunities-extension-form .opportunities-extension-select-wrap {\n width: 100%;\n}\n\n.opportunities-extension-form .opportunities-extension-field select,\n.opportunities-extension-form .opportunities-extension-customer-filter-trigger,\n.opportunities-extension-form .opportunities-extension-table-search-field input {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n height: 32px;\n min-height: 32px;\n padding: 4px 8px;\n font-size: 12px;\n line-height: 20px;\n}\n\n.opportunities-extension-form .opportunities-extension-field select,\n.opportunities-extension-form .opportunities-extension-customer-filter-trigger {\n padding-right: 26px;\n}\n\n.opportunities-extension-form .opportunities-extension-select-wrap::after,\n.opportunities-extension-form .opportunities-extension-customer-filter-trigger::after {\n right: 10px;\n border-left-width: 4px;\n border-right-width: 4px;\n border-top-width: 5px;\n}\n\n.opportunities-extension-form .opportunities-extension-stage-filter-controls,\n.opportunities-extension-form .opportunities-extension-status-filter-controls {\n flex-wrap: nowrap;\n gap: 4px;\n}\n\n.opportunities-extension-form .opportunities-extension-stage-filter-button,\n.opportunities-extension-form .opportunities-extension-status-filter-button,\n.opportunities-extension-form .opportunities-extension-activity-filter-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n height: 28px;\n min-height: 28px;\n padding: 4px 7px;\n border: 1px solid currentColor;\n border-radius: 18px;\n font-family: inherit;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0;\n line-height: 18px;\n white-space: nowrap;\n}\n\n.opportunities-extension-cell-actions > .opportunities-extension-products-count,\n.opportunities-extension-cell-actions > .opportunities-extension-products-ramp,\n.opportunities-extension-cell-actions > .opportunities-extension-crm-sr-badge {\n display: inline-flex;\n align-items: center;\n justify-content: flex-start;\n box-sizing: border-box;\n flex: 0 0 auto;\n gap: 4px;\n height: 20px;\n min-height: 20px;\n margin: 0;\n padding: 0 6px;\n border-width: 1px;\n border-style: solid;\n border-radius: 8px;\n font-family: inherit;\n font-size: 10px;\n font-style: normal;\n font-weight: 600;\n line-height: 16px;\n letter-spacing: 0;\n text-align: center;\n vertical-align: middle;\n white-space: nowrap;\n}\n\n.opportunities-extension-cell-actions .opportunities-extension-badge-icon,\n.opportunities-extension-activity-filter-button .opportunities-extension-badge-icon {\n display: block;\n order: -1;\n flex: 0 0 16px;\n width: 16px;\n height: 16px;\n margin: 0;\n padding: 0;\n border: 0;\n color: inherit;\n fill: none;\n stroke: currentColor;\n stroke-width: 2.25;\n stroke-linecap: round;\n stroke-linejoin: round;\n pointer-events: none;\n}\n\n@media (max-width: 1150px) {\n .opportunities-extension-form {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n }\n .opportunities-extension-badge-filters > .opportunities-extension-status-filter,\n .opportunities-extension-badge-filters > .opportunities-extension-stage-filter {\n grid-column: span 2;\n }\n}\n\n@media (max-width: 700px) {\n .opportunities-extension-form {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n .opportunities-extension-form .opportunities-extension-stage-filter-controls,\n .opportunities-extension-form .opportunities-extension-status-filter-controls {\n flex-wrap: wrap;\n }\n .opportunities-extension-badge-filters > .opportunities-extension-status-filter,\n .opportunities-extension-badge-filters > .opportunities-extension-stage-filter {\n grid-template-rows: auto auto;\n }\n}\n\n@media (max-width: 420px) {\n .opportunities-extension-form { grid-template-columns: minmax(0, 1fr); }\n .opportunities-extension-badge-filters > .opportunities-extension-status-filter,\n .opportunities-extension-badge-filters > .opportunities-extension-stage-filter { grid-column: auto; }\n}\n\n.opportunities-extension-badge-filters { display: contents; }\n.opportunities-extension-activity-filter { display: grid; }\n.opportunities-extension-activity-filter[hidden] { display: none; }\n.opportunities-extension-activity-filter-controls { display: flex; align-items: center; flex-wrap: nowrap; gap: 4px; }\n/* Flexible fields share the space left by the four compact badge groups. */\n.opportunities-extension-form[data-activity-filters=\"true\"] {\n grid-template-columns: repeat(4, minmax(0, 1fr)) repeat(4, max-content);\n column-gap: 8px;\n}\n.opportunities-extension-activity-filter-button {\n min-width: 32px;\n gap: 4px;\n color: #185b30;\n background: #fff;\n cursor: pointer;\n}\n.opportunities-extension-activity-filter-button[data-value=\"missing\"] { color: #9f201b; }\n.opportunities-extension-activity-filter-button[data-value=\"under4\"] { color: #005f7d; }\n.opportunities-extension-activity-filter-button[aria-pressed=\"true\"] { color: #fff; background: #25653a; border-color: #25653a; }\n.opportunities-extension-activity-filter-button[aria-pressed=\"true\"][data-value=\"missing\"] { background: #a12d27; border-color: #a12d27; }\n.opportunities-extension-activity-filter-button[aria-pressed=\"true\"][data-value=\"under4\"] { background: #006b86; border-color: #006b86; }\n.opportunities-extension-activity-filter-button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter { color: #eeeae6; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter-button { color: #b8e4c6; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter-button:not([aria-pressed=\"true\"]) { background: #333130; border-color: #6a6560; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter-button[data-value=\"missing\"] { color: #ffd9d5; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter-button[data-value=\"under4\"] { color: #b5e9fc; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter-button[aria-pressed=\"true\"] { border-color: currentColor; }\n@media (max-width: 1350px) {\n .opportunities-extension-form[data-activity-filters=\"true\"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }\n .opportunities-extension-form[data-activity-filters=\"true\"] .opportunities-extension-badge-filters {\n display: flex;\n grid-column: 1 / -1;\n flex-wrap: wrap;\n align-items: start;\n gap: 12px 20px;\n }\n .opportunities-extension-form[data-activity-filters=\"true\"] .opportunities-extension-badge-filters > section {\n width: auto;\n max-width: 100%;\n grid-template-rows: auto auto;\n }\n}\n@media (max-width: 700px) {\n .opportunities-extension-form[data-activity-filters=\"true\"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .opportunities-extension-activity-filter-controls { flex-wrap: wrap; }\n}\n@media (max-width: 420px) {\n .opportunities-extension-form[data-activity-filters=\"true\"] { grid-template-columns: minmax(0, 1fr); }\n}\n\n.opportunities-extension-add-action {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n flex: 0 0 16px;\n width: 16px;\n height: 16px;\n padding: 0;\n margin: 0 0 0 2px;\n border: 0;\n border-radius: 3px;\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n}\n.opportunities-extension-add-action:hover { background: rgba(127, 127, 127, .2); }\n.opportunities-extension-add-action:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-action-form-modal {\n position: absolute;\n inset: 0;\n z-index: 5;\n display: grid;\n place-items: center;\n padding: 12px;\n background: rgba(0, 0, 0, .42);\n}\n.opportunities-extension-action-form-dialog {\n box-sizing: border-box;\n width: min(560px, 100%);\n max-height: calc(100vh - 24px);\n overflow-y: auto;\n background: #fff;\n color: #312d2a;\n border: 1px solid #b9b4ae;\n border-radius: 6px;\n box-shadow: 0 12px 36px rgba(0, 0, 0, .25);\n}\n.opportunities-extension-action-form {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 14px;\n padding: 18px;\n}\n.opportunities-extension-action-form-heading { min-width: 0; overflow-wrap: anywhere; }\n.opportunities-extension-action-form-context {\n display: grid;\n grid-template-columns: max-content minmax(0, 1fr);\n gap: 6px 12px;\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 18px;\n}\n.opportunities-extension-action-form-context dt { font-weight: 600; }\n.opportunities-extension-action-form-context dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }\n.opportunities-extension-action-form-field { display: grid; gap: 6px; min-width: 0; font-size: 13px; }\n.opportunities-extension-action-form-field[hidden] { display: none; }\n.opportunities-extension-action-form-status { margin: 0; font-size: 13px; overflow-wrap: anywhere; }\n.opportunities-extension-action-form-status[role=\"alert\"] { color: #a12d27; }\n.opportunities-extension-action-form-wide,\n.opportunities-extension-action-form-footer { grid-column: 1 / -1; }\n.opportunities-extension-action-form-field input,\n.opportunities-extension-action-form-field select,\n.opportunities-extension-action-form-field textarea {\n box-sizing: border-box;\n min-width: 0;\n width: 100%;\n border: 1px solid #8f8983;\n border-radius: 4px;\n padding: 8px;\n font: inherit;\n line-height: 20px;\n color: inherit;\n background: #fff;\n}\n.opportunities-extension-action-form-field textarea { resize: vertical; min-height: 96px; }\n.opportunities-extension-action-form-field select { height: 38px; }\n.opportunities-extension-action-form-field select[hidden] { display: none; }\n.opportunities-extension-sr-form-dialog { width: min(900px, 100%); }\n.opportunities-extension-sr-confirmation { z-index: 6; }\n.opportunities-extension-sr-confirm-dialog { width: min(800px, 100%); display: flex; flex-direction: column; overflow: hidden; }\n.opportunities-extension-sr-confirm-dialog > header,\n.opportunities-extension-sr-confirm-dialog > footer { flex-shrink: 0; }\n.opportunities-extension-sr-confirm-dialog > footer { padding: 16px 20px; }\n.opportunities-extension-sr-confirm-body { min-height: 0; overflow: auto; padding: 0 20px; }\n.opportunities-extension-sr-confirm-body > p { font-size: 13px; font-weight: 600; }\n.opportunities-extension-sr-confirm-payload { margin: 0; padding: 16px; border: 1px solid #b9b4ae; border-radius: 4px; background: #f6f5f4; color: #312d2a; font: 13px/1.6 Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }\n.opportunities-extension-sr-confirmation :focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-sr-confirm-payload { background: #211f1d; color: #eeeae6; border-color: #756f68; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-sr-confirmation :focus-visible { outline-color: #80d6e8; }\n.opportunities-extension-sr-form-dialog > .opportunities-extension-account-plans-header { padding: 20px 24px; }\n.opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { gap: 20px 24px; padding: 24px; align-items: start; }\n.opportunities-extension-sr-form-dialog .opportunities-extension-action-form-field { gap: 8px; }\n.opportunities-extension-sr-form-dialog textarea { min-height: 112px; }\n.opportunities-extension-sr-form-dialog .opportunities-extension-sr-service-option { padding: 12px 0; gap: 10px; }\n.opportunities-extension-sr-form-dialog .opportunities-extension-sr-service-abstract { font-size: 13px; line-height: 20px; }\n@media (min-width: 1000px) {\n .opportunities-extension-sr-form-dialog { width: min(1120px, 100%); }\n .opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 24px; }\n .opportunities-extension-sr-services { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 28px; align-items: start; }\n .opportunities-extension-sr-services .opportunities-extension-sr-service-option { margin: 0; border-top: 1px solid #aaa6a040; }\n .opportunities-extension-sr-services .opportunities-extension-sr-service-option:nth-child(-n+2) { border-top: 0; }\n}\n@media (max-width: 600px) {\n .opportunities-extension-sr-form-dialog > .opportunities-extension-account-plans-header { padding: 16px; }\n .opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { grid-template-columns: minmax(0, 1fr); gap: 18px; padding: 16px; }\n}\n.opportunities-extension-sr-lookup { min-width: 0; padding: 0; margin: 0; border: 0; font-size: 13px; }\n.opportunities-extension-sr-lookup legend { padding: 0 0 8px; font-weight: 600; }\n.opportunities-extension-sr-lookup[aria-busy=\"true\"] > p { opacity: .65; }\n.opportunities-extension-sr-service-option { display: flex; align-items: flex-start; gap: 8px; padding: 9px 0; cursor: pointer; }\n.opportunities-extension-sr-service-option + .opportunities-extension-sr-service-option { border-top: 1px solid #aaa6a040; }\n.opportunities-extension-sr-service-option input { flex: 0 0 16px; width: 16px; height: 16px; margin: 2px 0 0; accent-color: #00758f; }\n.opportunities-extension-sr-service-option > span { min-width: 0; overflow-wrap: anywhere; }\n.opportunities-extension-sr-service-name { display: block; font-weight: 600; line-height: 20px; }\n.opportunities-extension-sr-service-abstract { display: block; margin-top: 3px; color: #625d58; font-size: 12px; line-height: 18px; white-space: pre-wrap; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-sr-service-abstract { color: #c7c2bc; }\n.opportunities-extension-sr-lookup-retry { color: inherit; background: transparent; border: 1px solid #8f8983; border-radius: 4px; padding: 5px 10px; cursor: pointer; }\n.opportunities-extension-sr-lookup-retry[hidden] { display: none; }\n.opportunities-extension-action-form [aria-invalid=\"true\"] { border-color: #b3261e; outline: 1px solid #b3261e; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form [aria-invalid=\"true\"] { border-color: #ffaaa4; outline-color: #ffaaa4; }\n.opportunities-extension-modal .opportunities-extension-copy-toast { z-index: 20; }\n.opportunities-extension-action-combobox { position: relative; min-width: 0; }\n.opportunities-extension-action-combobox input { height: 38px; }\n.opportunities-extension-action-options {\n position: absolute;\n z-index: 10;\n top: calc(100% + 4px);\n left: 0;\n right: 0;\n box-sizing: border-box;\n max-height: 180px;\n overflow-y: auto;\n overscroll-behavior: contain;\n border: 1px solid #8f8983;\n border-radius: 4px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, .2);\n background: #fff;\n color: #292725;\n}\n.opportunities-extension-action-options[data-placement=\"above\"] { top: auto; bottom: calc(100% + 4px); }\n.opportunities-extension-action-options[hidden] { display: none; }\n.opportunities-extension-action-option { padding: 8px; line-height: 20px; overflow-wrap: anywhere; cursor: pointer; }\n.opportunities-extension-action-option:hover,\n.opportunities-extension-action-option[aria-selected=\"true\"] { background: #e3f2f5; color: #005b70; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-options { background: #211f1d; color: #eeeae6; border-color: #918a82; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-option:hover,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-option[aria-selected=\"true\"] { background: #174955; color: #c9f4ff; }\n.opportunities-extension-action-form[data-state=\"loading\"] .opportunities-extension-action-combobox input:disabled { animation: opportunities-action-options-loading 1s ease-in-out infinite alternate; }\n.opportunities-extension-action-form[data-state=\"loading\"] .opportunities-extension-action-form-field > :disabled {\n animation: opportunities-action-options-loading 1s ease-in-out infinite alternate;\n}\n@keyframes opportunities-action-options-loading { from { opacity: .45; } to { opacity: .85; } }\n@media (prefers-reduced-motion: reduce) {\n .opportunities-extension-action-form[data-state=\"loading\"] .opportunities-extension-action-form-field > :disabled,\n .opportunities-extension-action-form[data-state=\"loading\"] .opportunities-extension-action-combobox input:disabled { animation: none; }\n}\n.opportunities-extension-action-form-footer { display: flex; justify-content: flex-end; gap: 8px; }\n.opportunities-extension-action-form-footer button {\n border: 1px solid #8f8983;\n border-radius: 4px;\n padding: 7px 14px;\n font: inherit;\n font-size: 13px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n.opportunities-extension-action-form-footer button:disabled { opacity: .55; cursor: not-allowed; }\n.opportunities-extension-action-form :focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form-dialog {\n color-scheme: dark;\n background: #292725;\n color: #eeeae6;\n border-color: #756f68;\n}\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form-field input,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form-field select,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form-field textarea { background: #211f1d; border-color: #918a82; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form-status[role=\"alert\"] { color: #ffd9d5; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form :focus-visible { outline-color: #80d6e8; }\n@media (max-width: 420px) {\n .opportunities-extension-action-form { grid-template-columns: minmax(0, 1fr); }\n}\n\n/* Keep layout rules on the outer table, never on expanded product tables. */\n.opportunities-extension-table[data-opportunities-layout=\"inline-actions\"] {\n width: 100%;\n min-width: 1120px;\n table-layout: fixed;\n}\n\n/* Fixed table layout shares leftover width only between the two auto columns. */\n.opportunities-extension-table > thead > tr > th[data-column=\"name\"] { width: auto; }\n.opportunities-extension-table > thead > tr > th[data-column=\"customer\"] { width: auto; }\n.opportunities-extension-table > thead > tr > th[data-column=\"owner\"] { width: 132px; }\n.opportunities-extension-table > thead > tr > th[data-column=\"optyNumber\"] { width: 106px; }\n.opportunities-extension-table > thead > tr > th[data-column=\"revenue\"] { width: 124px; }\n.opportunities-extension-table > thead > tr > th[data-column=\"closeDate\"] { width: 100px; }\n.opportunities-extension-table > thead > tr > th[data-column=\"status\"] { width: 114px; }\n.opportunities-extension-table > thead > tr > th[data-column=\"winProbability\"] { width: 110px; }\n.opportunities-extension-table > thead > tr > th[data-column=\"stage\"] { width: 94px; }\n.opportunities-extension-table > thead > tr > th[data-column=\"lastUpdateDate\"] { width: 136px; }\n\n.opportunities-extension-table > tbody > tr > td[data-column] {\n padding: 10px 8px;\n overflow: visible;\n text-overflow: clip;\n white-space: normal;\n font-variant-numeric: tabular-nums;\n}\n\n.opportunities-extension-table > tbody > tr > td[data-column=\"name\"],\n.opportunities-extension-table > tbody > tr > td[data-column=\"customer\"] {\n position: relative;\n padding-right: 16px;\n padding-left: 12px;\n padding-bottom: 38px;\n vertical-align: top;\n}\n\n.opportunities-extension-table > tbody > tr > td[data-column=\"owner\"] {\n padding-right: 16px;\n}\n\n.opportunities-extension-table > tbody > tr > td[data-column=\"closeDate\"],\n.opportunities-extension-table > tbody > tr > td[data-column=\"lastUpdateDate\"],\n.opportunities-extension-table > tbody > tr > td[data-column=\"status\"],\n.opportunities-extension-table > tbody > tr > td[data-column=\"stage\"],\n.opportunities-extension-table > tbody > tr > td[data-column=\"winProbability\"] {\n white-space: nowrap;\n}\n\n.opportunities-extension-cell-content {\n display: block;\n min-width: 0;\n min-height: 2.7em;\n line-height: 1.35;\n}\n\n.opportunities-extension-cell-content > :first-child {\n display: block;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.opportunities-extension-cell-actions {\n position: absolute;\n left: 12px;\n right: 16px;\n bottom: 10px;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: 6px;\n height: 24px;\n}\n\n.opportunities-extension-cell-actions .opportunities-extension-action-button {\n flex: 0 0 24px;\n width: 24px;\n height: 24px;\n padding: 2px;\n}\n\n.opportunities-extension-table > tbody > tr > td[data-column=\"customer\"] .opportunities-extension-cell-actions {\n min-width: 84px;\n}\n\n.opportunities-extension-cell-actions .opportunities-extension-action-button svg {\n width: 18px;\n height: 18px;\n}\n\n.opportunities-extension-cell-actions button:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n}\n\n.opportunities-extension-products-ramp {\n min-width: 0;\n max-width: 100%;\n border: 1px solid #bba5cf;\n background: #f0e8f7;\n color: #63358a;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-ramp {\n background: #43334f;\n color: #ead5fc;\n border-color: #9372ac;\n}\n\n.opportunities-extension-products-ramp[data-missing=\"true\"] {\n background: #fde8e7;\n color: #9f201b;\n border-color: #c74640;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-ramp[data-missing=\"true\"] {\n background: #542824;\n color: #ffd9d5;\n border-color: #ef938b;\n}\n\n.opportunities-extension-table > thead > tr > th[data-column] .opportunities-extension-sort-button {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 12px;\n gap: 6px;\n padding: 10px 8px;\n align-items: center;\n}\n\n.opportunities-extension-table > thead > tr > th[data-column=\"name\"] .opportunities-extension-sort-button,\n.opportunities-extension-table > thead > tr > th[data-column=\"customer\"] .opportunities-extension-sort-button {\n padding-left: 12px;\n padding-right: 16px;\n}\n\n.opportunities-extension-sort-label {\n min-width: 0;\n white-space: normal;\n overflow-wrap: anywhere;\n}\n\n.opportunities-extension-table > thead > tr > th[data-column] .opportunities-extension-sort-button::after {\n position: static;\n justify-self: end;\n}\n.opportunities-extension-crm-sr-badge[type=\"button\"] { cursor: pointer; font-family: inherit; }\n.opportunities-extension-sr-popover {\n position: fixed; z-index: 8; box-sizing: border-box; overflow: auto;\n padding: 12px; border: 1px solid #b9b5b0; border-radius: 6px;\n background: #fff; color: #302e2b; box-shadow: 0 6px 24px #0003;\n font: 12px/1.5 Arial, sans-serif; text-align: left; overflow-wrap: anywhere;\n}\n.opportunities-extension-sr-popover section { padding: 8px 0; }\n.opportunities-extension-sr-popover section + section { border-top: 1px solid #dedbd7; }\n.opportunities-extension-sr-number { display: flex; align-items: center; gap: 8px; padding-right: 30px; font-weight: 600; }\n.opportunities-extension-sr-popover a { color: #00677d; text-decoration: underline; }\n.opportunities-extension-sr-details { white-space: pre-wrap; margin-top: 6px; }\n.opportunities-extension-sr-children { margin: 10px 0 0 8px; padding-left: 10px; border-left: 2px solid #8c969a; overflow-wrap: anywhere; }\n.opportunities-extension-sr-children[aria-busy=\"true\"] { min-height: 28px; animation: opportunities-action-options-loading 1s ease-in-out infinite alternate; }\n.opportunities-extension-sr-children[role=\"alert\"] { color: #a12d27; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-sr-children[role=\"alert\"] { color: #ffd9d5; }\n@media (prefers-reduced-motion: reduce) { .opportunities-extension-sr-children[aria-busy=\"true\"] { animation: none; } }\n.opportunities-extension-sr-dismiss {\n position: absolute; top: 6px; right: 6px; width: 28px; height: 28px;\n display: grid; place-items: center; padding: 4px; border: 0;\n background: transparent; color: inherit; cursor: pointer;\n}\n.opportunities-extension-sr-dismiss svg { width: 16px; height: 16px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-sr-popover { background: #292827; color: #f1efec; border-color: #77716b; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-sr-popover section + section { border-color: #5f5953; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-sr-popover a { color: #7ddced; }\n.opportunities-extension-title-row {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px 16px;\n min-width: 0;\n flex: 1;\n}\n.opportunities-extension-title-row h1 {\n display: block;\n margin: 0;\n padding: 0;\n line-height: 1;\n}\n.opportunities-extension-title-row .opportunities-extension-subtitle-row {\n display: flex;\n align-items: center;\n align-self: center;\n flex-wrap: wrap;\n gap: 8px;\n margin: 0;\n padding: 0;\n min-width: 0;\n}\n.opportunities-extension-title-row .opportunities-extension-subtitle-row > span {\n box-sizing: border-box;\n margin: 0;\n min-height: 22px;\n line-height: 16px;\n}\n.opportunities-extension-title-row + .opportunities-extension-header-actions { flex-shrink: 0; }\n.opportunities-extension-refresh-all:disabled { opacity: .65; cursor: wait; }\n.opportunities-extension-refresh-all:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-refresh-all[aria-busy=\"true\"] svg { animation: opportunities-refresh-spin 1s linear infinite; }\n@keyframes opportunities-refresh-spin { to { transform: rotate(360deg); } }\n@media (prefers-reduced-motion: reduce) {\n .opportunities-extension-refresh-all[aria-busy=\"true\"] svg { animation: none; }\n}\n\n.opportunities-extension-account-plans-dialog.opportunities-extension-install-base-dialog {\n width: min(1700px, 100%);\n}\n.opportunities-extension-install-base-dialog .opportunities-extension-account-plans-header {\n padding: 10px 12px;\n gap: 12px;\n}\n.opportunities-extension-install-base-dialog .opportunities-extension-account-plans-header h2 {\n font-size: 16px;\n line-height: 22px;\n}\n.opportunities-extension-install-base-dialog .opportunities-extension-account-plans-header p {\n font-size: 12px;\n line-height: 16px;\n margin-top: 3px;\n overflow-wrap: anywhere;\n}\n.opportunities-extension-install-base-dialog .opportunities-extension-icon-button {\n width: 28px;\n height: 28px;\n min-width: 28px;\n min-height: 28px;\n padding: 4px;\n}\n.opportunities-extension-install-base-dialog .opportunities-extension-customer-table-toolbar {\n padding: 8px 12px;\n}\n.opportunities-extension-install-base-dialog .opportunities-extension-customer-table-search {\n width: min(320px, 100%);\n height: 32px;\n padding: 4px 8px;\n font-size: 12px;\n line-height: 20px;\n}\n.opportunities-extension-install-base-content {\n min-width: 0;\n min-height: 0;\n overflow: auto;\n scrollbar-gutter: stable;\n}\n.opportunities-extension-install-base-message { margin: 10px 12px; font-size: 12px; line-height: 16px; }\n.opportunities-extension-install-base-table {\n width: 100%; min-width: 1120px; table-layout: fixed; border-collapse: collapse; font-size: 11px;\n}\n.opportunities-extension-install-base-table col { width: 7%; }\n.opportunities-extension-install-base-table .install-base-col-partyName { width: 12%; }\n.opportunities-extension-install-base-table .install-base-col-productGroupName { width: 13%; }\n.opportunities-extension-install-base-table .install-base-col-productName { width: 13%; }\n.opportunities-extension-install-base-table .install-base-col-quantity { width: 5%; }\n.opportunities-extension-install-base-table .install-base-col-quantityType { width: 8%; }\n.opportunities-extension-install-base-table th,\n.opportunities-extension-install-base-table td {\n padding: 8px 6px; text-align: left; vertical-align: middle; overflow-wrap: anywhere;\n border-bottom: 1px solid #e8e5e1; line-height: 1.4;\n}\n.opportunities-extension-install-base-table th { position: sticky; top: 0; z-index: 1; background: #faf9f8; }\n.opportunities-extension-install-base-table td[data-format=\"amount\"],\n.opportunities-extension-install-base-table td[data-format=\"quantity\"] { text-align: right; font-variant-numeric: tabular-nums; }\n.opportunities-extension-install-base-table td[data-format=\"date\"] { white-space: nowrap; }\n.opportunities-extension-install-base-status {\n display: inline-block; padding: 2px 5px; border-radius: 12px; font-size: 10px; font-weight: 600;\n color: #5f5a55; background: #eceae8; white-space: nowrap;\n}\n.opportunities-extension-install-base-status[data-status=\"ACTIVE\"] { color: #17693a; background: #e1f1e5; }\n.opportunities-extension-install-base-status[data-status=\"UNSUPPORTED\"] { color: #855300; background: #fff0d6; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-content,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-table,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-table th,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-table td { color: #f6f4f2; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-message { color: #c9c5c1; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-message[role=\"alert\"] { color: #ffb4ab; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-table th { background: #353331; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-table th,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-table td { border-color: #484541; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-status { color: #dedbd7; background: #484541; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-status[data-status=\"ACTIVE\"] { color: #b8e4c6; background: #214d34; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-install-base-status[data-status=\"UNSUPPORTED\"] { color: #ffdfa2; background: #614418; }\n@media (prefers-reduced-motion: reduce) {\n .opportunities-extension-install-base-table .opportunities-extension-account-plans-skeleton-bar { animation: none; }\n}\n\n.opportunities-extension-account-plans-dialog.opportunities-extension-service-requests-dialog { width: min(1700px, 100%); }\n.opportunities-extension-service-requests-content { min-width: 0; min-height: 0; overflow: auto; scrollbar-gutter: stable; }\n.opportunities-extension-service-requests-message { margin: 16px; font-size: 13px; }\n.opportunities-extension-service-requests-table {\n width: 100%; min-width: 1200px; table-layout: fixed; border-collapse: collapse; font-size: 12px;\n}\n.opportunities-extension-service-requests-table col { width: 7%; }\n.opportunities-extension-service-requests-table .service-requests-col-name { width: 17%; }\n.opportunities-extension-service-requests-table .service-requests-col-id { width: 10%; }\n.opportunities-extension-service-requests-table .service-requests-col-severity { width: 5%; }\n.opportunities-extension-service-requests-table .service-requests-col-subStatus { width: 14%; }\n.opportunities-extension-service-requests-table .service-requests-col-productName { width: 12%; }\n.opportunities-extension-service-requests-table th,\n.opportunities-extension-service-requests-table td {\n padding: 12px 8px; text-align: left; vertical-align: middle; overflow-wrap: anywhere;\n border-bottom: 1px solid #e8e5e1; line-height: 1.4;\n}\n.opportunities-extension-service-requests-table th { position: sticky; top: 0; z-index: 1; background: #faf9f8; }\n.opportunities-extension-service-requests-table td[data-field=\"created\"],\n.opportunities-extension-service-requests-table td[data-field=\"lastUpdated\"],\n.opportunities-extension-service-requests-table td[data-field=\"id\"] { white-space: nowrap; font-variant-numeric: tabular-nums; }\n.opportunities-extension-service-request-status {\n display: inline-block; padding: 3px 6px; border-radius: 12px; font-size: 10px; font-weight: 600;\n color: #5f5a55; background: #eceae8;\n}\n.opportunities-extension-service-request-status[data-status=\"OPEN\"] { color: #17693a; background: #e1f1e5; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-requests-content,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-requests-table,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-requests-table th,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-requests-table td { color: #f6f4f2; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-requests-message { color: #c9c5c1; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-requests-message[role=\"alert\"] { color: #ffb4ab; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-requests-table th { background: #353331; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-requests-table th,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-requests-table td { border-color: #484541; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-request-status { color: #dedbd7; background: #484541; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-service-request-status[data-status=\"OPEN\"] { color: #b8e4c6; background: #214d34; }\n@media (prefers-reduced-motion: reduce) {\n .opportunities-extension-service-requests-table .opportunities-extension-account-plans-skeleton-bar { animation: none; }\n}\n\n/* Scope sizing to Cloud Consumption, not other dialogs sharing its components. */\n[data-consumption-layout=\"compact\"] .opportunities-extension-account-plans-header { padding: 10px 12px; gap: 12px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-account-plans-header h2 { font-size: 16px; line-height: 22px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-account-plans-header p { font-size: 12px; line-height: 16px; margin-top: 3px; overflow-wrap: anywhere; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-icon-button { width: 28px; height: 28px; min-width: 28px; min-height: 28px; padding: 4px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-account-plans-tabs { padding: 4px 12px 0; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-account-plan-tab { min-height: 30px; padding: 4px 8px; font-size: 12px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-account-plan-panel { padding: 8px 12px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-account-plan-details { gap: 0 12px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-account-plan-details > div { grid-template-columns: minmax(85px, 35%) minmax(0, 1fr); min-height: 32px; padding: 4px 0; gap: 6px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-account-plan-details dt { font-size: 11px; line-height: 16px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-account-plan-details dd { font-size: 12px; line-height: 16px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-consumption-trend { margin-top: 8px; padding-top: 6px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-consumption-periods button { min-height: 28px; padding: 4px 8px; font-size: 12px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-consumption-navigation { grid-template-columns: 28px minmax(120px, auto) 28px; min-height: 30px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-consumption-navigation strong { font-size: 12px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-consumption-navigation-button { width: 28px; height: 28px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-consumption-visual { grid-template-columns: minmax(0, 1fr) 150px; gap: 8px; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-consumption-svg { height: 100%; max-height: 100%; min-width: 0; }\n[data-consumption-layout=\"compact\"] .opportunities-extension-consumption-legend button { font-size: 11px; padding: 4px; gap: 5px; }\n\n/* Small viewports must scroll rather than clip details or collapse the chart. */\n@media (max-height: 560px), (max-width: 700px) {\n [data-consumption-layout=\"compact\"] .opportunities-extension-account-plans-panels { overflow: auto; }\n [data-consumption-layout=\"compact\"] .opportunities-extension-account-plan-panel { height: auto; min-height: 100%; grid-template-rows: auto minmax(280px, 1fr); }\n}\n@media (max-width: 700px) {\n [data-consumption-layout=\"compact\"] .opportunities-extension-account-plan-details { grid-template-columns: minmax(0, 1fr); }\n [data-consumption-layout=\"compact\"] .opportunities-extension-consumption-visual { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; }\n [data-consumption-layout=\"compact\"] .opportunities-extension-consumption-legend { display: flex; flex-wrap: wrap; }\n}\n\n.opportunities-extension-plan-detail-badges { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }\n.opportunities-extension-consumption-trend-header { align-items: center; justify-content: space-between; gap: 12px; }\n.opportunities-extension-consumption-trend-header > .opportunities-extension-plan-detail-badges { min-width: 0; }\n.opportunities-extension-plan-detail-skeleton { width: 64px; height: 20px; }\n.opportunities-extension-projection-badge {\n display: inline-flex; align-items: center; justify-content: center; border: 1px solid transparent;\n padding: 3px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; line-height: 1.3;\n color: #5f5a55; background: #eceae8; white-space: nowrap;\n}\nbutton.opportunities-extension-projection-badge { cursor: pointer; }\n.opportunities-extension-projection-badge[data-status=\"forfeit\"] { color: #a12424; background: #fbe5e5; }\n.opportunities-extension-projection-badge[data-status=\"overage\"] { color: #185a9d; background: #e0edfc; }\n.opportunities-extension-projection-badge:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-projection-popover {\n position: fixed; z-index: 8; box-sizing: border-box; padding: 14px; overflow: auto; overscroll-behavior: contain;\n border: 1px solid #b9b4ae; border-radius: 4px; background: #ffffff; color: #312d2a;\n box-shadow: 0 4px 16px #0003; font-size: 12px; line-height: 1.4; overflow-wrap: anywhere;\n}\n.opportunities-extension-projection-popover[hidden] { display: none; }\n.opportunities-extension-projection-popover .opportunities-extension-projection-dismiss {\n display: grid; place-items: center; width: 24px; height: 24px; margin-left: auto; margin-bottom: 8px; padding: 4px; border: 1px solid #b9b4ae;\n border-radius: 4px; background: transparent; color: inherit; cursor: pointer; font-size: 12px;\n}\n.opportunities-extension-projection-dismiss svg { width: 14px; height: 14px; }\n.opportunities-extension-projection-line h3 { margin: 0 0 8px; font-size: 12px; }\n.opportunities-extension-projection-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 16px; margin: 0; }\n.opportunities-extension-projection-fields > div { min-width: 0; }\n.opportunities-extension-projection-fields dt { font-weight: 600; color: #5f5a55; font-size: 11px; }\n.opportunities-extension-projection-fields dd { margin: 2px 0 0; font-variant-numeric: tabular-nums; }\n.opportunities-extension-projection-fields a { color: #006d7a; text-decoration: underline; }\n.opportunities-extension-projection-children { margin-top: 14px; padding-left: 12px; border-left: 2px solid #dedbd7; }\n.opportunities-extension-projection-children > section + section { margin-top: 16px; }\n.opportunities-extension-plan-detail-error { color: #a12424; font-size: 12px; }\n.opportunities-extension-projection-empty { color: #5f5a55; font-size: 12px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-popover { color: #f1efec; background: #302e2b; border-color: #716b64; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-badge { color: #dedbd7; background: #484541; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-badge[data-status=\"forfeit\"] { color: #ffb9b9; background: #682e32; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-badge[data-status=\"overage\"] { color: #c5dfff; background: #224b76; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-fields dt,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-empty { color: #cec9c2; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-fields a { color: #76dbea; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-plan-detail-error { color: #ffb9b9; }\n@media (max-width: 480px) { .opportunities-extension-projection-fields { grid-template-columns: minmax(0, 1fr); } }\n@media (max-width: 480px) {\n .opportunities-extension-consumption-trend-header > .opportunities-extension-consumption-periods { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n}\n@media (prefers-reduced-motion: reduce) { .opportunities-extension-plan-detail-skeleton { animation: none; } }\n\n.opportunities-extension-account-plan-details > .opportunities-extension-contract-row {\n align-items: center;\n}\n.opportunities-extension-contract-row > dt,\n.opportunities-extension-contract-row > dd,\n.opportunities-extension-contract-ratecard > span {\n line-height: 20px;\n}\n.opportunities-extension-contract-ratecard {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px 10px;\n}\n.opportunities-extension-ratecard-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n align-self: center;\n box-sizing: border-box;\n height: 28px;\n margin: 0;\n flex: 0 0 auto;\n padding: 0 8px;\n border: 1px solid #8aaeb9;\n border-radius: 4px;\n background: #fff;\n color: #006b80;\n font: inherit;\n font-size: 12px;\n font-weight: 600;\n line-height: 20px;\n cursor: pointer;\n}\n.opportunities-extension-ratecard-button:hover { background: #e8f4f6; }\n.opportunities-extension-ratecard-overlay {\n position: absolute;\n inset: 0;\n z-index: 8;\n display: flex;\n justify-content: flex-end;\n overflow: hidden;\n overflow: clip;\n background: rgba(0, 0, 0, 0);\n transition: background-color 320ms ease;\n}\n.opportunities-extension-ratecard-overlay[data-open=\"true\"] { background: rgba(0, 0, 0, .32); }\n.opportunities-extension-ratecard-drawer {\n --ratecard-bg: #fff;\n --ratecard-heading: #faf9f8;\n --ratecard-text: #312d2a;\n --ratecard-muted: #615a55;\n --ratecard-border: #dedbd8;\n --ratecard-accent: #00758f;\n --ratecard-skeleton: #eae7e4;\n display: grid;\n grid-template-rows: auto auto auto minmax(0, 1fr);\n width: min(1520px, calc(100% - 48px));\n height: 100%;\n min-width: 0;\n box-sizing: border-box;\n overflow: hidden;\n background: var(--ratecard-bg);\n color: var(--ratecard-text);\n box-shadow: -6px 0 24px rgba(0, 0, 0, .2);\n transform: translateX(100%);\n transition: transform 320ms cubic-bezier(.22, .61, .36, 1);\n}\n.opportunities-extension-ratecard-overlay[data-open=\"true\"] .opportunities-extension-ratecard-drawer { transform: translateX(0); }\n.opportunities-extension-ratecard-header > div { min-width: 0; overflow-wrap: anywhere; }\n.opportunities-extension-ratecard-plan-details { padding: 12px 20px 0; }\n.opportunities-extension-ratecard-toolbar { display: flex; align-items: center; gap: 16px; padding: 12px 20px; }\n.opportunities-extension-ratecard-search {\n width: min(360px, 100%);\n min-width: 0;\n box-sizing: border-box;\n padding: 9px 12px;\n border: 1px solid var(--ratecard-muted);\n border-radius: 4px;\n background: var(--ratecard-bg);\n color: var(--ratecard-text);\n font: inherit;\n font-size: 13px;\n}\n.opportunities-extension-ratecard-search::placeholder { color: var(--ratecard-muted); opacity: 1; }\n.opportunities-extension-ratecard-count { color: var(--ratecard-muted); font-size: 12px; white-space: nowrap; }\n.opportunities-extension-ratecard-viewport { overflow: auto; min-height: 0; min-width: 0; scrollbar-gutter: stable; }\n.opportunities-extension-ratecard-table { width: 100%; min-width: 1100px; table-layout: fixed; border-collapse: separate; border-spacing: 0; font-size: 12px; }\n.opportunities-extension-ratecard-table th, .opportunities-extension-ratecard-table td {\n padding: 11px 10px;\n border-bottom: 1px solid var(--ratecard-border);\n vertical-align: top;\n overflow-wrap: anywhere;\n text-align: left;\n}\n.opportunities-extension-ratecard-table th { position: sticky; top: 0; z-index: 1; background: var(--ratecard-heading); }\n.opportunities-extension-ratecard-sort {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 12px;\n align-items: center;\n gap: 5px;\n width: 100%;\n padding: 0;\n border: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n font-weight: 600;\n text-align: left;\n cursor: pointer;\n}\n.opportunities-extension-ratecard-sort-icon { color: var(--ratecard-accent); text-align: center; }\n.opportunities-extension-ratecard-table .opportunities-extension-ratecard-numeric { text-align: right; font-variant-numeric: tabular-nums; }\n.opportunities-extension-ratecard-table .opportunities-extension-ratecard-notice { padding: 28px 20px; text-align: center; }\n.opportunities-extension-ratecard-notice [role=\"alert\"] { color: #b3261e; }\n.opportunities-extension-ratecard-skeleton { display: block; width: 85%; height: 14px; border-radius: 3px; background: var(--ratecard-skeleton); animation: opportunities-ratecard-pulse 1.1s ease-in-out infinite alternate; }\n.opportunities-extension-ratecard-drawer :focus-visible, .opportunities-extension-ratecard-button:focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }\n.opportunities-extension-ratecard-overlay[data-closing=\"true\"] { pointer-events: none; background: rgba(0, 0, 0, 0); transition-duration: 180ms; }\n.opportunities-extension-ratecard-overlay[data-closing=\"true\"] .opportunities-extension-ratecard-drawer { transform: translateX(100%); transition-duration: 180ms; }\n[data-theme=\"dark\"] .opportunities-extension-ratecard-drawer {\n --ratecard-bg: #292725;\n --ratecard-heading: #35322f;\n --ratecard-text: #f5f3f1;\n --ratecard-muted: #c2bcb6;\n --ratecard-border: #514c47;\n --ratecard-accent: #59d1e5;\n --ratecard-skeleton: #514c47;\n}\n[data-theme=\"dark\"] .opportunities-extension-ratecard-button { color: #59d1e5; border-color: #6298a2; background: #292725; }\n[data-theme=\"dark\"] .opportunities-extension-ratecard-button:hover { background: #313f40; }\n[data-theme=\"dark\"] .opportunities-extension-ratecard-notice [role=\"alert\"] { color: #ffb4ab; }\n[data-theme=\"dark\"] .opportunities-extension-ratecard-drawer :focus-visible, [data-theme=\"dark\"] .opportunities-extension-ratecard-button:focus-visible { outline-color: #59d1e5; }\n@keyframes opportunities-ratecard-pulse { from { opacity: .45; } to { opacity: 1; } }\n@media (max-width: 720px) {\n .opportunities-extension-ratecard-drawer { width: 100%; }\n .opportunities-extension-ratecard-toolbar { flex-wrap: wrap; gap: 8px; }\n}\n@media (prefers-reduced-motion: reduce) {\n .opportunities-extension-ratecard-overlay, .opportunities-extension-ratecard-drawer, .opportunities-extension-ratecard-skeleton,\n .opportunities-extension-ratecard-overlay[data-closing=\"true\"], .opportunities-extension-ratecard-overlay[data-closing=\"true\"] .opportunities-extension-ratecard-drawer { animation: none; transition: none; }\n}\n\n.opportunities-extension-account-plans-dialog.opportunities-extension-install-base-dialog,\n.opportunities-extension-account-plans-dialog.opportunities-extension-service-requests-dialog {\n grid-template-rows: auto auto minmax(0, 1fr);\n}\n.opportunities-extension-customer-table-toolbar { padding: 12px 16px; border-bottom: 1px solid #dedbd7; }\n.opportunities-extension-customer-table-search {\n box-sizing: border-box;\n width: min(420px, 100%);\n min-width: 0;\n height: 38px;\n padding: 8px 12px;\n border: 1px solid #8f8a85;\n border-radius: 4px;\n background: #fff;\n color: #312d2a;\n font: inherit;\n font-size: 13px;\n}\n.opportunities-extension-customer-table-search::placeholder { color: #5f5a55; opacity: 1; }\n.opportunities-extension-customer-table-search:focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-table-toolbar { border-color: #484541; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-table-search { background: #353331; color: #f6f4f2; border-color: #8f8a85; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-table-search::placeholder { color: #c9c5c1; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-customer-table-search:focus-visible { outline-color: #59d1e5; }\n"; // Source: src/shared/messages.js /** Stable wire names. All payloads remain plain, serializable objects. */ function createMessageTypes() { return Object.freeze({ getXsrfToken: "opportunitiesExtension.getXsrfToken", refreshXsrfCookie: "opportunitiesExtension.refreshXsrfCookie", requestOpptyProducts: "opportunitiesExtension.requestOpptyProducts", opptyProductsProgress: "opportunitiesExtension.opptyProductsProgress", opptyProductsRequestDebug: "opportunitiesExtension.opptyProductsRequestDebug", requestConsumerTokenService: "opportunitiesExtension.requestConsumerTokenService", requestAccountPlans: "opportunitiesExtension.requestAccountPlans", requestInstallBase: "opportunitiesExtension.requestInstallBase", requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest", requestWlActions: "opportunitiesExtension.requestWlActions", requestActionTeamMembers: "opportunitiesExtension.requestActionTeamMembers", requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup", requestMgmtAction: "opportunitiesExtension.requestMgmtAction", requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest", requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar", requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest", prepareSrSession: "opportunitiesExtension.prepareSrSession", requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage", requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform", requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness", requestCurrentUser: "opportunitiesExtension.requestCurrentUser", invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache", requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend", requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail", requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan" }); } // Source: src/shared/debug-body.js function createDebugBodyPreview(text, limit) { if (text.length <= limit) return { value: text || "(empty response body)", truncated: false }; try { const normalized = text.replace(/^\uFEFF/, "").trimStart() .replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, ""); let value = JSON.parse(normalized); for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) { try { value = JSON.parse(value); } catch { break; } } // Bound retained data without cutting JSON syntax or losing sensitive field names. let remaining = Math.max(0, limit - 256); const visit = (node, depth = 0) => { if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]"; if (node === null || typeof node !== "object") { const serialized = JSON.stringify(node); if (serialized.length > remaining) { remaining = 0; return "[omitted from debug preview]"; } remaining -= serialized.length + 1; return node; } const result = Array.isArray(node) ? [] : Object.create(null); remaining -= 2; for (const key of Object.keys(node)) { const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2; if (remaining < cost + 64) break; remaining -= cost; result[key] = visit(node[key], depth + 1); } return result; }; return { value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }), truncated: true }; } catch { return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true }; } } // Source: src/content/contracts.js function createContentContracts() { const TILE_ID = "c_5c719a88b5624b1eaf81defe2c2ex4x5"; const TILE_LABEL_ID = `${TILE_ID}_0`; const SALES_GROUP_SELECTOR = "#yourapps_groupNode_sales"; const ADD_ICON_SELECTOR = ".flat-grid-cell.flat-grid-cell-addicon"; const MODAL_ID = "opportunities-extension-modal"; const STYLE_ID = "opportunities-extension-redwood-styles"; const ESCAPE_LISTENER_KEY = "opportunitiesExtensionEscapeListener"; const DEBUG_PANEL_ID = "opportunities-extension-debug-panel"; const OPPORTUNITIES_TABLE_ID = "opportunities-extension-table"; const STAGE_FILTERS_ID = "opportunities-extension-stage-filters"; const STATUS_FILTERS_ID = "opportunities-extension-status-filters"; const CUSTOMER_FILTER_BUTTON_ID = "opportunities-extension-customer-filter-button"; const CUSTOMER_FILTER_PANEL_ID = "opportunities-extension-customer-filter-panel"; const CUSTOMER_FILTER_SEARCH_ID = "opportunities-extension-customer-filter-search"; const CUSTOMER_FILTER_SELECT_ALL_ID = "opportunities-extension-customer-filter-select-all"; const CUSTOMER_FILTER_LIST_ID = "opportunities-extension-customer-filter-list"; const OWNER_FILTER_BUTTON_ID = "opportunities-extension-owner-filter-button"; const OWNER_FILTER_PANEL_ID = "opportunities-extension-owner-filter-panel"; const OWNER_FILTER_SEARCH_ID = "opportunities-extension-owner-filter-search"; const OWNER_FILTER_SELECT_ALL_ID = "opportunities-extension-owner-filter-select-all"; const OWNER_FILTER_LIST_ID = "opportunities-extension-owner-filter-list"; const TABLE_SEARCH_ID = "opportunities-extension-table-search"; const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard"; const PRODUCTS_PROGRESS_ID = "opportunities-extension-products-progress"; const PRODUCTS_CACHE_TIMESTAMP_ID = "opportunities-extension-products-cache-timestamp"; const FILTER_PREFERENCES_STORAGE_KEY = "opportunities-extension-filter-preferences"; const THEME_STORAGE_KEY = "opportunities-extension-theme"; const OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY = "opportunities-extension-opportunity-type-preferences"; const PREFERENCES_MODAL_ID = "opportunities-extension-preferences-modal"; const ACCOUNT_PLANS_MODAL_ID = "opportunities-extension-account-plans-modal"; const PRODUCTS_MESSAGE_LISTENER_KEY = "opportunitiesExtensionProductsMessageListener"; const OPPORTUNITY_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid="; const ACCOUNT_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/accounts/accounts-detail?id="; const DEBUG_BODY_LIMIT = 12000; const OPPORTUNITIES_PAGE_LIMIT = 15; const DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS = 24; const TOKEN_RELAY_URL = "https://eeho.fa.us2.oraclecloud.com/fscmRestApi/tokenrelay"; const OPPORTUNITIES_QUERY_URL = "https://eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/11.13.18.05/custom-actions/queries"; const AUTH_STATUS = { idle: "idle", pending: "pending", authenticated: "authenticated", unauthenticated: "unauthenticated" }; const CONSUMPTION_TREND_PERIODS = [ { value: "quarterly", label: "Quarterly", windowSize: 8 }, { value: "monthly", label: "Monthly", windowSize: 13 }, { value: "weekly", label: "Weekly", windowSize: 13 }, { value: "daily", label: "Daily", windowSize: 30 } ]; const CONSUMPTION_TREND_SERIES = [ { value: "funded", label: "Funded Allocation" }, { value: "overage", label: "Overage" }, { value: "average", label: "Average Consumption" } ]; const DEFAULT_CONSUMPTION_TREND_PERIOD = "weekly"; const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; const STAGE_OPTIONS = ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"]; const STATUS_OPTIONS = ["OPEN", "CLOSED", "LOST", "WON"]; const OPPORTUNITY_TYPE_OPTIONS = [ { label: "All records I can see", value: "ORA_ALLOPTIES" }, { label: "I am credit receiver", value: "ORA_CREDITRECEIVER_ISME" }, { label: "I am on the team", value: "ORA_MYSALESTEAMOPTIES" }, { label: "I or my subordinate is a credit receiver", value: "ORA_CREDITRECEIVER_MYORG" }, { label: "I own", value: "ORA_MYOPTIES" }, { label: "My subordinates are on the team", value: "ORA_MYSUBORDSSALESTEAMOPTIES" }, { label: "My subordinates own", value: "ORA_MYSUBORDINATESOPTIES" }, { label: "My territory", value: "ORA_MYASSGTERROPTIES" }, { label: "My territory hierarchy", value: "ORA_MYASSGDESCTERROPTIES" }, { label: "My territory membership", value: "ORA_MYASSGMEMBTERROPTIES" }, { label: "My territory membership hierarchy", value: "ORA_MYASSGMEMBDESCTERROPTIES" } ]; const DEFAULT_OPPORTUNITY_TYPE_VALUES = [ "ORA_MYSALESTEAMOPTIES", "ORA_MYASSGTERROPTIES", "ORA_CREDITRECEIVER_ISME" ]; const OPPTY_PRODUCTS_CACHE_OPTIONS = [ { label: "1 hour", value: 1 }, { label: "6 hours", value: 6 }, { label: "12 hours", value: 12 }, { label: "24 hours (default)", value: 24 }, { label: "48 hours", value: 48 }, { label: "7 days", value: 168 } ]; const PERIOD_OPTIONS = [ "Current Quarter", "Next Quarter", "Previous Quarter", "Current Fiscal Year", "4 Rolling Quarters (CQ + 3)", "RENEWALS (Current + Past Due)" ]; const ALLOWED_PATHS = [ "/hcmUI/faces/FuseWelcome", "/fscmUI/faces/FuseWelcome" ]; return { TILE_ID, TILE_LABEL_ID, SALES_GROUP_SELECTOR, ADD_ICON_SELECTOR, MODAL_ID, STYLE_ID, ESCAPE_LISTENER_KEY, DEBUG_PANEL_ID, OPPORTUNITIES_TABLE_ID, STAGE_FILTERS_ID, STATUS_FILTERS_ID, CUSTOMER_FILTER_BUTTON_ID, CUSTOMER_FILTER_PANEL_ID, CUSTOMER_FILTER_SEARCH_ID, CUSTOMER_FILTER_SELECT_ALL_ID, CUSTOMER_FILTER_LIST_ID, OWNER_FILTER_BUTTON_ID, OWNER_FILTER_PANEL_ID, OWNER_FILTER_SEARCH_ID, OWNER_FILTER_SELECT_ALL_ID, OWNER_FILTER_LIST_ID, TABLE_SEARCH_ID, STAGE_DASHBOARD_ID, PRODUCTS_PROGRESS_ID, PRODUCTS_CACHE_TIMESTAMP_ID, FILTER_PREFERENCES_STORAGE_KEY, THEME_STORAGE_KEY, OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, PREFERENCES_MODAL_ID, ACCOUNT_PLANS_MODAL_ID, PRODUCTS_MESSAGE_LISTENER_KEY, OPPORTUNITY_DETAIL_URL, ACCOUNT_DETAIL_URL, DEBUG_BODY_LIMIT, OPPORTUNITIES_PAGE_LIMIT, DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS, TOKEN_RELAY_URL, OPPORTUNITIES_QUERY_URL, AUTH_STATUS, CONSUMPTION_TREND_PERIODS, CONSUMPTION_TREND_SERIES, DEFAULT_CONSUMPTION_TREND_PERIOD, SVG_NAMESPACE, STAGE_OPTIONS, STATUS_OPTIONS, OPPORTUNITY_TYPE_OPTIONS, DEFAULT_OPPORTUNITY_TYPE_VALUES, OPPTY_PRODUCTS_CACHE_OPTIONS, PERIOD_OPTIONS, ALLOWED_PATHS }; } // Source: src/auth/session-state.js function createAuthSessionState({ AUTH_STATUS } = {}) { return { tokenRelayRequest: null, consumerTokenServiceRequest: null, accessToken: "", principal: "", authStatus: AUTH_STATUS.idle }; } // Source: src/auth/sales-common-state.js /** @returns {{salesCommonToken: null | {access_token: string, expiresAt: number}, salesCommonTokenRequest: null | Promise}} */ function createAuthSalesCommonState() { return { salesCommonToken: null, salesCommonTokenRequest: null }; } // Source: src/features/opportunities/query-state.js function createOpportunitiesQueryState() { return { periodRequestVersion: 0 }; } // Source: src/features/opportunities/table-state.js function createOpportunitiesTableState() { return { opportunitiesTableState: { items: [], status: "idle", message: "", sortKey: "", sortDirection: "ascending" } }; } // Source: src/features/opportunities/filter-state.js function createOpportunitiesFilterState({ STAGE_OPTIONS, STATUS_OPTIONS } = {}) { return { selectedStages: new Set(STAGE_OPTIONS), selectedStatuses: new Set(STATUS_OPTIONS), selectedCustomerKeys: new Set(), customerSearch: "", selectedOwnerKeys: new Set(), ownerSearch: "", tableSearch: "", hasSavedCustomerFilter: false, hasSavedOwnerFilter: false, customerFilterInitialized: false, ownerFilterInitialized: false }; } // Source: src/features/preferences/state.js function createPreferencesState({ DEFAULT_OPPORTUNITY_TYPE_VALUES, DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS } = {}) { return { selectedOpportunityTypeValues: new Set(DEFAULT_OPPORTUNITY_TYPE_VALUES), opptyProductsCacheHours: DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS }; } // Source: src/features/products/content-state.js function createProductsContentState() { return { currentProductsRequestId: "", productsRenderQueued: false, opptyProductsByNumber: new Map(), expandedProductOptyNumbers: new Set(), productsProgress: { state: "idle", completed: 0, total: 0, failed: 0, cacheHits: 0 }, productsLastCachedAt: 0 }; } // Source: src/features/ai-summary/state.js function createAiSummaryState() { return { aiSummariesByOptyNumber: new Map() }; } // Source: src/features/consumption/plans-state.js function createConsumptionPlansState() { return { accountPlansRequestState: new Map() }; } // Source: src/features/consumption/trend-state.js function createConsumptionTrendState() { return { consumptionTrendCache: new Map(), consumptionTrendWindowOffsets: new Map(), consumptionTrendSeriesFilters: new Map() }; } // Source: src/features/opportunities/dashboard-state.js function createOpportunitiesDashboardState({ STAGE_OPTIONS } = {}) { return { stageDashboardAmounts: new Map([["TOTAL", 0], ...STAGE_OPTIONS.map((stage) => [stage, 0])]) }; } // Source: src/features/debug/state.js function createDebugState() { return { requestLog: [], debugSecretsVisible: false }; } // Source: src/features/opportunities/columns.js function createOpportunitiesColumns({ formatOracleResponseDate, formatRevenue, formatWinProbability, getNestedValue, getOwnerName }) { return [ { key: "name", label: "Name", value: (item) => item.Name, display: (item) => item.Name }, { key: "optyNumber", label: "Opty Number", value: (item) => item.OptyNumber, display: (item) => item.OptyNumber }, { key: "customer", label: "Customer", value: (item) => getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]), display: (item) => getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]) }, { key: "owner", label: "Owner", value: getOwnerName, display: getOwnerName }, { key: "revenue", label: "Amount", value: (item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]), display: formatRevenue }, { key: "closeDate", label: "Close Date", value: (item) => item.EffectiveDate, display: (item) => formatOracleResponseDate(item.EffectiveDate) }, { key: "status", label: "Status", value: (item) => item.StatusCode, display: (item) => item.StatusCode }, { key: "winProbability", label: "Win Probability", value: (item) => getNestedValue(item, ["PrimaryRevenue", "WinProb"]), display: formatWinProbability }, { key: "stage", label: "Stage", value: (item) => item.ForecastGroup_c, display: (item) => item.ForecastGroup_c }, { key: "lastUpdateDate", label: "Last Update Date", value: (item) => item.LastUpdateDate, display: (item) => formatOracleResponseDate(item.LastUpdateDate, true) } ]; } // Source: src/features/opportunities/badge-content.js /* * Lucide icons: package, chart-no-axes-column-increasing, clipboard-list, list-checks. * https://github.com/lucide-icons/lucide (ISC License) * Copyright (c) 2026 Lucide Icons and Contributors * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ function createOpportunityBadgeContent({ environment }) { const shapes = { products: [ ["path", { d: "M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" }], ["path", { d: "M12 22V12" }], ["polyline", { points: "3.29 7 12 12 20.71 7" }], ["path", { d: "m7.5 4.27 9 5.15" }] ], ramp: [ ["path", { d: "M5 21v-6" }], ["path", { d: "M12 21V9" }], ["path", { d: "M19 21V3" }] ], sr: [ ["rect", { width: "8", height: "4", x: "8", y: "2", rx: "1", ry: "1" }], ["path", { d: "M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" }], ["path", { d: "M12 11h4" }], ["path", { d: "M12 16h4" }], ["path", { d: "M8 11h.01" }], ["path", { d: "M8 16h.01" }] ], actions: [ ["path", { d: "M13 5h8" }], ["path", { d: "M13 12h8" }], ["path", { d: "M13 19h8" }], ["path", { d: "m3 17 2 2 4-4" }], ["path", { d: "m3 7 2 2 4-4" }] ] }; function setContent(badge, kind, value) { badge.textContent = String(value); const namespace = "http://www.w3.org/2000/svg"; const icon = environment.document.createElementNS(namespace, "svg"); for (const [name, attribute] of Object.entries({ class: "opportunities-extension-badge-icon", viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", stroke: "currentColor", "stroke-width": "2.25", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", focusable: "false", "data-badge-icon": kind })) icon.setAttribute(name, attribute); for (const [tag, attributes] of shapes[kind]) { const shape = environment.document.createElementNS(namespace, tag); for (const [name, attribute] of Object.entries(attributes)) shape.setAttribute(name, attribute); icon.append(shape); } badge.append(icon); } return { setContent }; } // Source: src/content/fusion-page.js function createContentFusionPage({ ADD_ICON_SELECTOR, ALLOWED_PATHS, SALES_GROUP_SELECTOR, TILE_ID, TILE_LABEL_ID, appendPath, openOpportunitiesModal, environment }) { const { Node, document, window } = environment; function createOpportunitiesTile() { const wrapper = document.createElement("div"); wrapper.className = "flat-grid-cell"; const item = document.createElement("div"); item.id = TILE_ID; item.className = "app-nav-item opportunities-extension-tile"; item.setAttribute("filmstrip", "Opportunities Extension"); item.setAttribute("page", "undefined"); item.setAttribute("index", "0"); item.setAttribute("type", "subcluster"); item.setAttribute("title", "Opportunities Extension"); item.setAttribute("group", "groupNode_tools"); item.setAttribute("destinationurl", "https://gxpap.oracle.com/ords/pgxpap/f?p=138"); item.setAttribute("targetframe", "_blank"); item.setAttribute("isdesturlexist", "true"); item.setAttribute("role", "presentation"); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 48 48"); svg.setAttribute("style", "fill:currentColor"); svg.setAttribute("class", "svg-nav suiicon svg-bkgd09"); svg.setAttribute("data-icon", "navi_reportsearch"); svg.setAttribute("role", "presentation"); svg.setAttribute("focusable", "false"); appendPath(svg, "svg-shortcut", "M28 42.5l-3 2.7v-1.7c-.4 0-1.4 0-2.5.6-1.3 1-1.5 1.6-1.5 1.6s-.4-1.2.8-2.7c1.2-1.6 2.6-1.7 3.2-1.6v-1.6l3 2.7z"); appendPath(svg, "svg-cluster", "M28.5 41.3c.6 0 1.2.5 1.2 1.2s-.6 1.2-1.2 1.2-1.2-.5-1.2-1.2.5-1.2 1.2-1.2zm-4 0c.6 0 1.2.5 1.2 1.2s-.6 1.2-1.2 1.2c-.7 0-1.2-.5-1.2-1.2s.5-1.2 1.2-1.2zm-4 0c.7 0 1.2.5 1.2 1.2s-.5 1.2-1.2 1.2-1.2-.5-1.2-1.2.5-1.2 1.2-1.2z"); appendPath(svg, "svg-icon15", "M16 31l-1.6-1-3.4 6.5s0 1 .5 1.4c.5.2 1.4-.4 1.4-.4l3-6.7z"); appendPath(svg, "svg-icon03", "M36 10H12c-.8 0-2 1.2-2 2v20c0 .4.2.8.5 1l2-3.6c-1-1.4-1.6-3-1.6-5 0-4.2 3.3-7.6 7.4-7.6H20V16h2v1.7c.7.4 1.3 1 1.8 1.5.6.5 1 1 1.3 1.8h4v7h-4c-.3.7-.8 1.4-1.4 2H35v2H20.3l-2 .2H18L17 34h19c.8 0 2-1.2 2-2V12c0-.8-1.2-2-2-2zm-23 4v-2h22v2H13zm22 14h-5V17h5v11z"); appendPath(svg, "svg-icon12", "M18.5 19c-3 0-5.5 2.5-5.5 5.5s2.5 5.5 5.5 5.5 5.5-2.5 5.5-5.5-2.5-5.5-5.5-5.5zm0 9c-2 0-3.5-1.6-3.5-3.5 0-2 1.6-3.5 3.5-3.5s3.5 1.6 3.5 3.5c0 2-1.6 3.5-3.5 3.5z"); appendPath(svg, "svg-outline", "M35 34.56H13a2.7 2.7 0 0 1-3-3V14a2.76 2.76 0 0 1 3-3h22a2.74 2.74 0 0 1 3 3v17.56a2.68 2.68 0 0 1-3 3zM16.98 22.32a4.72 4.72 0 1 0 4.73 4.73 4.72 4.72 0 0 0-4.73-4.73zM24 25h3.47v4.72H24V25zm5.78-4.66h4.69v9.38h-4.69v-9.38zM13.5 14.5h20.9v2.4H13.5v-2.4zm6.9 17.59l-.01-1.94zm-2.04-12.75l-5.35-.02zm2.13 12.67H35.7zm-.09-8.05L20.39 19z"); appendPath(svg, "svg-outline", "M16.98 31.72a4.68 4.68 0 1 0-4.75-4.67 4.74 4.74 0 0 0 4.75 4.67zm-1.44-.5l-3.6 6.83zM8 40"); const link = document.createElement("a"); link.id = TILE_LABEL_ID; link.className = "app-nav-label flat-grid-nav-label"; link.href = "#"; link.textContent = "Opportunities Extension"; item.append(svg, link); wrapper.append(item); wrapper.addEventListener("click", handleTileClick); return wrapper; } function handleTileClick(event) { event.preventDefault(); event.stopPropagation(); openOpportunitiesModal(); } function isAllowedPage() { if (window.location.hostname !== "eeho.fa.us2.oraclecloud.com") { return false; } return ALLOWED_PATHS.some((path) => { return window.location.pathname === path || window.location.pathname.startsWith(`${path}/`); }); } function findInsertionPoint(salesGroup) { const addIconInsideGroup = salesGroup.querySelector(ADD_ICON_SELECTOR); if (addIconInsideGroup) { return addIconInsideGroup; } const container = salesGroup.closest(".flat-grid, .flat-grid-container, [id*='yourapps'], [id*='groupNode']") || salesGroup.parentElement || document.body; const addIconCells = Array.from(container.querySelectorAll(ADD_ICON_SELECTOR)); return addIconCells.find((cell) => { return Boolean(salesGroup.compareDocumentPosition(cell) & Node.DOCUMENT_POSITION_FOLLOWING); }) || null; } function insertTile() { if (document.getElementById(TILE_ID)) { return true; } const salesGroup = document.querySelector(SALES_GROUP_SELECTOR); const addIconCell = salesGroup ? findInsertionPoint(salesGroup) : null; if (!salesGroup || !addIconCell) { return false; } addIconCell.before(createOpportunitiesTile()); return true; } return { createOpportunitiesTile, handleTileClick, isAllowedPage, findInsertionPoint, insertTile }; } // Source: src/content/modal.js function createContentModal({ ACCOUNT_PLANS_MODAL_ID, AUTH_STATUS, CUSTOMER_FILTER_BUTTON_ID, CUSTOMER_FILTER_LIST_ID, CUSTOMER_FILTER_PANEL_ID, CUSTOMER_FILTER_SEARCH_ID, CUSTOMER_FILTER_SELECT_ALL_ID, DEBUG_PANEL_ID, ESCAPE_LISTENER_KEY, MODAL_ID, OPPORTUNITIES_TABLE_ID, OWNER_FILTER_BUTTON_ID, OWNER_FILTER_LIST_ID, OWNER_FILTER_PANEL_ID, OWNER_FILTER_SEARCH_ID, OWNER_FILTER_SELECT_ALL_ID, PERIOD_OPTIONS, PREFERENCES_MODAL_ID, PRODUCTS_CACHE_TIMESTAMP_ID, PRODUCTS_PROGRESS_ID, STAGE_DASHBOARD_ID, STAGE_FILTERS_ID, STATUS_FILTERS_ID, TABLE_SEARCH_ID, applyTheme, authSessionState, closeAccountPlansModal, closeInstallBaseModal, closeServiceRequestsModal, closeActionForm, closeCustomerFilterPanel, closeOwnerFilterPanel, closePreferencesModal, createSettingsIcon, createRefreshAllButton, createActivityFilterControls, debugState, ensureExtensionStyles, getCustomerOptions, getOwnerOptions, getSavedOpportunityTypeValues, getSavedOpptyProductsCacheHours, getSavedTheme, openPreferencesModal, opportunitiesFilterState, opportunitiesQueryState, preferencesState, renderCustomerFilter, renderCustomerFilterList, renderOpportunitiesTable, renderOwnerFilter, renderOwnerFilterList, renderStageFilterButtons, renderStatusFilterButtons, requestOpportunitiesForPeriod, closeCrmSrPopover, requestTokenRelayOnce, ensureCurrentUser, prepareSrSession, resetCustomerAndOwnerFiltersForPeriod, resetOpportunitiesTable, restoreFilterPreferences, saveFilterPreferences, toggleCustomerFilterPanel, toggleDebugPanel, toggleOwnerFilterPanel, environment }) { const { document, window, setTimeout, clearTimeout, matchMedia } = environment; let authCompletion = null; function resetAuthCompletion() { if (!authCompletion) return; clearTimeout(authCompletion.waitTimer); clearTimeout(authCompletion.hideTimer); authCompletion.badge.setAttribute("data-fading", "false"); authCompletion = null; } function openOpportunitiesModal() { ensureExtensionStyles(); debugState.debugSecretsVisible = false; const existingModal = document.getElementById(MODAL_ID); if (existingModal) { existingModal.hidden = false; prepareSrSession?.(); existingModal.querySelector("select").focus(); document.documentElement.classList.add("opportunities-extension-scroll-lock"); ensureEscapeKeyHandler(); ensureCurrentUser().then(() => { if (existingModal.hidden) return; return requestTokenRelayOnce(); }) .then(() => renderOpportunitiesTable()).catch(() => {}); return; } const overlay = document.createElement("div"); debugState.requestLog = []; authSessionState.tokenRelayRequest = null; authSessionState.consumerTokenServiceRequest = null; authSessionState.accessToken = ""; authSessionState.principal = ""; opportunitiesQueryState.periodRequestVersion = 0; authSessionState.authStatus = AUTH_STATUS.idle; resetOpportunitiesTable(); preferencesState.selectedOpportunityTypeValues = new Set(getSavedOpportunityTypeValues()); preferencesState.opptyProductsCacheHours = getSavedOpptyProductsCacheHours(); overlay.id = MODAL_ID; overlay.className = "opportunities-extension-modal"; overlay.setAttribute("data-theme", getSavedTheme()); overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-modal", "true"); overlay.setAttribute("aria-labelledby", "opportunities-extension-title"); const shell = document.createElement("section"); shell.className = "opportunities-extension-shell"; const header = document.createElement("header"); header.className = "opportunities-extension-header"; const title = document.createElement("h1"); title.id = "opportunities-extension-title"; title.textContent = "Opportunities Extension"; const authBadge = document.createElement("span"); authBadge.className = "opportunities-extension-auth-badge opportunities-extension-auth-badge-pending"; authBadge.setAttribute("data-auth-status", AUTH_STATUS.pending); authBadge.textContent = "Authenticating"; const subtitleRow = document.createElement("div"); subtitleRow.className = "opportunities-extension-subtitle-row"; const productsProgressBadge = document.createElement("span"); productsProgressBadge.id = PRODUCTS_PROGRESS_ID; productsProgressBadge.className = "opportunities-extension-products-progress"; productsProgressBadge.setAttribute("role", "status"); productsProgressBadge.hidden = true; const productsCacheTimestamp = document.createElement("span"); productsCacheTimestamp.id = PRODUCTS_CACHE_TIMESTAMP_ID; productsCacheTimestamp.className = "opportunities-extension-products-cache-timestamp"; productsCacheTimestamp.setAttribute("role", "status"); productsCacheTimestamp.hidden = true; subtitleRow.append(authBadge, productsProgressBadge, productsCacheTimestamp); const titleBlock = document.createElement("div"); titleBlock.className = "opportunities-extension-title-row"; titleBlock.append(title, subtitleRow); const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.className = "opportunities-extension-icon-button"; closeButton.setAttribute("aria-label", "Fechar"); closeButton.textContent = "X"; closeButton.addEventListener("click", closeOpportunitiesModal); const themeButton = document.createElement("button"); themeButton.type = "button"; themeButton.className = "opportunities-extension-icon-button opportunities-extension-theme-toggle"; themeButton.addEventListener("click", () => { const nextTheme = overlay.getAttribute("data-theme") === "dark" ? "light" : "dark"; applyTheme(overlay, themeButton, nextTheme); }); applyTheme(overlay, themeButton, overlay.getAttribute("data-theme")); const preferencesButton = document.createElement("button"); preferencesButton.type = "button"; preferencesButton.className = "opportunities-extension-icon-button"; preferencesButton.setAttribute("aria-label", "Abrir preferências"); preferencesButton.title = "Preferences"; preferencesButton.append(createSettingsIcon()); preferencesButton.addEventListener("click", openPreferencesModal); const headerActions = document.createElement("div"); headerActions.className = "opportunities-extension-header-actions"; headerActions.append(themeButton, createRefreshAllButton(() => select.value), preferencesButton, closeButton); header.append(titleBlock, headerActions); const body = document.createElement("main"); body.className = "opportunities-extension-body"; const form = document.createElement("form"); form.className = "opportunities-extension-form"; const field = document.createElement("label"); field.className = "opportunities-extension-field"; const labelText = document.createElement("span"); labelText.textContent = "Reference"; const selectWrap = document.createElement("span"); selectWrap.className = "opportunities-extension-select-wrap"; const select = document.createElement("select"); select.name = "opportunityPeriod"; PERIOD_OPTIONS.forEach((optionLabel) => { const option = document.createElement("option"); option.value = optionLabel; option.textContent = optionLabel; select.append(option); }); select.addEventListener("change", () => { resetCustomerAndOwnerFiltersForPeriod(); saveFilterPreferences(select.value); requestOpportunitiesForPeriod(select.value); }); selectWrap.append(select); field.append(labelText, selectWrap); restoreFilterPreferences(select); const stageFilter = document.createElement("section"); stageFilter.className = "opportunities-extension-stage-filter"; const stageFilterLabel = document.createElement("span"); stageFilterLabel.className = "opportunities-extension-stage-filter-label"; stageFilterLabel.textContent = "Stage"; const stageFilters = document.createElement("div"); stageFilters.id = STAGE_FILTERS_ID; stageFilters.className = "opportunities-extension-stage-filter-controls"; stageFilters.setAttribute("role", "group"); stageFilters.setAttribute("aria-label", "Filter by stage"); stageFilter.append(stageFilterLabel, stageFilters); const customerFilter = document.createElement("section"); customerFilter.className = "opportunities-extension-customer-filter"; const customerFilterLabel = document.createElement("span"); customerFilterLabel.className = "opportunities-extension-customer-filter-label"; customerFilterLabel.textContent = "Customer"; const customerFilterButton = document.createElement("button"); customerFilterButton.id = CUSTOMER_FILTER_BUTTON_ID; customerFilterButton.type = "button"; customerFilterButton.className = "opportunities-extension-customer-filter-trigger"; customerFilterButton.setAttribute("aria-expanded", "false"); customerFilterButton.setAttribute("aria-controls", CUSTOMER_FILTER_PANEL_ID); customerFilterButton.addEventListener("click", toggleCustomerFilterPanel); const customerFilterPanel = document.createElement("section"); customerFilterPanel.id = CUSTOMER_FILTER_PANEL_ID; customerFilterPanel.className = "opportunities-extension-customer-filter-panel"; customerFilterPanel.hidden = true; const customerSearchInput = document.createElement("input"); customerSearchInput.id = CUSTOMER_FILTER_SEARCH_ID; customerSearchInput.className = "opportunities-extension-customer-filter-search"; customerSearchInput.type = "search"; customerSearchInput.placeholder = "Search customers"; customerSearchInput.setAttribute("aria-label", "Search customers"); customerSearchInput.addEventListener("input", () => { opportunitiesFilterState.customerSearch = customerSearchInput.value; saveFilterPreferences(); renderCustomerFilterList(); }); const selectAllLabel = document.createElement("label"); selectAllLabel.className = "opportunities-extension-customer-filter-select-all"; const selectAllCheckbox = document.createElement("input"); selectAllCheckbox.id = CUSTOMER_FILTER_SELECT_ALL_ID; selectAllCheckbox.type = "checkbox"; selectAllCheckbox.addEventListener("change", () => { const customerOptions = getCustomerOptions(); opportunitiesFilterState.selectedCustomerKeys = selectAllCheckbox.checked ? new Set(customerOptions.map((customer) => customer.key)) : new Set(); opportunitiesFilterState.hasSavedCustomerFilter = true; saveFilterPreferences(); renderCustomerFilter(); renderOpportunitiesTable(); }); const selectAllText = document.createElement("span"); selectAllText.textContent = "Select all"; selectAllLabel.append(selectAllCheckbox, selectAllText); const customerFilterList = document.createElement("div"); customerFilterList.id = CUSTOMER_FILTER_LIST_ID; customerFilterList.className = "opportunities-extension-customer-filter-list"; customerFilterPanel.append(customerSearchInput, selectAllLabel, customerFilterList); customerFilter.append(customerFilterLabel, customerFilterButton, customerFilterPanel); const ownerFilter = document.createElement("section"); ownerFilter.className = "opportunities-extension-customer-filter"; const ownerFilterLabel = document.createElement("span"); ownerFilterLabel.className = "opportunities-extension-customer-filter-label"; ownerFilterLabel.textContent = "Owner"; const ownerFilterButton = document.createElement("button"); ownerFilterButton.id = OWNER_FILTER_BUTTON_ID; ownerFilterButton.type = "button"; ownerFilterButton.className = "opportunities-extension-customer-filter-trigger"; ownerFilterButton.setAttribute("aria-expanded", "false"); ownerFilterButton.setAttribute("aria-controls", OWNER_FILTER_PANEL_ID); ownerFilterButton.addEventListener("click", toggleOwnerFilterPanel); const ownerFilterPanel = document.createElement("section"); ownerFilterPanel.id = OWNER_FILTER_PANEL_ID; ownerFilterPanel.className = "opportunities-extension-customer-filter-panel"; ownerFilterPanel.hidden = true; const ownerSearchInput = document.createElement("input"); ownerSearchInput.id = OWNER_FILTER_SEARCH_ID; ownerSearchInput.className = "opportunities-extension-customer-filter-search"; ownerSearchInput.type = "search"; ownerSearchInput.placeholder = "Search owners"; ownerSearchInput.setAttribute("aria-label", "Search owners"); ownerSearchInput.addEventListener("input", () => { opportunitiesFilterState.ownerSearch = ownerSearchInput.value; saveFilterPreferences(); renderOwnerFilterList(); }); const ownerSelectAllLabel = document.createElement("label"); ownerSelectAllLabel.className = "opportunities-extension-customer-filter-select-all"; const ownerSelectAllCheckbox = document.createElement("input"); ownerSelectAllCheckbox.id = OWNER_FILTER_SELECT_ALL_ID; ownerSelectAllCheckbox.type = "checkbox"; ownerSelectAllCheckbox.addEventListener("change", () => { const ownerOptions = getOwnerOptions(); opportunitiesFilterState.selectedOwnerKeys = ownerSelectAllCheckbox.checked ? new Set(ownerOptions.map((owner) => owner.key)) : new Set(); opportunitiesFilterState.hasSavedOwnerFilter = true; saveFilterPreferences(); renderOwnerFilter(); renderOpportunitiesTable(); }); const ownerSelectAllText = document.createElement("span"); ownerSelectAllText.textContent = "Select all"; ownerSelectAllLabel.append(ownerSelectAllCheckbox, ownerSelectAllText); const ownerFilterList = document.createElement("div"); ownerFilterList.id = OWNER_FILTER_LIST_ID; ownerFilterList.className = "opportunities-extension-customer-filter-list"; ownerFilterPanel.append(ownerSearchInput, ownerSelectAllLabel, ownerFilterList); ownerFilter.append(ownerFilterLabel, ownerFilterButton, ownerFilterPanel); const tableSearchField = document.createElement("label"); tableSearchField.className = "opportunities-extension-table-search-field"; const tableSearchLabel = document.createElement("span"); tableSearchLabel.textContent = "Search"; const tableSearchInput = document.createElement("input"); tableSearchInput.id = TABLE_SEARCH_ID; tableSearchInput.type = "search"; tableSearchInput.placeholder = "Search opportunities"; tableSearchInput.setAttribute("aria-label", "Search opportunities"); tableSearchInput.value = opportunitiesFilterState.tableSearch; tableSearchInput.addEventListener("input", () => { opportunitiesFilterState.tableSearch = tableSearchInput.value; saveFilterPreferences(); renderOpportunitiesTable(); }); tableSearchField.append(tableSearchLabel, tableSearchInput); const statusFilter = document.createElement("section"); statusFilter.className = "opportunities-extension-status-filter"; const statusFilterLabel = document.createElement("span"); statusFilterLabel.className = "opportunities-extension-status-filter-label"; statusFilterLabel.textContent = "Status"; const statusFilters = document.createElement("div"); statusFilters.id = STATUS_FILTERS_ID; statusFilters.className = "opportunities-extension-status-filter-controls"; statusFilters.setAttribute("role", "group"); statusFilters.setAttribute("aria-label", "Filter by status"); statusFilter.append(statusFilterLabel, statusFilters); form.addEventListener("submit", (event) => event.preventDefault()); const badgeFilters = createActivityFilterControls(form, statusFilter, stageFilter); form.append(field, customerFilter, ownerFilter, tableSearchField, badgeFilters); body.append(form); const stageDashboard = document.createElement("section"); stageDashboard.id = STAGE_DASHBOARD_ID; stageDashboard.className = "opportunities-extension-stage-dashboard"; stageDashboard.setAttribute("aria-label", "Opportunity amount by stage"); body.append(stageDashboard); const tableSurface = document.createElement("section"); tableSurface.className = "opportunities-extension-table-surface"; const opportunitiesTable = document.createElement("table"); opportunitiesTable.id = OPPORTUNITIES_TABLE_ID; opportunitiesTable.className = "opportunities-extension-table"; opportunitiesTable.setAttribute("aria-label", "Opportunities"); tableSurface.append(opportunitiesTable); body.append(tableSurface); const debugPanel = document.createElement("aside"); debugPanel.id = DEBUG_PANEL_ID; debugPanel.className = "opportunities-extension-debug-panel"; debugPanel.hidden = true; body.append(debugPanel); shell.append(header, body); overlay.append(shell); document.body.append(overlay); document.documentElement.classList.add("opportunities-extension-scroll-lock"); ensureEscapeKeyHandler(); renderStageFilterButtons(); renderStatusFilterButtons(); renderCustomerFilter(); renderOwnerFilter(); renderOpportunitiesTable(); requestOpportunitiesForPeriod(select.value); prepareSrSession?.(); select.focus(); } function updateAuthBadge(status) { const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`); if (!badge) { return; } const badgeStatus = status === AUTH_STATUS.authenticated || status === AUTH_STATUS.unauthenticated ? status : AUTH_STATUS.pending; if (authCompletion && (authCompletion.badge !== badge || badgeStatus !== AUTH_STATUS.authenticated)) resetAuthCompletion(); badge.hidden = Boolean(authCompletion?.hidden); if (badgeStatus === AUTH_STATUS.authenticated && !authCompletion) { const current = { badge, hidden: false, waitTimer: null, hideTimer: null }; authCompletion = current; badge.setAttribute("data-fading", "false"); current.waitTimer = setTimeout(() => { if (authCompletion !== current) return; badge.setAttribute("data-fading", "true"); const reducedMotion = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches; current.hideTimer = setTimeout(() => { if (authCompletion !== current) return; current.hidden = true; badge.hidden = true; }, reducedMotion ? 0 : 300); }, 5000); } badge.setAttribute("data-auth-status", badgeStatus); badge.className = `opportunities-extension-auth-badge opportunities-extension-auth-badge-${badgeStatus}`; badge.textContent = badgeStatus === AUTH_STATUS.authenticated ? "Authenticated" : badgeStatus === AUTH_STATUS.unauthenticated ? "Unauthenticated" : "Authenticating"; } function closeOpportunitiesModal() { resetAuthCompletion(); closeCrmSrPopover(); const modal = document.getElementById(MODAL_ID); if (modal) { closePreferencesModal(); closeAccountPlansModal(); closeInstallBaseModal(); closeServiceRequestsModal(); closeActionForm(false); modal.hidden = true; } debugState.debugSecretsVisible = false; document.documentElement.classList.remove("opportunities-extension-scroll-lock"); } function ensureEscapeKeyHandler() { if (window[ESCAPE_LISTENER_KEY]) { return; } window[ESCAPE_LISTENER_KEY] = true; document.addEventListener("keydown", (event) => { const modal = document.getElementById(MODAL_ID); if (!modal || modal.hidden) { return; } if (event.ctrlKey && event.key.toLowerCase() === "d") { event.preventDefault(); event.stopPropagation(); toggleDebugPanel(); return; } if (event.key === "Escape") { if (closeActionForm()) { event.preventDefault(); return; } if (closeServiceRequestsModal()) { event.preventDefault(); return; } if (closeInstallBaseModal()) { event.preventDefault(); return; } const accountPlansModal = document.getElementById(ACCOUNT_PLANS_MODAL_ID); if (accountPlansModal && !accountPlansModal.hidden) { closeAccountPlansModal(); event.preventDefault(); return; } const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID); if (preferencesModal && !preferencesModal.hidden) { closePreferencesModal(); event.preventDefault(); return; } if (closeCustomerFilterPanel()) { event.preventDefault(); return; } if (closeOwnerFilterPanel()) { event.preventDefault(); return; } closeOpportunitiesModal(); } }); document.addEventListener("pointerdown", (event) => { const modal = document.getElementById(MODAL_ID); const panel = document.getElementById(CUSTOMER_FILTER_PANEL_ID); const ownerPanel = document.getElementById(OWNER_FILTER_PANEL_ID); if (!modal || modal.hidden) { return; } if (panel && !panel.hidden) { const customerFilter = panel.closest(".opportunities-extension-customer-filter"); if (customerFilter && !customerFilter.contains(event.target)) { closeCustomerFilterPanel(false); } } if (ownerPanel && !ownerPanel.hidden) { const ownerFilter = ownerPanel.closest(".opportunities-extension-customer-filter"); if (ownerFilter && !ownerFilter.contains(event.target)) { closeOwnerFilterPanel(false); } } }); } return { openOpportunitiesModal, updateAuthBadge, closeOpportunitiesModal, ensureEscapeKeyHandler }; } // Source: src/content/theme.js function createContentTheme({ THEME_STORAGE_KEY, createThemeIcon, environment }) { const getLocalStorage = () => environment.localStorage; function getSavedTheme() { try { return getLocalStorage().getItem(THEME_STORAGE_KEY) === "dark" ? "dark" : "light"; } catch (error) { return "light"; } } function applyTheme(modal, button, theme) { const isDarkTheme = theme === "dark"; modal.setAttribute("data-theme", isDarkTheme ? "dark" : "light"); button.textContent = ""; button.setAttribute("aria-label", isDarkTheme ? "Ativar modo claro" : "Ativar modo escuro"); button.title = isDarkTheme ? "Light mode" : "Dark mode"; button.append(createThemeIcon(isDarkTheme ? "sun" : "moon")); try { getLocalStorage().setItem(THEME_STORAGE_KEY, isDarkTheme ? "dark" : "light"); } catch (error) { // Continue with the selected theme when browser storage is unavailable. } } return { getSavedTheme, applyTheme }; } // Source: src/content/icons.js function createContentIcons({ appendPath, environment }) { const { document } = environment; function createThemeIcon(iconName) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); if (iconName === "sun") { path.setAttribute("d", "M12 3V1m0 22v-2m9-9h2M1 12h2m15.36 6.36 1.42 1.42M4.22 4.22l1.42 1.42m12.72 0 1.42-1.42M4.22 19.78l1.42-1.42M16.5 12a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Z"); } else { path.setAttribute("d", "M20.5 14.2A8.5 8.5 0 0 1 9.8 3.5a8.5 8.5 0 1 0 10.7 10.7Z"); } path.setAttribute("fill", "none"); path.setAttribute("stroke", "currentColor"); path.setAttribute("stroke-linecap", "round"); path.setAttribute("stroke-linejoin", "round"); path.setAttribute("stroke-width", "1.8"); svg.append(path); return svg; } function createSettingsIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const upperControl = document.createElementNS("http://www.w3.org/2000/svg", "circle"); const lowerControl = document.createElementNS("http://www.w3.org/2000/svg", "circle"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("class", "opportunities-extension-settings-icon"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); appendPath(svg, "", "M4 7h16M4 17h16"); [upperControl, lowerControl].forEach((control, index) => { control.setAttribute("cx", index === 0 ? "9" : "15"); control.setAttribute("cy", index === 0 ? "7" : "17"); control.setAttribute("r", "2.5"); control.setAttribute("fill", "currentColor"); control.setAttribute("stroke", "currentColor"); control.setAttribute("stroke-width", "1.8"); }); svg.querySelector("path").setAttribute("fill", "none"); svg.querySelector("path").setAttribute("stroke", "currentColor"); svg.querySelector("path").setAttribute("stroke-linecap", "round"); svg.querySelector("path").setAttribute("stroke-width", "1.8"); svg.append(upperControl, lowerControl); return svg; } function createCloseIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); path.setAttribute("d", "m6 6 12 12M18 6 6 18"); path.setAttribute("fill", "none"); path.setAttribute("stroke", "currentColor"); path.setAttribute("stroke-linecap", "round"); path.setAttribute("stroke-width", "1.8"); svg.append(path); return svg; } function createRefreshIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); path.setAttribute("d", "M20 7v5h-5M4 17v-5h5M6.1 6.1A8 8 0 0 1 19.7 10M4.3 14a8 8 0 0 0 13.6 3.9"); path.setAttribute("fill", "none"); path.setAttribute("stroke", "currentColor"); path.setAttribute("stroke-width", "1.8"); path.setAttribute("stroke-linecap", "round"); path.setAttribute("stroke-linejoin", "round"); svg.append(path); return svg; } return { createThemeIcon, createSettingsIcon, createCloseIcon, createRefreshIcon }; } // Source: src/content/svg.js function createSvgPaths({ environment }) { const { document } = environment; function appendPath(svg, className, d) { const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("class", className); path.setAttribute("d", d); svg.append(path); } return { appendPath }; } // Source: src/content/styles.js function createContentStyles({ STYLE_ID, redwoodStyles, environment }) { const { document } = environment; function ensureExtensionStyles() { if (document.getElementById(STYLE_ID)) { return; } const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = redwoodStyles; document.head.append(style); } return { ensureExtensionStyles }; } // Source: src/auth/content-session.js function createAuthContentSession({ messageTypes, AUTH_STATUS, appendBackgroundRequestLog, authSessionState, requestTokenRelay, sendRuntimeMessage, updateAuthBadge }) { function requestTokenRelayOnce() { if (!authSessionState.tokenRelayRequest) { authSessionState.authStatus = AUTH_STATUS.pending; updateAuthBadge(authSessionState.authStatus); authSessionState.tokenRelayRequest = requestTokenRelay() .then((token) => { authSessionState.accessToken = token; authSessionState.authStatus = AUTH_STATUS.authenticated; requestConsumerTokenServiceOnce().catch(() => {}); return token; }) .catch((error) => { authSessionState.accessToken = ""; authSessionState.principal = ""; authSessionState.authStatus = AUTH_STATUS.unauthenticated; throw error; }) .finally(() => { updateAuthBadge(authSessionState.authStatus); }); } else { updateAuthBadge(authSessionState.authStatus); } return authSessionState.tokenRelayRequest; } function requestConsumerTokenServiceOnce() { if (!authSessionState.consumerTokenServiceRequest) { authSessionState.consumerTokenServiceRequest = sendRuntimeMessage({ type: messageTypes.requestConsumerTokenService }).then((response) => { appendBackgroundRequestLog(response && response.debugRequests); if (!response || !response.ok) { throw new Error(response && response.error ? response.error : "Unable to load consumer token service."); } return response.authorizations; }); } return authSessionState.consumerTokenServiceRequest; } return { requestTokenRelayOnce, requestConsumerTokenServiceOnce }; } // Source: src/auth/current-user.js function createCurrentUser({ sendRuntimeMessage, appendBackgroundRequestLog, messageTypes }) { let pending = null; let allowed = false; let userId = ""; function ensure() { if (!pending) { pending = sendRuntimeMessage({ type: messageTypes.requestCurrentUser }).then(response => { appendBackgroundRequestLog(response?.debugRequests); const roles = response?.user?.roles; userId = response?.ok === true && typeof response.user?.id === "string" ? response.user.id : ""; allowed = response?.ok === true && Array.isArray(roles) && roles.every(role => typeof role === "string") && !roles.includes("SALES_REP"); return allowed; }).catch(() => { allowed = false; return false; }); } return pending; } return { ensure, allowsCrmActivities: () => allowed, getUserId: () => userId }; } // Source: src/auth/sales-common.js function createAuthSalesCommon({ TOKEN_RELAY_URL, authSalesCommonState, requestTokenRelay }) { function requestTokenRelaySalesCommon() { if (authSalesCommonState.salesCommonToken && Date.now() < authSalesCommonState.salesCommonToken.expiresAt) { return Promise.resolve(authSalesCommonState.salesCommonToken.access_token); } if (authSalesCommonState.salesCommonTokenRequest) { return authSalesCommonState.salesCommonTokenRequest; } authSalesCommonState.salesCommonTokenRequest = requestTokenRelay({ name: "requestTokenRelaySalesCommon", url: `${TOKEN_RELAY_URL}?scopesfq=urn%3Aopc%3Aresource%3Afusion%3Aeeho%3Asales-common%2F`, includeExpiry: true }).then((token) => { authSalesCommonState.salesCommonToken = token; return token.access_token; }).finally(() => { authSalesCommonState.salesCommonTokenRequest = null; }); return authSalesCommonState.salesCommonTokenRequest; } function invalidateSalesCommonToken(rejectedToken) { if (authSalesCommonState.salesCommonToken && authSalesCommonState.salesCommonToken.access_token === rejectedToken) { authSalesCommonState.salesCommonToken = null; } } return { requestTokenRelaySalesCommon, invalidateSalesCommonToken }; } // Source: src/auth/token-relay.js function createAuthTokenRelay({ TOKEN_RELAY_URL, authSessionState, createDebugBody, getXsrfToken, logRequestFailure, logRequestStart, logRequestSuccess, maskToken, readResponseBodyForDebug, refreshXsrfCookie, renderDebugPanel, environment }) { const { fetch } = environment; async function requestTokenRelay({ name = "requestTokenRelay", url = TOKEN_RELAY_URL, includeExpiry = false } = {}) { if (url === TOKEN_RELAY_URL) authSessionState.principal = ""; let refreshResult = null; let refreshAttempted = false; while (true) { const xsrfTokenSource = await getXsrfToken(); if (!xsrfTokenSource.token) { if (refreshAttempted) { const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult, name, url); logRequestFailure(requestEntry, "XSRF token cookie not found after automatic refresh."); throw new Error("XSRF token cookie not found after automatic refresh."); } refreshResult = await refreshXsrfCookie("missing-cookie"); refreshAttempted = true; continue; } const requestEntry = createTokenRelayRequestEntry(xsrfTokenSource, refreshResult, name, url); try { const response = await fetch(url, { method: "GET", credentials: "include", headers: { "x-xsrf-token": xsrfTokenSource.token } }); const responseBody = await readResponseBodyForDebug(response); if (!response.ok) { logRequestSuccess(requestEntry, response, responseBody); if (!refreshAttempted && isInvalidXsrfStatus(response.status)) { refreshResult = await refreshXsrfCookie(`tokenrelay-${response.status}`); refreshAttempted = true; requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult); renderDebugPanel(); continue; } throw new Error(`Token relay failed with status ${response.status}.`); } let responseData; try { responseData = await response.json(); } catch (error) { logRequestSuccess(requestEntry, response, responseBody); if (!refreshAttempted) { refreshResult = await refreshXsrfCookie("tokenrelay-invalid-response"); refreshAttempted = true; requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult); renderDebugPanel(); continue; } throw error; } if (!responseData || typeof responseData.access_token !== "string" || !responseData.access_token) { logRequestSuccess(requestEntry, response, responseBody); if (!refreshAttempted) { refreshResult = await refreshXsrfCookie("tokenrelay-missing-access-token"); refreshAttempted = true; requestEntry.metadata.xsrfRefresh = formatXsrfRefreshResult(refreshResult); renderDebugPanel(); continue; } throw new Error("Token relay response does not contain access_token."); } logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({ ...responseData, access_token: maskToken(responseData.access_token) }, null, 2))); if (url === TOKEN_RELAY_URL) { authSessionState.principal = typeof responseData.principal === "string" ? responseData.principal.trim() : ""; } if (includeExpiry) { const lifetime = Number(responseData.expires_in); return { access_token: responseData.access_token, expiresAt: Number.isFinite(lifetime) && lifetime > 0 ? requestEntry.startedAt.getTime() + lifetime * 1000 : 0 }; } return responseData.access_token; } catch (error) { logRequestFailure(requestEntry, error.message || "Request failed."); throw error; } } } function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult, name = "requestTokenRelay", url = TOKEN_RELAY_URL) { return logRequestStart({ name, method: "GET", url, headers: { "x-xsrf-token": xsrfTokenSource.token ? maskToken(xsrfTokenSource.token) : "(missing)" }, metadata: { xsrfCookieName: xsrfTokenSource.cookieName || "(missing)", xsrfTokenSource: xsrfTokenSource.source, xsrfMatchedCookies: xsrfTokenSource.matchedCookieNames && xsrfTokenSource.matchedCookieNames.length ? xsrfTokenSource.matchedCookieNames.join(", ") : "(none)", xsrfLookupDetails: xsrfTokenSource.lookupDetails && xsrfTokenSource.lookupDetails.length ? xsrfTokenSource.lookupDetails.join(" | ") : "(none)", xsrfTokenError: xsrfTokenSource.error || "", xsrfRefresh: formatXsrfRefreshResult(refreshResult) } }); } function isInvalidXsrfStatus(status) { return status === 400 || status === 401 || status === 403; } function formatXsrfRefreshResult(result) { if (!result) { return "not-requested"; } const outcome = result.refreshed ? "refreshed" : "not-refreshed"; const cookieName = result.cookieName || "(none)"; const duration = Number.isFinite(result.durationMs) ? `${result.durationMs}ms` : "unknown"; return `${result.reason}; ${outcome}; cookie=${cookieName}; duration=${duration}; ${result.error || ""}`.trim(); } return { requestTokenRelay, createTokenRelayRequestEntry, isInvalidXsrfStatus, formatXsrfRefreshResult }; } // Source: src/auth/xsrf-content.js function createAuthXsrfContent({ getXsrfTokenFromExtensionCookies, environment }) { const { document } = environment; async function getXsrfToken() { const extensionCookie = await getXsrfTokenFromExtensionCookies(); if (extensionCookie.token) { return extensionCookie; } const xsrfCookie = document.cookie .split(";") .map((cookie) => cookie.trim()) .find((cookie) => cookie.startsWith("XSRF-TOKEN-")); if (!xsrfCookie) { return { token: "", cookieName: "", source: extensionCookie.error ? "document.cookie fallback after cookies API error" : "document.cookie fallback", matchedCookieNames: extensionCookie.matchedCookieNames || [], lookupDetails: extensionCookie.lookupDetails || [], error: extensionCookie.error || "" }; } const cookieName = xsrfCookie.slice(0, xsrfCookie.indexOf("=")); const tokenValue = xsrfCookie.slice(xsrfCookie.indexOf("=") + 1); return { token: decodeURIComponent(tokenValue), cookieName, source: "document.cookie", matchedCookieNames: extensionCookie.matchedCookieNames || [], lookupDetails: extensionCookie.lookupDetails || [], error: "" }; } return { getXsrfToken }; } // Source: src/platform/content-messages.js function createPlatformContentMessages({ messageTypes, environment }) { const { browser, chrome } = environment; function sendRuntimeMessage(message) { const runtimeApi = typeof browser !== "undefined" ? browser : chrome; if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) { return Promise.reject(new Error("Runtime messaging API unavailable.")); } if (typeof browser !== "undefined") { return runtimeApi.runtime.sendMessage(message); } return new Promise((resolve, reject) => { runtimeApi.runtime.sendMessage(message, (response) => { const lastError = runtimeApi.runtime.lastError; if (lastError) { reject(new Error(lastError.message)); return; } resolve(response); }); }); } function getXsrfTokenFromExtensionCookies() { const runtimeApi = typeof browser !== "undefined" ? browser : chrome; if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) { return Promise.resolve({ token: "", cookieName: "", source: "unavailable cookies API", error: "Runtime messaging API unavailable." }); } return new Promise((resolve) => { runtimeApi.runtime.sendMessage({ type: messageTypes.getXsrfToken }, (response) => { const lastError = runtimeApi.runtime.lastError; if (lastError) { resolve({ token: "", cookieName: "", source: "cookies API", matchedCookieNames: [], lookupDetails: [], error: lastError.message }); return; } resolve({ token: response && response.ok ? response.token : "", cookieName: response && response.ok ? response.cookieName : "", source: "cookies API", matchedCookieNames: response && response.matchedCookieNames ? response.matchedCookieNames : [], lookupDetails: response && response.lookupDetails ? response.lookupDetails : [], error: response && response.error ? response.error : "" }); }); }); } function refreshXsrfCookie(reason) { const runtimeApi = typeof browser !== "undefined" ? browser : chrome; if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) { return Promise.resolve({ ok: false, refreshed: false, cookieName: "", durationMs: 0, reason, error: "Runtime messaging API unavailable." }); } return new Promise((resolve) => { runtimeApi.runtime.sendMessage({ type: messageTypes.refreshXsrfCookie }, (response) => { const lastError = runtimeApi.runtime.lastError; if (lastError) { resolve({ ok: false, refreshed: false, cookieName: "", durationMs: 0, reason, error: lastError.message }); return; } resolve({ ...(response || { ok: false, refreshed: false, cookieName: "", durationMs: 0, error: "Empty refresh response." }), reason }); }); }); } return { sendRuntimeMessage, getXsrfTokenFromExtensionCookies, refreshXsrfCookie }; } // Source: src/features/opportunities/controller.js function createOpportunitiesController({ STAGE_OPTIONS, STATUS_OPTIONS, authSessionState, consumptionPlansState, consumptionTrendState, getFiscalPeriodRange, opportunitiesDashboardState, opportunitiesFilterState, opportunitiesQueryState, opportunitiesTableState, preferencesState, productsContentState, renderProductsProgress, requestOpportunitiesPage, resetCrmServiceRequests, resetWorkloadActions, requestOpptyProductsInBackground, requestTokenRelayOnce, ensureCurrentUser, setOpportunitiesTableState }) { async function requestOpportunitiesForPeriod(period) { const dateRange = getFiscalPeriodRange(period, new Date()); const requestVersion = ++opportunitiesQueryState.periodRequestVersion; resetWorkloadActions(); await ensureCurrentUser(); if (!isCurrentPeriodRequest(requestVersion)) return; if (preferencesState.selectedOpportunityTypeValues.size === 0) { setOpportunitiesTableState({ items: [], status: "empty", message: "Select at least one Opportunity Type View in Preferences." }); return; } if (!dateRange) { setOpportunitiesTableState({ items: [], status: "empty", message: "No opportunities to display for this period." }); return; } try { const token = authSessionState.accessToken || await requestTokenRelayOnce(); if (requestVersion !== opportunitiesQueryState.periodRequestVersion) { return; } await requestOpportunities(token, period, dateRange, requestVersion); } catch (error) { // Authentication and request errors are recorded by their respective request handlers. } } async function requestOpportunities(token, period, dateRange, requestVersion) { if (isCurrentPeriodRequest(requestVersion)) { productsContentState.currentProductsRequestId = ""; productsContentState.opptyProductsByNumber = new Map(); productsContentState.expandedProductOptyNumbers = new Set(); productsContentState.productsProgress = { state: "idle", completed: 0, total: 0, failed: 0, cacheHits: 0 }; consumptionPlansState.accountPlansRequestState = new Map(); consumptionTrendState.consumptionTrendCache = new Map(); consumptionTrendState.consumptionTrendWindowOffsets = new Map(); consumptionTrendState.consumptionTrendSeriesFilters = new Map(); renderProductsProgress(); setOpportunitiesTableState({ items: [], status: "loading", message: "Loading opportunities..." }); } try { if (!isCurrentPeriodRequest(requestVersion)) return; const allItems = await requestOpportunitiesPage(token, period, dateRange, 0, [], requestVersion, 1); if (allItems && isCurrentPeriodRequest(requestVersion)) { setOpportunitiesTableState({ items: allItems, status: "ready", message: "" }); requestOpptyProductsInBackground(allItems, token); } } catch (error) { if (isCurrentPeriodRequest(requestVersion)) { setOpportunitiesTableState({ items: [], status: "error", message: "Unable to load opportunities." }); } throw error; } } function resetOpportunitiesTable() { resetWorkloadActions(); resetCrmServiceRequests(); opportunitiesFilterState.selectedStages = new Set(STAGE_OPTIONS); opportunitiesFilterState.selectedStatuses = new Set(STATUS_OPTIONS); opportunitiesFilterState.selectedCustomerKeys = new Set(); opportunitiesFilterState.customerSearch = ""; opportunitiesFilterState.selectedOwnerKeys = new Set(); opportunitiesFilterState.ownerSearch = ""; opportunitiesFilterState.tableSearch = ""; opportunitiesFilterState.hasSavedCustomerFilter = false; opportunitiesFilterState.hasSavedOwnerFilter = false; opportunitiesFilterState.customerFilterInitialized = false; opportunitiesFilterState.ownerFilterInitialized = false; productsContentState.currentProductsRequestId = ""; productsContentState.opptyProductsByNumber = new Map(); productsContentState.expandedProductOptyNumbers = new Set(); productsContentState.productsProgress = { state: "idle", completed: 0, total: 0, failed: 0, cacheHits: 0 }; productsContentState.productsLastCachedAt = 0; consumptionPlansState.accountPlansRequestState = new Map(); consumptionTrendState.consumptionTrendCache = new Map(); consumptionTrendState.consumptionTrendWindowOffsets = new Map(); consumptionTrendState.consumptionTrendSeriesFilters = new Map(); opportunitiesDashboardState.stageDashboardAmounts = new Map([["TOTAL", 0], ...STAGE_OPTIONS.map((stage) => [stage, 0])]); opportunitiesTableState.opportunitiesTableState = { items: [], status: "idle", message: "", sortKey: "", sortDirection: "ascending" }; } function isCurrentPeriodRequest(requestVersion) { return requestVersion === opportunitiesQueryState.periodRequestVersion; } return { requestOpportunitiesForPeriod, requestOpportunities, resetOpportunitiesTable, isCurrentPeriodRequest }; } // Source: src/services/fusion/opportunities.js function createFusionOpportunities({ OPPORTUNITIES_PAGE_LIMIT, OPPORTUNITIES_QUERY_URL, isCurrentPeriodRequest, logRequestFailure, logRequestStart, logRequestSuccess, maskToken, getOpportunityTypeValues, readResponseBodyForDebug, environment }) { const { fetch } = environment; async function requestOpportunitiesPage(token, period, dateRange, offset, accumulatedItems, requestVersion, page) { if (!isCurrentPeriodRequest(requestVersion)) { return null; } const payload = createOpportunitiesQueryPayload(dateRange, offset); const requestBody = JSON.stringify(payload); const requestEntry = logRequestStart({ name: "requestOpportunities", method: "POST", url: OPPORTUNITIES_QUERY_URL, headers: { Accept: "application/json", Authorization: `Bearer ${maskToken(token)}`, "Content-Type": "application/json", Origin: "https://eeho.fa.us2.oraclecloud.com", Preference: "transient" }, body: JSON.stringify(payload, null, 2), metadata: { period, page, offset, startCloseDate: dateRange.startCloseDate, endCloseDate: dateRange.endCloseDate } }); try { const response = await fetch(OPPORTUNITIES_QUERY_URL, { method: "POST", credentials: "include", headers: { Accept: "application/json", Authorization: `Bearer ${token}`, "Content-Type": "application/json", Origin: "https://eeho.fa.us2.oraclecloud.com", Preference: "transient" }, body: requestBody }); const responseBody = await readResponseBodyForDebug(response); logRequestSuccess(requestEntry, response, responseBody); if (!response.ok) { throw new Error(`Opportunities query failed with status ${response.status}.`); } const responseData = await response.json(); const pageItems = Array.isArray(responseData.items) ? responseData.items : []; const allItems = accumulatedItems.concat(pageItems); if (!responseData.hasMore) { return allItems; } const responseOffset = Number(responseData.offset); const responseCount = Number(responseData.count); const receivedCount = Number.isFinite(responseCount) ? responseCount : pageItems.length; const currentOffset = Number.isFinite(responseOffset) ? responseOffset : offset; if (receivedCount <= 0) { throw new Error("Opportunities query returned hasMore without additional results."); } return requestOpportunitiesPage( token, period, dateRange, currentOffset + receivedCount, allItems, requestVersion, page + 1 ); } catch (error) { logRequestFailure(requestEntry, error.message || "Request failed."); throw error; } } function createOpportunitiesQueryPayload(dateRange, offset) { return { aggregationResults: false, applications: ["ORACLE-ISS-APP"], onlyData: true, entity: "Opportunity", q: { op: "$and", criteria: [ { op: "$or", criteria: Array.from(getOpportunityTypeValues(), (value) => ({ op: "$eq", attribute: "RecordSet", value })) }, { op: "$wi", attribute: "EffectiveDate", value1: dateRange.startCloseDate, value2: dateRange.endCloseDate, dynamicDate: false } ] }, keywords: null, keywordsFields: [ "Name", "OptyNumber", "PrimaryRevenue.WinProb", "CustomerAccount", "PrimaryRevenue.RevnAmount", "EffectiveDate", "StatusCode", "DealRisk_c", "Owner", "ForecastGroup_c", "LastUpdateDate", "PrimaryRevenue.RevnAmountCurcyCode", "OptyId" ], fields: [ "Name", "OptyNumber", "PrimaryRevenue.WinProb", "CustomerAccount", "PrimaryRevenue.RevnAmount", "EffectiveDate", "StatusCode", "DealRisk_c", "Owner", "ForecastGroup_c", "LastUpdateDate", "PrimaryRevenue.RevnAmountCurcyCode", "OptyId", "CustomerAccount.PartyUniqueName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber", "CustomerAccount.Region_c" ], sort: [], language: "en", skipInValidFields: true, skipHiddenFromUIFields: true, copiedFrom: "queries/d5dbcd01-9ff3-40be-9b5b-64744cbf7162", limit: OPPORTUNITIES_PAGE_LIMIT, offset }; } return { requestOpportunitiesPage, createOpportunitiesQueryPayload }; } // Source: src/services/fusion/crm-service-requests.js function createFusionCrmServiceRequests({ isCurrentPeriodRequest, logRequestStart, logRequestSuccess, logRequestFailure, readResponseBodyForDebug, maskToken, environment }) { const { fetch } = environment; const url = "https://eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/latest/custom-actions/queries"; function createPayload(offset) { return { aggregationResults: false, applications: ["ORACLE-ISS-APP"], onlyData: true, entity: "CRMServiceRequest", copiedFrom: "queries/c521309c-1310-49a0-9723-da2eb1988205", q: { op: "$and", criteria: [ { op: "$in", attribute: "StatusCd", values: ["SVC_PENDING_DELIVERY"] }, { op: "$eq", attribute: "RecordSet", value: "ORA_SVC_MY_SR_TEAM_SRS" } ] }, keywords: null, fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "_actions", "Title", "Owner.PersonFirstName", "Owner.PersonLastName", "Owner.PrimaryEmail.EmailAddress", "DoNotCallFlag", "DoNotEmailFlag"], sort: [{ attribute: "_score", direction: "descending" }], language: "en", keywordsFields: ["SrNumber", "ServiceNew_Id_c.RecordName", "CustomerAccount", "OpportunityName_Id_c", "OpportunityName_Id_c.OptyNumber", "Owner"], skipInValidFields: true, limit: 15, offset }; } async function requestCrmServiceRequest(token, version, isActive = () => isCurrentPeriodRequest(version), onProgress = () => {}) { const items = []; let offset = 0; let attempt = 1; while (isActive()) { const payload = createPayload(offset); const headers = { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${token}`, Origin: "https://eeho.fa.us2.oraclecloud.com", Preference: "transient" }; const entry = logRequestStart({ name: "resquestCrmServiceRequest", method: "POST", url, headers: { ...headers, Authorization: `Bearer ${maskToken(token)}` }, body: JSON.stringify(payload, null, 2), metadata: { offset, attempt } }); try { const response = await fetch(url, { method: "POST", credentials: "include", headers, body: JSON.stringify(payload) }); logRequestSuccess(entry, response, await readResponseBodyForDebug(response)); if (response.status !== 200) throw new Error(`CRM service request query failed with status ${response.status}.`); const data = await response.json(); if (!data || !Array.isArray(data.items) || typeof data.hasMore !== "boolean") { throw new Error("CRM service request query returned an invalid response."); } if (!isActive()) return null; const received = data.count === undefined ? data.items.length : Number(data.count); const current = data.offset === undefined ? offset : Number(data.offset); if (data.hasMore && (!data.items.length || !Number.isInteger(received) || received <= 0 || !Number.isInteger(current) || current !== offset || current + received <= offset)) { throw new Error("CRM service request pagination did not advance."); } items.push(...data.items); onProgress(items.length, Number.isFinite(data.totalResults) && data.totalResults >= items.length ? data.totalResults : null); if (!data.hasMore) return items; offset = current + received; attempt = 1; } catch (error) { logRequestFailure(entry, error.message || "CRM service request query failed."); if (attempt === 5) throw error; attempt += 1; } } return null; } return { createPayload, requestCrmServiceRequest }; } // Source: src/features/opportunities/crm-service-requests.js function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, environment }) { let status = "idle"; let byOpportunity = new Map(); let pending = null; let generation = 0; let loadedItems = []; function reset() { closeSrForm(); closePopover(); generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); loadedItems = []; reportProgress("idle"); } function load(token, version, cacheOptions = null) { if (!allowsCrmActivities() || !isCurrentPeriodRequest(version)) return; if (status === "ready") { return Promise.resolve(cacheOptions ? activityCache.write("sr", "all", cacheOptions.cachedAt, loadedItems) : undefined).then(() => true); } if (!pending) { const request = run(token, version, generation, cacheOptions); pending = request; request.finally(() => { if (pending === request) pending = null; }); } return pending; } async function run(token, version, currentGeneration, cacheOptions) { const isActive = () => currentGeneration === generation; status = "loading"; try { const cached = cacheOptions ? await activityCache.read("sr", "all", cacheOptions.cachedAt) : null; if (!isActive()) return false; if (!cached && cacheOptions?.cacheOnly) { status = "idle"; return false; } let items = cached?.items; if (!items) { reportProgress("running", 0); items = await requestCrmServiceRequest(token, version, isActive, (count, total) => { if (isActive()) reportProgress("running", count, total); }); if (items && isActive() && cacheOptions) await activityCache.write("sr", "all", cacheOptions.cachedAt, items); } if (!items || !isActive()) return false; const grouped = new Map(); for (const item of items) { const number = item?.OpportunityName_Id_c?.OptyNumber; if (!number) continue; const key = String(number); if (!grouped.has(key)) grouped.set(key, []); grouped.get(key).push(item); } byOpportunity = grouped; loadedItems = items; status = "ready"; if (!cached) reportProgress("complete", items.length, items.length); return true; } catch { if (isActive()) { status = "error"; reportProgress("error"); } return false; } } function getSummary(optyNumber) { if (!allowsCrmActivities() || !optyNumber) return null; const items = byOpportunity.get(String(optyNumber)) || []; const hours = items.map(item => item.ReportedHours_c).filter(value => (typeof value === "number" || (typeof value === "string" && value.trim() !== "")) && Number.isFinite(Number(value)) ).map(Number); return { state: status === "ready" ? items.length ? "matched" : "missing" : status, reportedHours: hours.length === items.length ? hours.reduce((total, value) => total + value, 0) : null, hasUnderFourHours: status === "ready" && hours.some(value => value < 4) }; } function createBadge(optyNumber) { if (!allowsCrmActivities() || !optyNumber || status === "idle" || status === "loading") return null; const items = byOpportunity.get(String(optyNumber)) || []; const badge = environment.document.createElement(items.length && status === "ready" ? "button" : "span"); badge.className = "opportunities-extension-crm-sr-badge"; badge.setAttribute("data-state", status === "error" ? "error" : items.length ? "matched" : "missing"); const summary = getSummary(optyNumber); const reportedHours = summary.reportedHours !== null ? new Intl.NumberFormat("en-US", { useGrouping: false, maximumFractionDigits: 10 }).format(summary.reportedHours) : "N/D"; setBadgeContent(badge, "sr", status === "error" ? "?" : items.length ? `${reportedHours}${reportedHours === "N/D" ? "" : "h"}` : "0h"); const display = value => value === null || value === undefined || value === "" ? "N/D" : String(value); const descriptions = items.map(item => [ `Title: ${display(item.Title)}`, `Opportunity: ${display(item.OpportunityName_Id_c?.OptyNumber)}`, `Reported Hours: ${display(item.ReportedHours_c)}`, `Owner: ${[item.Owner?.PersonFirstName, item.Owner?.PersonLastName].filter(Boolean).join(" ") || "N/D"}`, `Email: ${display(item.Owner?.PrimaryEmail?.EmailAddress)}`, `Do Not Call: ${display(item.DoNotCallFlag)}`, `Do Not Email: ${display(item.DoNotEmailFlag)}` ].join("\n")); const description = status === "error" ? "Unable to check CRM service requests. See Debug Requests." : !items.length ? "No matching CRM service request with status SVC_PENDING_DELIVERY in your SR team." : items.map((item, index) => `SR Number: ${display(item.SrNumber)}\n${descriptions[index]}`).join("\n\n"); badge.setAttribute("aria-label", description); badge.title = "Whether a Service Request has been created for the opportunities."; if (items.length && status === "ready") bindPopover(badge, items, descriptions); if (!items.length && status === "ready") badge.append(createSrButton(String(optyNumber))); badge.tabIndex = 0; return badge; } return { reset, load, createBadge, getSummary }; } // Source: src/features/opportunities/product-activities.js function createProductActivities({ productsContentState, opportunitiesQueryState, loadCrm, loadActions, blockActions, render }) { let cycle = null; function begin(requestId, token, opportunityNumbers = null) { cycle = { requestId, token, numbers: opportunityNumbers && new Set(opportunityNumbers.map(String)), version: opportunitiesQueryState.periodRequestVersion, completed: false }; } function complete(requestId) { const current = cycle; if (!current || current.completed || current.requestId !== requestId || productsContentState.currentProductsRequestId !== requestId) return; current.completed = true; const entries = [...productsContentState.opptyProductsByNumber].filter(([number]) => !current.numbers || current.numbers.has(String(number))); const products = entries.map(([, result]) => result).filter(result => !result.error); if (!products.length) return; const cachedAt = Math.max(...products.map(result => Number(result.cachedAt) || 0)); const isActive = () => cycle === current && productsContentState.currentProductsRequestId === requestId && opportunitiesQueryState.periodRequestVersion === current.version; // Product cache hits do not guarantee that the corresponding activity cache exists. return Promise.resolve(loadCrm(current.token, current.version, { cachedAt, cacheOnly: false })).then(async success => { if (!isActive()) return; if (success === false) { products.forEach(blockActions); render(); return; } const pending = []; const seen = new Set(); for (const result of products) { const items = result.items.filter(item => { const id = String(item?.RevnId ?? ""); if (seen.has(id)) return false; seen.add(id); return true; }); pending.push(loadActions({ ...result, items }, true, { cachedAt: result.cachedAt, cacheOnly: false })); } await Promise.allSettled(pending); if (!isActive()) return; if (cachedAt > 0) productsContentState.productsLastCachedAt = cachedAt; render(); }).catch(() => { if (isActive()) { products.forEach(blockActions); render(); } }); } return { begin, complete }; } // Source: src/features/opportunities/refresh-all.js function createRefreshAll({ messageTypes, sendRuntimeMessage, activityCache, resetRelatedData, opportunitiesQueryState, productsContentState, consumptionPlansState, consumptionTrendState, aiSummaryState, reload, onFailure, notify, createRefreshIcon, environment }) { let pending = null; function refresh(period) { if (pending) return pending; opportunitiesQueryState.periodRequestVersion += 1; productsContentState.currentProductsRequestId = ""; productsContentState.opptyProductsByNumber = new Map(); productsContentState.expandedProductOptyNumbers = new Set(); productsContentState.productsProgress = { state: "idle", completed: 0, total: 0, failed: 0, cacheHits: 0 }; consumptionPlansState.accountPlansRequestState = new Map(); consumptionTrendState.consumptionTrendCache = new Map(); consumptionTrendState.consumptionTrendWindowOffsets = new Map(); consumptionTrendState.consumptionTrendSeriesFilters = new Map(); aiSummaryState.aiSummariesByOptyNumber = new Map(); resetRelatedData(); pending = (async () => { await activityCache.invalidate(); const response = await sendRuntimeMessage({ type: messageTypes.invalidateProductsCache }); if (!response?.ok) throw new Error(response?.error || "Unable to clear product cache."); await reload(period); })().catch(error => { onFailure(error); throw error; }).finally(() => { pending = null; }); return pending; } function createButton(getPeriod) { const button = environment.document.createElement("button"); button.type = "button"; button.className = "opportunities-extension-icon-button opportunities-extension-refresh-all"; button.append(createRefreshIcon()); button.title = "Refresh all"; button.setAttribute("aria-label", "Refresh all"); button.addEventListener("click", async () => { if (button.disabled) return; button.disabled = true; button.title = "Refreshing..."; button.setAttribute("aria-busy", "true"); try { await refresh(getPeriod()); } catch (error) { notify(error.message || "Unable to refresh cached data.", true); } finally { button.disabled = false; button.title = "Refresh all"; button.setAttribute("aria-busy", "false"); } }); return button; } return { refresh, createButton }; } // Source: src/features/opportunities/activity-cache.js function createActivityCache({ authSessionState, preferencesState, environment }) { const { browser, chrome } = environment; const invalidatedKey = "opportunitiesExtension.activities.invalidatedAt.v1"; async function invalidate() { await storage("set", { [invalidatedKey]: Date.now() }); const entries = await storage("get", null); const keys = Object.keys(entries || {}).filter(key => key.startsWith("opportunitiesExtension.activities.v1:")); if (keys.length) await storage("remove", keys); } function storage(method, argument) { if (browser?.storage?.local) return browser.storage.local[method](argument); if (!chrome?.storage?.local) return Promise.reject(new Error("Extension storage unavailable.")); return new Promise((resolve, reject) => { chrome.storage.local[method](argument, result => { const error = chrome.runtime?.lastError; if (error) reject(new Error(error.message)); else resolve(result); }); }); } function context(kind, id, timestamp) { const principal = authSessionState.principal; const fetchedAt = Number(timestamp); const hours = Number(preferencesState.opptyProductsCacheHours); const ttl = Number.isFinite(hours) && hours > 0 ? Math.min(hours, 720) * 3600000 : 86400000; if (!principal || !Number.isFinite(fetchedAt) || fetchedAt <= 0 || fetchedAt > Date.now() || Date.now() - fetchedAt >= ttl) return null; const identity = [principal, kind, String(id)]; const legacyKey = `opportunitiesExtension.activities.v1:${JSON.stringify(identity)}`; // SR snapshots from different product updates must not overwrite each other. if (kind === "sr") identity.push(fetchedAt); return { key: `opportunitiesExtension.activities.v1:${JSON.stringify(identity)}`, legacyKey, fetchedAt, ttl }; } async function read(kind, id, timestamp) { const scope = context(kind, id, timestamp); if (!scope) return null; try { const invalidated = await storage("get", invalidatedKey); if (scope.fetchedAt <= Number(invalidated?.[invalidatedKey] || 0)) return null; const result = await storage("get", scope.key); let entry = result?.[scope.key]; if (!entry && scope.key !== scope.legacyKey) { const legacy = await storage("get", scope.legacyKey); entry = legacy?.[scope.legacyKey]; } return entry?.fetchedAt === scope.fetchedAt && Array.isArray(entry.items) && entry.items.every(item => item && typeof item === "object" && !Array.isArray(item)) && typeof entry.hasNext === "boolean" ? entry : null; } catch { return null; } } async function write(kind, id, timestamp, items, hasNext = false) { const scope = context(kind, id, timestamp); if (!scope || !Array.isArray(items)) return; try { const invalidated = await storage("get", invalidatedKey); if (scope.fetchedAt <= Number(invalidated?.[invalidatedKey] || 0)) return; await storage("set", { [scope.key]: { fetchedAt: scope.fetchedAt, items, hasNext: Boolean(hasNext) } }); if (kind === "sr") { const entries = await storage("get", null); const prefix = `${scope.legacyKey.slice(0, -1)},`; const expired = Object.keys(entries || {}).filter(key => key.startsWith(prefix) && Number(entries[key]?.fetchedAt) <= Math.max(Date.now() - scope.ttl, Number(invalidated?.[invalidatedKey] || 0))); if (expired.length) await storage("remove", expired); } } catch { /* Storage failure must not discard successfully fetched data. */ } } return { invalidate, read, write }; } // Source: src/features/opportunities/activity-progress.js function createActivityProgress({ MODAL_ID, environment }) { const { document, setTimeout, clearTimeout, matchMedia } = environment; const entries = new Map(); function update(key, state, completed = 0, total = null) { const host = document?.querySelector?.(`#${MODAL_ID} .opportunities-extension-subtitle-row`); if (!host) return; let entry = entries.get(key); if (!entry) { const badge = document.createElement("span"); badge.className = "opportunities-extension-products-progress"; badge.setAttribute("role", "status"); badge.setAttribute("aria-live", "polite"); host.append(badge); entry = { badge, timer: null, hideTimer: null, state: "idle" }; entries.set(key, entry); } if (state === "complete" && entry.state === "complete") return; clearTimeout(entry.timer); clearTimeout(entry.hideTimer); entry.state = state; const label = key === "session" ? "SR session" : key === "sr" ? "Service Requests" : "Actions"; entry.badge.hidden = state === "idle"; entry.badge.setAttribute("data-state", state); entry.badge.setAttribute("data-fading", "false"); entry.badge.setAttribute("aria-busy", String(state === "running")); const count = total === null ? String(completed) : `${completed}/${total}`; entry.badge.textContent = state === "error" ? `${label} unavailable` : state === "complete-with-errors" ? `${label} ${count} (errors)` : state === "complete" ? `${label} ready ${completed}` : `${label} ${count}`; if (key === "session") entry.badge.textContent = state === "complete" ? "SR session ready" : state === "error" ? "SR session unavailable" : "SR session loading"; entry.badge.title = key === "session" ? "Service Request session initialization" : key === "sr" ? "CRM service request records loaded" : "Product workloads checked for Actions in the selected Reference, regardless of display filters"; if (state === "complete") entry.timer = setTimeout(() => { entry.badge.setAttribute("data-fading", "true"); const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches; entry.hideTimer = setTimeout(() => { entry.badge.hidden = true; }, reduced ? 0 : 300); }, 5000); } return { update }; } // Source: src/features/opportunities/activity-filters.js function createActivityFilters({ allowsCrmActivities, getSrSummary, getActionSummary, renderTable, setBadgeContent, environment }) { const selections = { sr: new Set(["matched", "missing"]), actions: new Set(["matched", "missing"]) }; let container = null; let form = null; const groups = []; const buttons = []; function createControls(parentForm, statusFilter, stageFilter) { form = parentForm; groups.length = 0; buttons.length = 0; container = environment.document.createElement("div"); container.className = "opportunities-extension-badge-filters"; for (const [kind, title, options] of [ ["sr", "Service Request", ["matched", "under4", "missing"]], ["actions", "WL Action", ["matched", "missing"]] ]) { const section = environment.document.createElement("section"); section.className = "opportunities-extension-activity-filter"; const label = environment.document.createElement("span"); label.textContent = title; const tooltip = kind === "sr" ? "Whether a Service Request has been created for the opportunities." : "Whether the Action is registered in Workload Workbench."; label.title = tooltip; const controls = environment.document.createElement("div"); controls.className = "opportunities-extension-activity-filter-controls"; controls.setAttribute("role", "group"); controls.setAttribute("aria-label", `Filter by ${title}`); for (const value of options) { const button = environment.document.createElement("button"); button.type = "button"; button.className = "opportunities-extension-activity-filter-button"; button.setAttribute("data-value", value); setBadgeContent(button, kind, value === "under4" ? "< 4h" : ""); const description = value === "under4" ? "At least one SR with reported hours below 4; replaces other SR filters." : `${kind === "sr" ? "Service Request" : "Action"} ${value === "matched" ? "registered" : "not registered"}`; button.title = `${tooltip}\n${description}`; button.setAttribute("aria-label", description); button.addEventListener("click", () => toggle(kind, value)); buttons.push({ button, kind, value }); controls.append(button); } section.append(label, controls); groups.push(section); container.append(section); } container.append(statusFilter, stageFilter); render(); return container; } function render() { if (!container) return; const allowed = allowsCrmActivities(); form.setAttribute("data-activity-filters", String(allowed)); for (const group of groups) group.hidden = !allowed; for (const { button, kind, value } of buttons) { button.setAttribute("aria-pressed", String(selections[kind].has(value))); } } function toggle(kind, value) { const selected = selections[kind]; if (!selected || !allowsCrmActivities()) return; if (selected.has(value)) selected.delete(value); else { if (value === "under4") selected.clear(); else selected.delete("under4"); selected.add(value); } render(); renderTable(); } function matchesSelection(selected, summary) { // Unrestricted filters retain rows whose activities are not known yet. if (selected.has("matched") && selected.has("missing")) return true; if (!summary) return false; if (selected.has("under4")) return summary.state === "matched" && summary.hasUnderFourHours; return selected.has(summary.state); } function matches(item) { if (!allowsCrmActivities()) return true; return matchesSelection(selections.sr, getSrSummary(item.OptyNumber)) && matchesSelection(selections.actions, getActionSummary(item.OptyNumber)); } return { createControls, render, toggle, matches }; } // Source: src/features/opportunities/crm-sr-popover.js function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, requestDetails, environment }) { const { document, window, setTimeout, clearTimeout } = environment; let active = null; let sequence = 0; function close(restoreFocus = false) { if (!active) return; const entry = active; active = null; clearTimeout(entry.timer); entry.panel.remove(); entry.button.setAttribute("aria-expanded", "false"); document.removeEventListener("pointerdown", entry.outside, true); document.removeEventListener("keydown", entry.keydown, true); window.removeEventListener("resize", entry.position); document.removeEventListener("scroll", entry.scroll, true); if (restoreFocus && entry.button.isConnected) { entry.suppressFocus = true; entry.button.focus(); entry.suppressFocus = false; } } function bind(button, items, descriptions) { button.type = "button"; button.setAttribute("aria-haspopup", "dialog"); button.setAttribute("aria-expanded", "false"); const id = `opportunities-extension-sr-details-${++sequence}`; button.setAttribute("aria-controls", id); const entry = { button, panel: null, timer: null, pinned: false, suppressFocus: false }; const contains = target => target && (button.contains(target) || entry.panel?.contains(target)); entry.outside = event => { if (!contains(event.target)) close(); }; entry.scroll = event => { if (!entry.panel.contains(event.target)) close(); }; entry.keydown = event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(true); } }; entry.position = () => { const rect = button.getBoundingClientRect(); const width = Math.min(480, window.innerWidth - 24); entry.panel.style.width = `${width}px`; entry.panel.style.maxHeight = `${Math.min(480, window.innerHeight - 24)}px`; const height = entry.panel.getBoundingClientRect().height; entry.panel.style.left = `${Math.max(12, Math.min(rect.left, window.innerWidth - width - 12))}px`; entry.panel.style.top = `${Math.max(12, Math.min(rect.bottom + 6, window.innerHeight - height - 12))}px`; }; function leave() { clearTimeout(entry.timer); if (!entry.pinned && !contains(document.activeElement)) { entry.timer = setTimeout(() => { if (active === entry) close(); }, 200); } } function focusOut(event) { if (!contains(event.relatedTarget) && active === entry) close(); } function open() { if (entry.suppressFocus) return; clearTimeout(entry.timer); if (active === entry) return; close(); const host = document.getElementById(MODAL_ID); if (!host) return; const panel = document.createElement("div"); entry.panel = panel; entry.pinned = false; panel.id = id; panel.className = "opportunities-extension-sr-popover"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-label", "CRM service requests"); const dismiss = document.createElement("button"); dismiss.type = "button"; dismiss.className = "opportunities-extension-sr-dismiss"; dismiss.title = "Close service request details"; dismiss.setAttribute("aria-label", dismiss.title); dismiss.append(createCloseIcon()); dismiss.addEventListener("click", () => close(true)); panel.append(dismiss); const detailLoads = []; items.forEach((item, index) => { const section = document.createElement("section"); const heading = document.createElement("div"); heading.className = "opportunities-extension-sr-number"; const number = item.SrNumber; const link = document.createElement(number ? "a" : "span"); link.textContent = number ? String(number) : "N/D"; if (number) { link.href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=${encodeURIComponent(String(number))}`; link.target = "_blank"; link.rel = "noopener noreferrer"; } const label = document.createElement("span"); label.textContent = "SR Number:"; heading.append(label, link); if (number) heading.append(createCopyButton(String(number), "SR number", "SR Number")); const details = document.createElement("div"); details.className = "opportunities-extension-sr-details"; details.textContent = descriptions[index]; section.append(heading, details); const children = document.createElement("div"); children.className = "opportunities-extension-sr-children"; children.setAttribute("aria-busy", "true"); children.setAttribute("role", "status"); children.textContent = "Loading SR details..."; section.append(children); detailLoads.push(async () => { try { const records = await requestDetails(item.SrId); if (active !== entry || entry.panel !== panel) return; children.textContent = ""; if (!records.length) children.textContent = "No further details."; for (const record of records) { const child = document.createElement("section"); const row = document.createElement("div"); row.className = "opportunities-extension-sr-number"; const label = document.createElement("span"); label.textContent = "Internal SR Number:"; const link = document.createElement(record.SrNumber ? "a" : "span"); link.textContent = record.SrNumber ? String(record.SrNumber) : "N/D"; if (record.SrNumber) { link.href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=${encodeURIComponent(String(record.SrNumber))}&srId=${encodeURIComponent(String(record.SrId ?? ""))}`; link.target = "_blank"; link.rel = "noopener noreferrer"; } row.append(label, link); if (record.SrNumber) row.append(createCopyButton(String(record.SrNumber), "SR number", "SR Number")); const body = document.createElement("div"); body.className = "opportunities-extension-sr-details"; body.textContent = ["Title", "OpportunityName_c", "StatusCd", "ServiceNew_c", "AccountPartyName", "LastUpdateDate", "SvcHealth_c", "AssigneePersonName", "OpenDate"].map(key => `${key === "ServiceNew_c" ? "Service Queue" : key}: ${record[key] == null || record[key] === "" ? "N/D" : String(record[key])}`).join("\n"); child.append(row, body); children.append(child); } } catch (error) { if (active !== entry || entry.panel !== panel) return; children.setAttribute("role", "alert"); children.textContent = error.message || "Unable to load SR details."; } finally { if (active === entry && entry.panel === panel) { children.setAttribute("aria-busy", "false"); entry.position(); } } }); panel.append(section); }); panel.addEventListener("pointerenter", () => clearTimeout(entry.timer)); panel.addEventListener("pointerleave", leave); panel.addEventListener("focusout", focusOut); host.append(panel); active = entry; detailLoads.forEach(load => load()); button.setAttribute("aria-expanded", "true"); entry.position(); document.addEventListener("pointerdown", entry.outside, true); document.addEventListener("keydown", entry.keydown, true); document.addEventListener("scroll", entry.scroll, true); window.addEventListener("resize", entry.position); } button.addEventListener("pointerenter", open); button.addEventListener("pointerleave", leave); button.addEventListener("focus", open); button.addEventListener("focusout", focusOut); button.addEventListener("click", event => { event.stopPropagation(); if (active === entry && entry.pinned) close(); else { open(); if (active === entry) { entry.pinned = true; entry.panel.querySelector("a, button").focus(); } } }); } return { bind, close }; } // Source: src/features/opportunities/crm-sr-form.js function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportunity, requestSave, notify, formatDate, requestPillars, requestLanguage, requestPlatform, requestServices, createCloseIcon, searchableSelect, environment }) { const { document } = environment; let active = null; function node(tag, className, text) { const element = document.createElement(tag); if (className) element.className = className; if (text !== undefined) element.textContent = text; return element; } function close(restoreFocus = true) { if (!active) return false; if (active.saving) return true; const previous = active; active = null; previous.modal.remove(); if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus(); return true; } function createButton(number) { const button = node("button", "opportunities-extension-add-action", "+"); button.type = "button"; button.title = "Add Service Request"; button.setAttribute("aria-label", `Add Service Request for ${number}`); button.setAttribute("aria-haspopup", "dialog"); button.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); open(number, button); }); return button; } function open(number, trigger) { if (active?.saving) return; const host = document.getElementById(MODAL_ID); if (!host || host.hidden || !allowsCrmActivities() || !isMissing(number)) return; close(false); const modal = node("section", "opportunities-extension-action-form-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "opportunities-extension-sr-form-title"); const dialog = node("div", "opportunities-extension-action-form-dialog opportunities-extension-sr-form-dialog"); const header = node("header", "opportunities-extension-account-plans-header"); const heading = node("div", "opportunities-extension-action-form-heading"); const title = node("h2", "", "Add Service Request"); title.id = "opportunities-extension-sr-form-title"; heading.append(title, node("p", "", String(number))); const opportunity = getOpportunity(number); const details = node("dl", "opportunities-extension-action-form-context"); for (const [label, value] of [ ["Customer", opportunity?.CustomerAccount?.PartyUniqueName], ["Name", opportunity?.Name], ["Close Date", formatDate(opportunity?.EffectiveDate)] ]) { details.append(node("dt", "", label), node("dd", "", value == null || value === "" ? "-" : String(value))); } heading.append(details); const dismiss = node("button", "opportunities-extension-icon-button"); dismiss.type = "button"; dismiss.setAttribute("aria-label", "Close Service Request form"); dismiss.append(createCloseIcon()); dismiss.addEventListener("click", () => close()); header.append(heading, dismiss); const form = node("form", "opportunities-extension-action-form"); form.noValidate = true; const fields = new Map(); const combos = new Map(); const loadingFields = new Set(["primaryPillar", "language", "platform", "services"]); const availableFields = new Set(); let serviceItems = []; const serviceRadios = []; let serviceGroup; const field = node("label", "opportunities-extension-action-form-field"); const select = node("select"); select.id = "opportunities-extension-sr-primary-pillar"; select.name = "primaryPillar"; select.required = true; field.htmlFor = select.id; field.append(node("span", "", "Primary pillar (required)"), select); const autocomplete = searchableSelect(select, field); fields.set("primaryPillar", select); combos.set(select, autocomplete); const status = node("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide"); status.setAttribute("role", "status"); const footer = node("footer", "opportunities-extension-action-form-footer"); const retry = node("button", "", "Retry"); retry.type = "button"; const cancel = node("button", "", "Cancel"); const save = node("button", "opportunities-extension-action-form-save", "Save"); save.type = "submit"; save.disabled = true; cancel.type = "button"; cancel.addEventListener("click", () => close()); footer.append(retry, save, cancel); form.append(field); const extraControls = []; const extraLoads = []; function textField(name, label, type) { const wrapper = node("label", "opportunities-extension-action-form-field" + (type === "textarea" ? " opportunities-extension-action-form-wide" : "")); const input = node(type === "textarea" ? "textarea" : "input"); if (type === "textarea") input.rows = 4; else input.type = type; input.id = `opportunities-extension-sr-${name}`; input.name = name; input.required = true; fields.set(name, input); wrapper.htmlFor = input.id; wrapper.append(node("span", "", `${label} (required)`), input); form.append(wrapper); extraControls.push(input); } textField("customerDeadline", "Customer deadline", "date"); function lookupField(name, label, request, radios = false) { const wrapper = node(radios ? "fieldset" : "div", "opportunities-extension-sr-lookup" + (radios ? " opportunities-extension-action-form-wide" : "")); const content = node("div", radios ? "opportunities-extension-sr-services" : ""); const message = node("p", "opportunities-extension-action-form-status"); const retryOption = node("button", "opportunities-extension-sr-lookup-retry", "Retry"); retryOption.type = "button"; retryOption.setAttribute("aria-label", `Retry ${label}`); retryOption.hidden = true; extraControls.push(retryOption); let input, combo; if (radios) { wrapper.append(node("legend", "", `${label} (required)`)); serviceGroup = wrapper; } else { const field = node("label", "opportunities-extension-action-form-field"); input = node("select"); input.id = `opportunities-extension-sr-${name}`; input.name = name; input.required = true; fields.set(name, input); field.htmlFor = input.id; field.append(node("span", "", `${label} (required)`), input); combo = searchableSelect(input, field); combos.set(input, combo); content.append(field); extraControls.push(combo.input); } wrapper.append(content, message, retryOption); form.append(wrapper); let loading = false; async function loadOptions() { if (loading || !current()) return; loading = true; loadingFields.add(name); availableFields.delete(name); updateSave(); retryOption.hidden = true; message.hidden = false; message.setAttribute("role", "status"); message.textContent = `Loading ${label.toLowerCase()}...`; wrapper.setAttribute("aria-busy", "true"); if (input) { input.disabled = true; input.textContent = ""; combo.sync(); } try { const items = await request(); if (!current()) return; if (items.length) availableFields.add(name); if (input) { const blank = node("option", "", items.length ? `Select ${label.toLowerCase()}` : "No options available"); blank.value = ""; input.append(blank); items.forEach(item => { const option = node("option", "", item.name); option.value = item.code; input.append(option); }); input.value = ""; input.disabled = !items.length; } else { content.textContent = ""; serviceItems = items; serviceRadios.length = 0; const seen = new Set(); items.forEach((item, index) => { if (seen.has(String(item.Id))) return; seen.add(String(item.Id)); const row = node("label", "opportunities-extension-sr-service-option"); const radio = node("input"); radio.type = "radio"; radio.name = "service"; radio.required = true; serviceRadios.push(radio); radio.value = String(item.Id); radio.id = `opportunities-extension-sr-service-${index}`; const copy = node("span"); const abstract = node("span", "opportunities-extension-sr-service-abstract", item.Abstract_c || ""); abstract.id = `${radio.id}-description`; radio.setAttribute("aria-describedby", abstract.id); row.htmlFor = radio.id; copy.append(node("span", "opportunities-extension-sr-service-name", item.RecordName), abstract); row.append(radio, copy); content.append(row); extraControls.push(radio); }); } message.textContent = items.length ? "" : "No options available."; message.hidden = !!items.length; } catch (error) { if (!current()) return; message.setAttribute("role", "alert"); message.textContent = error.message || `Unable to load ${label.toLowerCase()}.`; retryOption.hidden = false; } finally { loading = false; loadingFields.delete(name); if (current()) updateSave(); if (current()) { combo?.sync(); wrapper.setAttribute("aria-busy", "false"); } } } retryOption.addEventListener("click", loadOptions); extraLoads.push(loadOptions); } lookupField("language", "Language of delivery", requestLanguage); lookupField("platform", "Platform", requestPlatform); textField("description", "Description", "textarea"); lookupField("services", "Services", requestServices, true); form.append(status, footer); dialog.append(header, form); modal.append(dialog); host.append(modal); const view = { modal, trigger, loading: false }; active = view; const current = () => active === view && allowsCrmActivities(); function updateSave() { save.disabled = loadingFields.size > 0 || availableFields.size < 4 || Boolean(view.saving); } function confirmPayload(payload, requestHeaders = {}) { return new Promise(resolve => { const overlay = node("section", "opportunities-extension-action-form-modal opportunities-extension-sr-confirmation"); overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-modal", "true"); overlay.setAttribute("aria-labelledby", "opportunities-extension-sr-confirm-title"); const panel = node("div", "opportunities-extension-action-form-dialog opportunities-extension-sr-confirm-dialog"); const heading = node("header", "opportunities-extension-account-plans-header"); const title = node("h2", "", "Confirm Service Request"); title.id = "opportunities-extension-sr-confirm-title"; heading.append(title); const body = node("div", "opportunities-extension-sr-confirm-body"); const label = node("p", "", "requestPostServiceRequest"); label.id = "opportunities-extension-sr-confirm-payload-label"; const preview = node("pre", "opportunities-extension-sr-confirm-payload", JSON.stringify(payload, null, 2)); preview.tabIndex = 0; preview.setAttribute("aria-labelledby", label.id); body.append(label, preview); const headerPreviews = []; for (const name of ["requestSrTokenCatalog", "requestPostServiceRequest"]) { if (!requestHeaders[name]) continue; const headerLabel = node("p", "", `${name} - Headers`); headerLabel.id = `opportunities-extension-sr-confirm-${name}-headers`; const headerPreview = node("pre", "opportunities-extension-sr-confirm-payload", JSON.stringify(requestHeaders[name], null, 2)); headerPreview.tabIndex = 0; headerPreview.setAttribute("aria-labelledby", headerLabel.id); headerPreviews.push(headerPreview); body.append(headerLabel, headerPreview); if (name === "requestSrTokenCatalog" && !requestHeaders[name]["x-appbuilder-client-id"]) { body.append(node("p", "opportunities-extension-action-form-status", "x-appbuilder-client-id: not yet observed in SeaaS requests.")); } } const footer = node("footer", "opportunities-extension-action-form-footer"); const cancel = node("button", "", "Cancel"); const confirm = node("button", "", "Confirm Save"); cancel.type = confirm.type = "button"; footer.append(cancel, confirm); panel.append(heading, body, footer); overlay.append(panel); dialog.inert = true; dialog.setAttribute("aria-hidden", "true"); modal.setAttribute("aria-modal", "false"); modal.append(overlay); let settled = false; function finish(confirmed) { if (settled) return; settled = true; overlay.remove(); dialog.inert = false; dialog.setAttribute("aria-hidden", "false"); modal.setAttribute("aria-modal", "true"); resolve(confirmed); } cancel.addEventListener("click", () => finish(false)); confirm.addEventListener("click", () => finish(true)); overlay.addEventListener("keydown", event => { event.stopPropagation(); if (event.key === "Escape") { event.preventDefault(); finish(false); } if (event.key === "Tab") { const controls = [preview, ...headerPreviews, cancel, confirm]; const index = controls.indexOf(document.activeElement); if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus(); } } }); cancel.focus(); }); } form.addEventListener("submit", async event => { event.preventDefault(); if (!current() || view.saving || save.disabled) return; const invalid = []; const value = name => String(fields.get(name)?.value || "").trim(); for (const [name, input] of fields) { let valid = !!value(name); if (combos.has(input)) valid = valid && Array.from(input.children).some(option => option.value === value(name)); if (name === "customerDeadline") { const date = value(name); valid = /^\d{4}-\d{2}-\d{2}$/.test(date) && Number.isFinite(Date.parse(`${date}T00:00:00Z`)) && new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) === date; } const control = combos.get(input)?.input || input; control.setAttribute("aria-invalid", String(!valid)); if (!valid) invalid.push(control); } const selected = serviceRadios.find(radio => radio.checked); const service = serviceItems.find(item => String(item.Id) === selected?.value); serviceGroup.setAttribute("aria-invalid", String(!service)); if (!service) invalid.push(serviceRadios[0]); if (invalid.length) { status.hidden = false; status.setAttribute("role", "alert"); status.textContent = "Complete the highlighted required fields with valid values."; invalid[0]?.focus(); return; } const payload = { AccountPartyId: opportunity?.CustomerAccount?.PartyId, OpportunityName_Id_c: opportunity?.OptyId, Title: `${service.RecordName}-${number}-${opportunity?.CustomerAccount?.PartyUniqueName || ""}`, RegionText_c: opportunity?.CustomerAccount?.Region_c, CountryText_c: opportunity?.CustomerAccount?.PrimaryAddress?.CountryCode, ServiceNew_Id_c: service.Id, ProblemDescription: value("description"), Language_c: value("language"), CustomerDeadline_c: value("customerDeadline"), TechPlatformLookUp_c: value("platform"), PrimaryPillarNew_c: value("primaryPillar") }; view.saving = true; const controls = [dismiss, retry, cancel, ...fields.values(), ...serviceRadios, ...extraControls]; const disabled = controls.map(control => [control, control.disabled]); controls.forEach(control => { control.disabled = true; }); combos.forEach(combo => combo.sync()); updateSave(); save.textContent = "Preparing..."; form.setAttribute("aria-busy", "true"); status.hidden = true; try { const prepared = await requestSave({ phase: "prepare", payload }); if (!current() || host.hidden || !await confirmPayload(prepared.payload, prepared.headers)) return; save.textContent = "Saving..."; await requestSave({ phase: "confirm", payload: prepared.payload }); view.saving = false; close(); notify("Service Request saved successfully."); } catch (error) { status.hidden = false; status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to save Service Request."; notify(status.textContent, true); } finally { view.saving = false; if (current()) { disabled.forEach(([control, wasDisabled]) => { control.disabled = wasDisabled; }); combos.forEach(combo => combo.sync()); updateSave(); save.textContent = "Save"; form.setAttribute("aria-busy", "false"); save.focus(); } } }); function option(value, text) { const item = node("option", "", text); item.value = value; select.append(item); } async function load() { if (view.loading || !current()) return; view.loading = true; loadingFields.add("primaryPillar"); availableFields.delete("primaryPillar"); updateSave(); retry.hidden = true; select.disabled = true; select.textContent = ""; option("", "Loading..."); autocomplete.sync(); form.setAttribute("data-state", "loading"); form.setAttribute("aria-busy", "true"); status.hidden = false; status.setAttribute("role", "status"); status.textContent = "Loading primary pillars..."; try { const items = await requestPillars(); if (!current()) return; select.textContent = ""; option("", items.length ? "Select primary pillar" : "No options available"); items.forEach(item => option(item.code, item.name)); select.value = ""; select.disabled = !items.length; if (items.length) availableFields.add("primaryPillar"); form.setAttribute("data-state", "ready"); status.hidden = true; } catch (error) { if (!current()) return; form.setAttribute("data-state", "error"); select.textContent = ""; option("", "Unavailable"); status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to load primary pillars."; retry.hidden = false; } finally { view.loading = false; loadingFields.delete("primaryPillar"); if (current()) updateSave(); if (current()) { autocomplete.sync(); form.setAttribute("aria-busy", "false"); } } } retry.addEventListener("click", load); modal.addEventListener("click", event => { if (event.target === modal) close(); }); modal.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); } if (event.key !== "Tab") return; const controls = [dismiss, autocomplete.input, ...extraControls, retry, save, cancel].filter(control => !control.disabled && !control.hidden); const index = controls.indexOf(document.activeElement); if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus(); } }); dismiss.focus(); load(); extraLoads.forEach(loadOptions => loadOptions()); } return { createButton, close, open }; } // Source: src/features/opportunities/searchable-select.js function createSearchableSelect({ environment }) { const { document } = environment; function element(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function searchableSelect(select, field, onChange) { const box = element("div", "opportunities-extension-action-combobox"); const input = element("input"); input.type = "text"; input.autocomplete = "off"; input.required = !!select.required; input.setAttribute("aria-required", String(!!select.required)); select.addEventListener("invalid", event => { event.preventDefault(); input.focus(); }); input.id = `${select.id}-search`; field.htmlFor = input.id; const list = element("div", "opportunities-extension-action-options"); list.id = `${select.id}-options`; list.setAttribute("role", "listbox"); input.setAttribute("role", "combobox"); input.setAttribute("aria-autocomplete", "list"); input.setAttribute("aria-controls", list.id); input.setAttribute("aria-expanded", "false"); select.hidden = true; select.tabIndex = -1; box.append(input, list); field.append(box); let matches = [], index = -1; const options = () => Array.from(select.children).filter(item => item.value); const normalize = value => String(value).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); function closeList() { list.hidden = true; input.setAttribute("aria-expanded", "false"); input.setAttribute("aria-activedescendant", ""); } function sync() { closeList(); input.disabled = !!select.disabled; input.placeholder = select.children[0]?.textContent || "Select"; input.value = options().find(item => item.value === select.value)?.textContent || ""; input.title = input.value; } function highlight() { Array.from(list.children).forEach((node, i) => node.setAttribute("aria-selected", String(i === index))); input.setAttribute("aria-activedescendant", matches[index] ? `${list.id}-${index}` : ""); list.children[index]?.scrollIntoView?.({ block: "nearest" }); } function choose(i) { if (!matches[i]) return; const changed = select.value !== matches[i].value; select.value = matches[i].value; sync(); if (changed) onChange?.(); } function positionList() { const bounds = input.closest?.(".opportunities-extension-action-form-dialog")?.getBoundingClientRect(); const anchor = input.getBoundingClientRect?.(); if (!bounds || !anchor) return; const below = Math.max(0, bounds.bottom - anchor.bottom - 8); const above = Math.max(0, anchor.top - bounds.top - 8); const opensAbove = below < 180 && above > below; list.setAttribute("data-placement", opensAbove ? "above" : "below"); list.style.maxHeight = `${Math.min(180, opensAbove ? above : below)}px`; } function render(query = "") { if (input.disabled) return; matches = options().filter(item => normalize(`${item.textContent} ${item.value}`).includes(normalize(query))); index = -1; list.textContent = ""; matches.forEach((item, i) => { const row = element("div", "opportunities-extension-action-option", item.textContent); row.id = `${list.id}-${i}`; row.setAttribute("role", "option"); row.setAttribute("aria-selected", "false"); row.addEventListener("mousedown", event => event.preventDefault()); row.addEventListener("click", event => { event.preventDefault(); choose(i); }); list.append(row); }); if (!matches.length) { const empty = element("div", "opportunities-extension-action-option", "No results"); empty.setAttribute("role", "status"); list.append(empty); } positionList(); list.hidden = false; input.setAttribute("aria-expanded", "true"); input.setAttribute("aria-activedescendant", ""); } input.addEventListener("click", () => { render(); input.select?.(); }); input.addEventListener("input", () => render(input.value)); input.addEventListener("blur", sync); input.addEventListener("keydown", event => { if (event.key === "ArrowDown" || event.key === "ArrowUp") { event.preventDefault(); if (list.hidden) render(); if (matches.length) index = (index + (event.key === "ArrowDown" ? 1 : index < 0 ? 0 : -1) + matches.length) % matches.length; highlight(); } else if (event.key === "Enter" && !list.hidden) { event.preventDefault(); choose(index < 0 && matches.length === 1 ? 0 : index); } else if (event.key === "Escape" && !list.hidden) { event.preventDefault(); event.stopPropagation(); sync(); } else if (event.key === "Tab") sync(); }); sync(); return { input, sync }; } return searchableSelect; } // Source: src/features/opportunities/internal-sr-session.js function createInternalSrSession({ query, environment, sessionUrl, sessionLabel = "Internal SR" }) { const { document, setTimeout } = environment; let recovery = null; const wait = duration => new Promise(resolve => setTimeout(resolve, duration)); const now = () => environment.performance?.now() ?? Date.now(); function isUnauthorized(result) { const status = result?.httpStatus ?? result?.debugRequests?.at(-1)?.httpStatus; if (status != null) return Number(status) === 401; // Older background workers may return only the request error text. return /failed with status 401\.$/.test(result?.error || ""); } async function warmSession(id) { const frame = document.createElement("iframe"); frame.src = sessionUrl || "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=SR0001421040&srId=302963804324543&view=showisr"; frame.title = `${sessionLabel} session initialization`; frame.tabIndex = -1; frame.setAttribute("aria-hidden", "true"); frame.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms"); frame.style.cssText = "position:fixed;width:1px;height:1px;left:-10000px;top:0;border:0;opacity:0;pointer-events:none;"; let blocked = false; let loaded = false; frame.addEventListener("error", () => { blocked = true; }); frame.addEventListener("load", () => { loaded = true; }); try { if (!document.body) throw new Error(`The current page cannot initialize the ${sessionLabel} session.`); document.body.append(frame); const deadline = now() + 60000; for (let attempt = 0; now() < deadline; attempt += 1) { // Check immediately, then briefly poll faster before backing off. if (attempt > 0) await wait(Math.min(attempt <= 4 ? 250 : 1000, deadline - now())); if (now() >= deadline) break; if (blocked) throw new Error(`The ${sessionLabel} session iframe could not be loaded. Sign in manually and try again.`); const result = await query(id); if (result?.ok) return { id, result }; if (!isUnauthorized(result)) throw new Error(result?.error || `Unable to load ${sessionLabel} data.`); } throw new Error(`${sessionLabel} session timed out after 60 seconds: the API still returned HTTP 401. ${loaded ? "The iframe emitted a load event, but authentication was not confirmed." : "The iframe did not emit a load event."} Sign in manually and try again.`); } finally { // Stop remaining page resources without stopping the caller's page. try { frame.contentWindow?.stop(); } catch { /* Redirects can make the frame cross-origin. */ } frame.src = "about:blank"; frame.remove(); } } async function request(id) { let result = await query(id); for (let attempt = 0; !result?.ok && isUnauthorized(result) && attempt < 2; attempt += 1) { if (!recovery) { recovery = warmSession(id); const pending = recovery; pending.finally(() => { if (recovery === pending) recovery = null; }).catch(() => {}); } const recovered = await recovery; result = recovered.id === id ? recovered.result : await query(id); } if (!result?.ok) throw new Error(result?.error || `Unable to load ${sessionLabel} data.`); return result.items; } return { request }; } // Source: src/features/opportunities/sr-session-preparation.js function createSrSessionPreparation({ ensureCurrentUser, allowsCrmActivities, sendRequest, reportProgress, appendLog }) { let pending = null; function ensure() { if (pending) return pending; pending = (async () => { try { await ensureCurrentUser(); if (!allowsCrmActivities()) return; reportProgress("running"); const result = await sendRequest(); appendLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Session initialization failed."); reportProgress("complete"); } catch { reportProgress("error"); } })(); return pending; } return { ensure }; } // Source: src/features/opportunities/workload-actions.js function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivities, requestConsumerTokenServiceOnce, sendRuntimeMessage, appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, reportProgress, activityCache, createActionButton, closeActionForm, setBadgeContent, environment }) { let session = newSession(); function newSession() { return { enabled: true, records: new Map(), queue: [], running: 0, progress: new Set() }; } function updateProgress(current) { if (current !== session || !current.enabled || !current.progress.size) return; const records = [...current.progress]; const completed = records.filter(record => record.status !== "loading").length; const failed = records.filter(record => record.status === "error").length; reportProgress(completed < records.length ? "running" : failed ? "complete-with-errors" : "complete", completed, records.length); } function reset(enabled = true) { closeActionForm(false); session.enabled = false; for (const job of session.queue) job.record.resolveCompletion(); session.queue.length = 0; session = newSession(); session.enabled = enabled; reportProgress("idle"); } function idsFor(products) { return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))]; } function loadProducts(products, refresh = false, cacheOptions = null) { if (!session.enabled || !allowsCrmActivities()) return; const pending = []; if ([...session.progress].every(record => record.status !== "loading")) session.progress.clear(); for (const revnId of idsFor(products)) { if (!refresh && session.records.has(revnId)) { pending.push(session.records.get(revnId).completion); continue; } session.progress.delete(session.records.get(revnId)); const record = { status: "loading", actions: [], hasNext: false }; record.completion = new Promise(resolve => { record.resolveCompletion = resolve; }); pending.push(record.completion); session.records.set(revnId, record); if (!cacheOptions) session.progress.add(record); session.queue.push({ revnId, record, cacheOptions }); } updateProgress(session); pump(session); return Promise.all(pending); } function reopen() { reset(); for (const products of productsContentState.opptyProductsByNumber.values()) loadProducts(products); queueProductsTableRender(); } function blockProducts(products) { if (!allowsCrmActivities()) return; for (const id of idsFor(products)) { session.records.set(id, { status: "error", actions: [], hasNext: false, error: "CRM service requests unavailable." }); } updateProgress(session); } function pump(current) { while (current.enabled && current === session && current.running < 4 && current.queue.length) { const job = current.queue.shift(); current.running += 1; run(current, job).finally(() => { current.running -= 1; pump(current); }); } } async function run(current, { revnId, record, cacheOptions }) { try { const cached = cacheOptions ? await activityCache.read("actions", revnId, cacheOptions.cachedAt) : null; if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return; if (cached) { record.actions = cached.items; record.hasNext = cached.hasNext; record.status = "ready"; return; } if (cacheOptions?.cacheOnly) { record.status = "error"; record.error = "No matching activity cache for this product update."; return; } current.progress.add(record); updateProgress(current); let result; for (let attempt = 1; attempt <= 5; attempt += 1) { try { if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return; const authorizations = await requestConsumerTokenServiceOnce(); if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return; result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions }); if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return; appendBackgroundRequestLog(result?.debugRequests); if (!result?.ok) throw new Error(result?.error || "Unable to load workload actions."); break; } catch (error) { if (attempt === 5) throw error; } } record.actions = result.actions; record.hasNext = result.hasNext; record.status = "ready"; if (cacheOptions) await activityCache.write("actions", revnId, cacheOptions.cachedAt, result.actions, result.hasNext); } catch (error) { if (!current.enabled || current !== session) return; record.status = "error"; record.error = error.message || "Unable to load workload actions."; } finally { record.resolveCompletion(); if (current.enabled && current === session) { updateProgress(current); queueProductsTableRender(); } } } function getSummary(optyNumber) { if (!session.enabled || !allowsCrmActivities() || !optyNumber) return null; const products = productsContentState.opptyProductsByNumber.get(String(optyNumber)); if (!products) return null; const ids = idsFor(products); const records = ids.map(id => session.records.get(id)); const actions = records.flatMap(record => record?.actions || []); const pending = records.some(record => !record || record.status === "loading"); const uncertain = products.error || (products.items || []).some(item => !String(item?.RevnId ?? "").trim()) || records.some(record => record?.status === "error" || record?.hasNext); const principal = authSessionState.principal; const owned = Boolean(principal?.trim()) && actions.some(action => typeof action.owner === "string" && action.owner.trim().toLowerCase() === principal.trim().toLowerCase()); const state = owned ? "matched" : pending ? "loading" : uncertain || !principal ? "error" : "missing"; return { state, actions, records, principal, pending, uncertain }; } async function applySavedAction(optyNumber, revnId, response, submittedAction = {}) { const products = productsContentState.opptyProductsByNumber.get(String(optyNumber)); const id = String(revnId); if (!session.enabled || !allowsCrmActivities() || !idsFor(products).includes(id)) return false; const returned = Array.isArray(response) ? response : Array.isArray(response?.actions) ? response.actions : [response?.action || response]; const saved = returned.filter(action => action && typeof action === "object" && action.id != null && (action.revenueId == null || String(action.revenueId) === id)); if (!saved.length) return false; const previous = session.records.get(id); const actions = [...(previous?.actions || [])]; for (const action of saved) { // The POST can return a display name in owner, unlike the workload GET. const ownerEmail = typeof action.ownerEmail === "string" && action.ownerEmail.trim() ? action.ownerEmail.trim() : submittedAction.ownerEmail || action.owner || ""; const normalized = { ...action, ownerEmail, owner: ownerEmail }; const index = actions.findIndex(item => String(item.id) === String(action.id)); if (index === -1) actions.push(normalized); else actions[index] = { ...actions[index], ...normalized }; } const record = { status: "ready", actions, hasNext: previous?.hasNext || false, completion: Promise.resolve(), resolveCompletion() {} }; session.records.set(id, record); previous?.resolveCompletion?.(); session.progress.delete(previous); queueProductsTableRender(); if (products.cachedAt) await activityCache.write("actions", id, products.cachedAt, actions, record.hasNext); return true; } function createBadge(optyNumber) { const summary = getSummary(optyNumber); if (!summary) return null; const { state, actions, records, principal, pending, uncertain } = summary; const badge = environment.document.createElement("span"); badge.className = "opportunities-extension-crm-sr-badge opportunities-extension-wl-actions-badge"; badge.setAttribute("data-state", state); setBadgeContent(badge, "actions", state === "matched" || state === "missing" ? actions.length : state === "loading" ? "..." : "?"); badge.title = actions.length ? actions.map(action => [ `Action: ${action.name ?? "N/D"}`, `ID: ${action.id ?? "N/D"}`, `Status: ${action.status ?? "N/D"}`, `Owner: ${action.owner || "N/D"}`, `Email: ${action.ownerEmail || "N/D"}`, `Workload: ${action.workloadName || "N/D"}`, `Description: ${action.workloadDescription || "N/D"}`, `Start: ${action.startDate || "N/D"}`, `End: ${action.endDate || "N/D"}` ].join("\n")).join("\n\n") : state === "missing" ? "No actions found for this opportunity's products." : state === "loading" ? "Loading workload actions..." : "Unable to confirm workload actions. Check product RevnId and Debug Requests."; if (records.some(record => record?.hasNext)) badge.title += "\nAdditional actions exist; pagination is not enabled."; const errors = [...new Set(records.filter(record => record?.error).map(record => record.error))]; if (errors.length) badge.title += `\n${errors.join("\n")}`; if (!principal) badge.title += "\nTokenrelay principal is unavailable; ownership cannot be verified."; else if (actions.length && state === "missing") badge.title += "\nNone of the returned actions belong to the signed-in principal."; if (actions.length && (pending || uncertain)) badge.title += "\nSome workloads are still loading or could not be fully checked."; badge.tabIndex = 0; badge.setAttribute("aria-label", badge.title); if (state === "missing") badge.append(createActionButton(String(optyNumber))); return badge; } return { reset, reopen, loadProducts, blockProducts, createBadge, getSummary, applySavedAction }; } // Source: src/features/opportunities/workload-action-form.js function createWorkloadActionForm({ MODAL_ID, allowsCrmActivities, isMissingAction, getOpportunity, getProducts, requestTeamMembers, requestLookup, getCurrentUserId, saveAction, onActionSaved, notify, formatDate, createCloseIcon, searchableSelect, environment }) { const { document } = environment; let active = null; function element(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function close(restoreFocus = true) { if (!active) return false; if (active.saving) return true; const previous = active; active = null; previous.modal.remove(); if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus(); return true; } function createButton(optyNumber) { const button = element("button", "opportunities-extension-add-action", "+"); button.type = "button"; button.title = "Add Action"; button.setAttribute("aria-label", `Add Action for ${optyNumber}`); button.setAttribute("aria-haspopup", "dialog"); button.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); open(optyNumber, button); }); return button; } function open(optyNumber, trigger) { if (!allowsCrmActivities() || !isMissingAction(optyNumber)) return; const host = document.getElementById(MODAL_ID); if (!host || host.hidden) return; close(false); const modal = element("section", "opportunities-extension-action-form-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "opportunities-extension-action-form-title"); const dialog = element("div", "opportunities-extension-action-form-dialog"); const header = element("header", "opportunities-extension-account-plans-header"); const heading = element("div", "opportunities-extension-action-form-heading"); const title = element("h2", "", "Add Action"); title.id = "opportunities-extension-action-form-title"; heading.append(title, element("p", "", String(optyNumber))); const opportunity = getOpportunity(optyNumber); const details = element("dl", "opportunities-extension-action-form-context"); for (const [label, value] of [ ["Customer", opportunity?.CustomerAccount?.PartyUniqueName], ["Name", opportunity?.Name], ["Close Date", formatDate(opportunity?.EffectiveDate)] ]) { details.append(element("dt", "", label), element("dd", "", value == null || value === "" ? "-" : String(value))); } heading.append(details); const dismiss = element("button", "opportunities-extension-icon-button"); dismiss.type = "button"; dismiss.title = "Close"; dismiss.setAttribute("aria-label", "Close Action form"); dismiss.append(createCloseIcon()); dismiss.addEventListener("click", () => close()); header.append(heading, dismiss); const form = element("form", "opportunities-extension-action-form"); form.noValidate = true; const workloads = [...new Map(getProducts(optyNumber).filter(product => /^\d+$/.test(String(product?.RevnId ?? ""))) .map(product => [String(product.RevnId), product])).entries()]; const workloadField = element("label", "opportunities-extension-action-form-field opportunities-extension-action-form-wide"); const workload = element("select"); workload.id = "opportunities-extension-action-form-workload"; workloadField.htmlFor = workload.id; function option(select, value, label) { const item = element("option", "", label); item.value = value; select.append(item); } option(workload, "", "Select workload"); for (const [id, product] of workloads) option(workload, id, `${product.WorkloadName_c || product.ProdGroupName || "Workload"} (${id})`); workload.value = workloads.length === 1 ? workloads[0][0] : ""; workloadField.hidden = workloads.length <= 1; workloadField.append(element("span", "", "Workload"), workload); const status = element("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide"); status.setAttribute("role", "status"); form.append(workloadField, status); const searchable = new Map(); searchable.set(workload, searchableSelect(workload, workloadField, () => loadOptions())); const fields = []; const selects = []; const lookupKeys = { role: "WWB_ACTION_ROLE", action: "WWB_ACTION_NAME", team: "WWB_ACTION_TEAM", status: "WWB_ACTION_STATUS", complexity: "WWB_ACTION_COMPLEXITY" }; for (const [name, label, type] of [ ["owner", "Owner", "select"], ["role", "Role", "select"], ["action", "Action", "select"], ["team", "Team", "select"], ["startDate", "Start date", "date"], ["endDate", "End Date", "date"], ["status", "Status", "select"], ["complexity", "Complexity", "select"], ["notes", "Notes", "textarea"] ]) { const field = element("label", "opportunities-extension-action-form-field"); const input = element(type === "select" || type === "textarea" ? type : "input"); if (type === "select") selects.push({ input, label, name }); else if (type === "textarea") input.rows = 5; else input.type = type; input.disabled = true; input.required = name !== "notes"; input.setAttribute("aria-required", String(input.required)); input.name = name; input.id = `opportunities-extension-action-form-${name}`; field.htmlFor = input.id; if (["notes", "team", "action"].includes(name)) field.className += " opportunities-extension-action-form-wide"; field.append(element("span", "", `${label} (${input.required ? "required" : "optional"})`), input); if (type === "select") searchable.set(input, searchableSelect(input, field)); form.append(field); fields.push(input); } const footer = element("footer", "opportunities-extension-action-form-footer"); const save = element("button", "opportunities-extension-action-form-save", "Save"); save.type = "submit"; save.disabled = true; save.title = "Save Action"; const retry = element("button", "", "Retry"); retry.type = "button"; retry.hidden = true; const cancel = element("button", "", "Cancel"); cancel.type = "button"; cancel.addEventListener("click", () => close()); footer.append(retry, save, cancel); form.append(footer); dialog.append(header, form); modal.append(dialog); host.append(modal); const view = { modal, trigger, generation: 0, lookup: null, loadingId: null }; active = view; const isOpen = () => active === view && allowsCrmActivities(); let ready = false; function validate() { const invalid = []; const value = name => String(fields.find(field => field.name === name)?.value || "").trim(); for (const field of fields) { let valid = !field.required || !!value(field.name); if (field.required && searchable.has(field)) valid = Array.from(field.children).some(option => option.value && option.value === field.value); if (field.type === "date") { const date = value(field.name); valid = /^\d{4}-\d{2}-\d{2}$/.test(date) && Number.isFinite(Date.parse(`${date}T00:00:00.000Z`)); if (valid) valid = new Date(`${date}T00:00:00.000Z`).toISOString().slice(0, 10) === date; if (field.name === "endDate" && value("startDate") > date) valid = false; } const control = searchable.get(field)?.input || field; control.setAttribute("aria-invalid", String(!valid)); if (!valid) invalid.push(control); } return { invalid, value }; } form.addEventListener("submit", async event => { event.preventDefault(); if (!ready || !isOpen() || view.saving) return; const { invalid, value } = validate(); if (invalid.length) { status.hidden = false; status.setAttribute("role", "alert"); status.textContent = "Complete the highlighted required fields. End date must be on or after start date."; invalid[0].focus(); return; } const userId = getCurrentUserId(); if (!userId) { notify("Unable to save: current user is unavailable.", true); return; } const payload = { parentType: "WORKLOAD", parentId: workload.value, action: { ownerEmail: value("owner"), role: value("role"), name: value("action"), team: value("team"), tags: "", startDate: `${value("startDate")}T00:00:00.000Z`, endDate: `${value("endDate")}T00:00:00.000Z`, status: value("status"), complexity: value("complexity"), notes: value("notes") ? [value("notes")] : [], objective: "", createdBy: userId, updatedBy: userId } }; view.saving = true; save.disabled = cancel.disabled = dismiss.disabled = workload.disabled = true; fields.forEach(field => { field.disabled = true; }); searchable.forEach(control => control.sync()); save.textContent = "Saving..."; form.setAttribute("aria-busy", "true"); status.hidden = true; try { const result = await saveAction(payload, isOpen); if (!result) return; view.saving = false; close(); try { await onActionSaved(optyNumber, payload.parentId, result.result, payload.action); } catch { notify("Action saved, but the local badge could not be updated.", true); return; } notify("Action saved successfully."); } catch (error) { status.hidden = false; status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to save Action."; notify(status.textContent, true); } finally { view.saving = false; if (active === view) { save.disabled = cancel.disabled = dismiss.disabled = workload.disabled = false; fields.forEach(field => { field.disabled = false; }); searchable.forEach(control => control.sync()); save.textContent = "Save"; form.setAttribute("aria-busy", "false"); } } }); const settled = promise => promise.then(result => ({ result }), error => ({ error })); async function loadOptions(refresh = false) { const id = workload.value; if (!isOpen() || (view.loadingId === id && !refresh)) return; const generation = ++view.generation; const isCurrent = () => isOpen() && view.generation === generation; ready = false; save.disabled = true; retry.hidden = true; status.hidden = false; status.setAttribute("role", "status"); fields.forEach(input => { input.disabled = true; }); selects.forEach(({ input }) => { input.textContent = ""; option(input, "", "Loading..."); }); const syncSearch = () => selects.forEach(({ input }) => searchable.get(input).sync()); syncSearch(); if (!workloads.length) { form.setAttribute("data-state", "error"); form.setAttribute("aria-busy", "false"); status.setAttribute("role", "alert"); status.textContent = "No product with a valid RevnId is available for this opportunity."; selects.forEach(({ input }) => { input.textContent = ""; option(input, "", "Unavailable"); }); syncSearch(); return; } if (!view.lookup || refresh) view.lookup = settled(requestLookup(isOpen)); if (!workloads.some(([value]) => value === id)) { view.loadingId = null; form.setAttribute("data-state", "waiting"); form.setAttribute("aria-busy", "false"); status.textContent = "Select a workload to load Action options."; return; } view.loadingId = id; form.setAttribute("data-state", "loading"); form.setAttribute("aria-busy", "true"); status.textContent = "Loading Action options..."; try { const [lookup, team] = await Promise.all([view.lookup, settled(requestTeamMembers(id, isCurrent))]); if (!isCurrent()) return; if (lookup.error || team.error) throw lookup.error || team.error; const values = selects.map(({ name }) => name === "owner" ? team.result.members.map(({ email }) => ({ code: email, name: email })) : lookup.result.lookups[lookupKeys[name]]); const defaults = { owner: getCurrentUserId(), role: "Cloud Architect", action: "Success Plan & Consumption review", status: "Complete" }; const normalize = value => String(value ?? "").trim().toLowerCase(); for (const [index, { input, label, name }] of selects.entries()) { input.textContent = ""; option(input, "", values[index].length ? `Select ${label.toLowerCase()}` : "No options available"); for (const item of values[index]) option(input, item.code, item.name); const match = defaults[name] && values[index].find(item => normalize(name === "owner" ? item.code : item.name) === normalize(defaults[name])); input.value = match ? match.code : ""; } fields.forEach(input => { input.disabled = false; }); selects.forEach(({ input }, index) => { input.disabled = !values[index].length; }); form.setAttribute("data-state", "ready"); ready = true; save.disabled = false; status.hidden = true; syncSearch(); if (document.activeElement === dismiss) { const first = fields.find(field => !field.disabled); (searchable.get(first)?.input || first)?.focus(); } } catch (error) { if (!isCurrent()) return; form.setAttribute("data-state", "error"); status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to load Action options."; selects.forEach(({ input }) => { input.textContent = ""; option(input, "", "Unavailable"); }); syncSearch(); retry.hidden = false; } finally { if (isCurrent()) { view.loadingId = null; form.setAttribute("aria-busy", "false"); } } } retry.addEventListener("click", () => { if (!view.loadingId) loadOptions(true); }); workload.addEventListener("change", () => loadOptions()); modal.addEventListener("click", event => { if (event.target === modal) close(); }); modal.addEventListener("keydown", event => { if (event.key !== "Tab") return; const controls = [dismiss, ...(!workloadField.hidden ? [workload] : []), ...fields, retry, save, cancel] .map(control => searchable.get(control)?.input || control) .filter(control => !control.disabled && !control.hidden); const index = controls.indexOf(document.activeElement); if (!controls.length) { event.preventDefault(); return; } if (index === -1 || (!event.shiftKey && index === controls.length - 1) || (event.shiftKey && index === 0)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus(); } }); if (!workloadField.hidden) searchable.get(workload).input.focus(); else dismiss.focus(); loadOptions(); } return { createButton, open, close }; } // Source: src/features/opportunities/workload-action-options.js function createWorkloadActionOptions({ requestConsumerTokenServiceOnce, sendRuntimeMessage, appendBackgroundRequestLog, messageTypes }) { async function request(type, workloadId, isCurrent, payload) { const authorizations = await requestConsumerTokenServiceOnce(); if (!isCurrent()) return null; const authorization = authorizations?.workloadActions; if (!authorization?.value) throw new Error("WWB-TS uiaasHeader.value is unavailable."); const result = await sendRuntimeMessage({ type, ...(workloadId === undefined ? {} : { workloadId }), ...(payload ? { payload } : {}), authorization }); appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to load Action options."); return result; } return { save: (payload, isCurrent) => request(messageTypes.requestMgmtAction, undefined, isCurrent, payload), requestTeamMembers: (id, isCurrent) => request(messageTypes.requestActionTeamMembers, id, isCurrent), requestLookup: isCurrent => request(messageTypes.requestActionFindLookup, undefined, isCurrent) }; } // Source: src/features/opportunities/fiscal-periods.js function createOpportunitiesFiscalPeriods() { function getFiscalPeriodRange(period, currentDate) { const currentYear = currentDate.getFullYear(); const currentMonth = currentDate.getMonth(); const fiscalStartYear = currentMonth >= 5 ? currentYear : currentYear - 1; const currentQuarterIndex = Math.floor(((currentMonth - 5 + 12) % 12) / 3); if (period === "Current Fiscal Year") { const isBeforeFiscalYearStart = currentMonth < 5; const startYear = isBeforeFiscalYearStart ? currentYear - 1 : currentYear; const endYear = isBeforeFiscalYearStart ? currentYear : currentYear + 1; return createDateRange(startYear, 5, endYear, 4); } let quarterOffset; let numberOfQuarters = 1; if (period === "Current Quarter") { quarterOffset = currentQuarterIndex; } else if (period === "Next Quarter") { quarterOffset = currentQuarterIndex + 1; } else if (period === "Previous Quarter") { quarterOffset = currentQuarterIndex - 1; } else if (period === "4 Rolling Quarters (CQ + 3)") { quarterOffset = currentQuarterIndex; numberOfQuarters = 4; } else { return null; } const start = new Date(fiscalStartYear, 5 + (quarterOffset * 3), 1); const end = new Date(start.getFullYear(), start.getMonth() + (numberOfQuarters * 3), 0); return { startCloseDate: formatOracleDate(start), endCloseDate: formatOracleDate(end) }; } function createDateRange(startYear, startMonth, endYear, endMonth) { return { startCloseDate: formatOracleDate(new Date(startYear, startMonth, 1)), endCloseDate: formatOracleDate(new Date(endYear, endMonth + 1, 0)) }; } function formatOracleDate(date) { 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}T00:00:00.000Z`; } return { getFiscalPeriodRange, createDateRange, formatOracleDate }; } // Source: src/features/opportunities/table.js function createOpportunitiesTable({ ACCOUNT_DETAIL_URL, OPPORTUNITIES_COLUMNS, OPPORTUNITIES_TABLE_ID, OPPORTUNITY_DETAIL_URL, createCloudConsumptionButton, createInstallBaseButton, createServiceRequestsButton, createCopyOpportunityButton, createDetailLink, createProductsCountButton, createProductsRampBadge, createCrmServiceRequestBadge, closeCrmSrPopover, createWorkloadActionsBadge, createProductsDetailRow, createStageBadge, createStatusBadge, displayOpportunityValue, getNestedValue, getSortedOpportunities, opportunitiesTableState, productsContentState, renderCustomerFilter, renderOwnerFilter, renderActivityFilters, renderStageDashboard, resetCustomerFilter, resetOwnerFilter, environment }) { const { document } = environment; function setOpportunitiesTableState(nextState) { if (nextState.status === "ready" && Array.isArray(nextState.items)) { resetCustomerFilter(nextState.items); resetOwnerFilter(nextState.items); } opportunitiesTableState.opportunitiesTableState = { ...opportunitiesTableState.opportunitiesTableState, ...nextState, sortKey: nextState.items ? "" : opportunitiesTableState.opportunitiesTableState.sortKey, sortDirection: nextState.items ? "ascending" : opportunitiesTableState.opportunitiesTableState.sortDirection }; renderOpportunitiesTable(); renderCustomerFilter(); renderOwnerFilter(); } function renderOpportunitiesTable() { renderActivityFilters(); const table = document.getElementById(OPPORTUNITIES_TABLE_ID); if (!table) { return; } closeCrmSrPopover(); table.textContent = ""; table.setAttribute("data-opportunities-layout", "inline-actions"); table.setAttribute("aria-busy", opportunitiesTableState.opportunitiesTableState.status === "loading" ? "true" : "false"); const caption = document.createElement("caption"); caption.className = "opportunities-extension-visually-hidden"; caption.textContent = "Opportunities"; const tableHead = document.createElement("thead"); const headerRow = document.createElement("tr"); OPPORTUNITIES_COLUMNS.forEach((column) => { const header = document.createElement("th"); const isSorted = opportunitiesTableState.opportunitiesTableState.sortKey === column.key; header.scope = "col"; header.setAttribute("data-column", column.key); if (column.sortable === false) { const label = document.createElement("span"); label.className = "opportunities-extension-table-header-label"; label.textContent = column.label; header.append(label); } else { header.setAttribute("aria-sort", isSorted ? opportunitiesTableState.opportunitiesTableState.sortDirection : "none"); const sortButton = document.createElement("button"); sortButton.type = "button"; sortButton.className = "opportunities-extension-sort-button"; sortButton.setAttribute("data-sort-direction", isSorted ? opportunitiesTableState.opportunitiesTableState.sortDirection : "none"); sortButton.setAttribute("aria-label", `Sort by ${column.label}${isSorted ? `, ${opportunitiesTableState.opportunitiesTableState.sortDirection}` : ""}`); const sortLabel = document.createElement("span"); sortLabel.className = "opportunities-extension-sort-label"; sortLabel.textContent = column.label; sortButton.append(sortLabel); sortButton.addEventListener("click", () => sortOpportunitiesBy(column.key)); header.append(sortButton); } headerRow.append(header); }); tableHead.append(headerRow); const tableBody = document.createElement("tbody"); const items = getSortedOpportunities(); renderStageDashboard(items, opportunitiesTableState.opportunitiesTableState.items.length); if (opportunitiesTableState.opportunitiesTableState.status === "loading" || opportunitiesTableState.opportunitiesTableState.status === "error" || opportunitiesTableState.opportunitiesTableState.status === "empty" || (opportunitiesTableState.opportunitiesTableState.status === "ready" && items.length === 0)) { const row = document.createElement("tr"); const cell = document.createElement("td"); cell.className = "opportunities-extension-table-status"; cell.colSpan = OPPORTUNITIES_COLUMNS.length; cell.textContent = opportunitiesTableState.opportunitiesTableState.message || "No opportunities found."; row.append(cell); tableBody.append(row); } else { items.forEach((item) => { const row = document.createElement("tr"); OPPORTUNITIES_COLUMNS.forEach((column) => { const cell = document.createElement("td"); cell.setAttribute("data-column", column.key); if (column.key === "stage") { cell.append(createStageBadge(column.display(item))); } else if (column.key === "status") { cell.append(createStatusBadge(column.display(item))); } else if (column.key === "name" || column.key === "optyNumber") { const opportunityLink = createDetailLink( column.display(item), item.OptyNumber ? `${OPPORTUNITY_DETAIL_URL}${encodeURIComponent(item.OptyNumber)}` : "" ); if (column.key === "name") { const nameCellContent = document.createElement("span"); nameCellContent.className = "opportunities-extension-cell-content"; const productActions = document.createElement("span"); productActions.className = "opportunities-extension-cell-actions"; const productsResult = item.OptyNumber && productsContentState.opptyProductsByNumber.get(String(item.OptyNumber)); if (productsResult) { productActions.append(createProductsCountButton(item.OptyNumber, productsResult)); const rampBadge = createProductsRampBadge(productsResult); if (rampBadge) productActions.append(rampBadge); } const srBadge = createCrmServiceRequestBadge(item.OptyNumber); if (srBadge) productActions.append(srBadge); const actionsBadge = createWorkloadActionsBadge(item.OptyNumber); if (actionsBadge) productActions.append(actionsBadge); nameCellContent.append(opportunityLink, productActions); cell.append(nameCellContent); } else if (column.key === "optyNumber" && item.OptyNumber) { const opportunityCellContent = document.createElement("span"); opportunityCellContent.className = "opportunities-extension-opty-number-content"; opportunityCellContent.append(opportunityLink, createCopyOpportunityButton(item.OptyNumber)); cell.append(opportunityCellContent); } else { cell.append(opportunityLink); } } else if (column.key === "customer") { const customerCellContent = document.createElement("span"); customerCellContent.className = "opportunities-extension-cell-content"; const customerLink = createDetailLink( column.display(item), getNestedValue(item, ["CustomerAccount", "PartyId"]) ? `${ACCOUNT_DETAIL_URL}${encodeURIComponent(getNestedValue(item, ["CustomerAccount", "PartyId"]))}` : "" ); const customerActions = document.createElement("span"); customerActions.className = "opportunities-extension-cell-actions"; customerActions.append(createCloudConsumptionButton(item), createInstallBaseButton(item), createServiceRequestsButton(item)); customerCellContent.append(customerLink, customerActions); cell.append(customerCellContent); } else { cell.textContent = displayOpportunityValue(column, item); } row.append(cell); }); tableBody.append(row); if (item.OptyNumber && productsContentState.expandedProductOptyNumbers.has(String(item.OptyNumber))) { tableBody.append(createProductsDetailRow(item)); } }); } table.append(caption, tableHead, tableBody); } function sortOpportunitiesBy(key) { const isSameColumn = opportunitiesTableState.opportunitiesTableState.sortKey === key; opportunitiesTableState.opportunitiesTableState.sortKey = key; opportunitiesTableState.opportunitiesTableState.sortDirection = isSameColumn && opportunitiesTableState.opportunitiesTableState.sortDirection === "ascending" ? "descending" : "ascending"; renderOpportunitiesTable(); } return { setOpportunitiesTableState, renderOpportunitiesTable, sortOpportunitiesBy }; } // Source: src/features/opportunities/dashboard.js function createOpportunitiesDashboard({ STAGE_DASHBOARD_ID, STAGE_OPTIONS, getNestedValue, opportunitiesDashboardState, environment }) { const { document, requestAnimationFrame, performance } = environment; function renderStageDashboard(items, totalCount = items.length) { const dashboard = document.getElementById(STAGE_DASHBOARD_ID); if (!dashboard) { return; } dashboard.textContent = ""; const totalCard = document.createElement("section"); const totalLabel = document.createElement("span"); const totalValue = document.createElement("strong"); totalCard.className = "opportunities-extension-stage-dashboard-card opportunities-extension-total-dashboard-card"; totalCard.setAttribute("data-stage", "TOTAL"); totalLabel.textContent = "Total Opportunities"; totalValue.className = "opportunities-extension-stage-dashboard-value"; totalValue.setAttribute("aria-label", `${items.length} filtered opportunities out of ${totalCount} total`); animateStageDashboardValue( totalValue, "TOTAL", items.length, (amount) => `${Math.round(amount).toLocaleString()}/${totalCount.toLocaleString()}` ); totalCard.append(totalLabel, totalValue); dashboard.append(totalCard); STAGE_OPTIONS.forEach((stage) => { const stageItems = items.filter((item) => item.ForecastGroup_c === stage); const amount = stageItems.reduce((total, item) => { const value = Number(getNestedValue(item, ["PrimaryRevenue", "RevnAmount"])); return Number.isFinite(value) ? total + value : total; }, 0); const card = document.createElement("section"); const label = document.createElement("span"); const value = document.createElement("strong"); card.className = "opportunities-extension-stage-dashboard-card"; card.setAttribute("data-stage", stage); label.textContent = stage; value.className = "opportunities-extension-stage-dashboard-value"; value.setAttribute("aria-label", `${stage} total amount`); animateStageDashboardValue( value, stage, amount, createStageAmountFormatter(stageItems) ); card.append(label, value); dashboard.append(card); }); } function createStageAmountFormatter(items) { const currencies = new Set(items .map((item) => getNestedValue(item, ["PrimaryRevenue", "RevnAmountCurcyCode"])) .filter(Boolean)); if (currencies.size === 1) { const [currency] = currencies; try { const formatter = new Intl.NumberFormat(undefined, { style: "currency", currency, maximumFractionDigits: 0 }); return (amount) => formatter.format(amount); } catch (error) { // Fall through to a neutral numeric total when the currency code is invalid. } } return (amount) => amount.toLocaleString(undefined, { maximumFractionDigits: 0 }); } function animateStageDashboardValue(element, stage, target, format) { const current = opportunitiesDashboardState.stageDashboardAmounts.get(stage) || 0; opportunitiesDashboardState.stageDashboardAmounts.set(stage, target); if (current === target) { element.textContent = format(target); return; } const startTime = performance.now(); const duration = 480; function tick(now) { const progress = Math.min((now - startTime) / duration, 1); const easedProgress = 1 - Math.pow(1 - progress, 3); const value = current + ((target - current) * easedProgress); element.textContent = format(value); if (progress < 1) { requestAnimationFrame(tick); } else { element.textContent = format(target); } } requestAnimationFrame(tick); } return { renderStageDashboard, createStageAmountFormatter, animateStageDashboardValue }; } // Source: src/features/opportunities/filters.js function createOpportunitiesFilters({ CUSTOMER_FILTER_BUTTON_ID, CUSTOMER_FILTER_LIST_ID, CUSTOMER_FILTER_PANEL_ID, CUSTOMER_FILTER_SEARCH_ID, CUSTOMER_FILTER_SELECT_ALL_ID, OPPORTUNITIES_COLUMNS, OWNER_FILTER_BUTTON_ID, OWNER_FILTER_LIST_ID, OWNER_FILTER_PANEL_ID, OWNER_FILTER_SEARCH_ID, OWNER_FILTER_SELECT_ALL_ID, STAGE_FILTERS_ID, STAGE_OPTIONS, STATUS_FILTERS_ID, STATUS_OPTIONS, getCustomerFilterSummary, getCustomerKey, getCustomerOptions, getOwnerKey, getOwnerOptions, opportunitiesFilterState, opportunitiesTableState, renderOpportunitiesTable, saveFilterPreferences, stringifySearchValue, matchesActivities, environment }) { const { document } = environment; function resetCustomerAndOwnerFiltersForPeriod() { opportunitiesFilterState.selectedCustomerKeys = new Set(); opportunitiesFilterState.selectedOwnerKeys = new Set(); opportunitiesFilterState.customerSearch = ""; opportunitiesFilterState.ownerSearch = ""; opportunitiesFilterState.hasSavedCustomerFilter = false; opportunitiesFilterState.hasSavedOwnerFilter = false; opportunitiesFilterState.customerFilterInitialized = false; opportunitiesFilterState.ownerFilterInitialized = false; } function renderStageFilterButtons() { const controls = document.getElementById(STAGE_FILTERS_ID); if (!controls) { return; } controls.textContent = ""; STAGE_OPTIONS.forEach((stage) => { const button = document.createElement("button"); const isPressed = opportunitiesFilterState.selectedStages.has(stage); button.type = "button"; button.className = "opportunities-extension-stage-filter-button"; button.setAttribute("data-stage", stage); button.setAttribute("aria-pressed", isPressed ? "true" : "false"); button.textContent = stage; button.addEventListener("click", () => toggleStageFilter(stage)); controls.append(button); }); } function toggleStageFilter(stage) { if (opportunitiesFilterState.selectedStages.has(stage)) { opportunitiesFilterState.selectedStages.delete(stage); } else { opportunitiesFilterState.selectedStages.add(stage); } saveFilterPreferences(); renderStageFilterButtons(); renderOpportunitiesTable(); } function renderStatusFilterButtons() { const controls = document.getElementById(STATUS_FILTERS_ID); if (!controls) { return; } controls.textContent = ""; STATUS_OPTIONS.forEach((status) => { const button = document.createElement("button"); const isPressed = opportunitiesFilterState.selectedStatuses.has(status); button.type = "button"; button.className = "opportunities-extension-status-filter-button"; button.setAttribute("data-status", status); button.setAttribute("aria-pressed", isPressed ? "true" : "false"); button.textContent = status; button.addEventListener("click", () => toggleStatusFilter(status)); controls.append(button); }); } function toggleStatusFilter(status) { if (opportunitiesFilterState.selectedStatuses.has(status)) { opportunitiesFilterState.selectedStatuses.delete(status); } else { opportunitiesFilterState.selectedStatuses.add(status); } saveFilterPreferences(); renderStatusFilterButtons(); renderOpportunitiesTable(); } function toggleCustomerFilterPanel() { const panel = document.getElementById(CUSTOMER_FILTER_PANEL_ID); const button = document.getElementById(CUSTOMER_FILTER_BUTTON_ID); if (!panel || !button || button.disabled) { return; } panel.hidden = !panel.hidden; button.setAttribute("aria-expanded", panel.hidden ? "false" : "true"); if (!panel.hidden) { const searchInput = document.getElementById(CUSTOMER_FILTER_SEARCH_ID); searchInput.focus(); } } function closeCustomerFilterPanel(restoreFocus = true) { const panel = document.getElementById(CUSTOMER_FILTER_PANEL_ID); const button = document.getElementById(CUSTOMER_FILTER_BUTTON_ID); if (!panel || panel.hidden) { return false; } panel.hidden = true; button.setAttribute("aria-expanded", "false"); if (restoreFocus) { button.focus(); } return true; } function toggleOwnerFilterPanel() { const panel = document.getElementById(OWNER_FILTER_PANEL_ID); const button = document.getElementById(OWNER_FILTER_BUTTON_ID); if (!panel || !button || button.disabled) { return; } panel.hidden = !panel.hidden; button.setAttribute("aria-expanded", panel.hidden ? "false" : "true"); if (!panel.hidden) { const searchInput = document.getElementById(OWNER_FILTER_SEARCH_ID); searchInput.focus(); } } function closeOwnerFilterPanel(restoreFocus = true) { const panel = document.getElementById(OWNER_FILTER_PANEL_ID); const button = document.getElementById(OWNER_FILTER_BUTTON_ID); if (!panel || panel.hidden) { return false; } panel.hidden = true; button.setAttribute("aria-expanded", "false"); if (restoreFocus) { button.focus(); } return true; } function resetCustomerFilter(items) { const customerOptions = getCustomerOptions(items); if (!opportunitiesFilterState.customerFilterInitialized) { opportunitiesFilterState.selectedCustomerKeys = new Set(customerOptions.map((customer) => customer.key)); opportunitiesFilterState.customerFilterInitialized = true; } } function resetOwnerFilter(items) { const ownerOptions = getOwnerOptions(items); if (!opportunitiesFilterState.ownerFilterInitialized) { opportunitiesFilterState.selectedOwnerKeys = new Set(ownerOptions.map((owner) => owner.key)); opportunitiesFilterState.ownerFilterInitialized = true; } } function renderCustomerFilter() { const trigger = document.getElementById(CUSTOMER_FILTER_BUTTON_ID); const searchInput = document.getElementById(CUSTOMER_FILTER_SEARCH_ID); const selectAllCheckbox = document.getElementById(CUSTOMER_FILTER_SELECT_ALL_ID); const customerOptions = getCustomerOptions(); if (!trigger || !searchInput || !selectAllCheckbox) { return; } const selectedCount = customerOptions.filter((customer) => opportunitiesFilterState.selectedCustomerKeys.has(customer.key)).length; trigger.disabled = customerOptions.length === 0; trigger.textContent = getCustomerFilterSummary(customerOptions.length, selectedCount); searchInput.value = opportunitiesFilterState.customerSearch; selectAllCheckbox.disabled = customerOptions.length === 0; selectAllCheckbox.checked = customerOptions.length > 0 && selectedCount === customerOptions.length; selectAllCheckbox.indeterminate = selectedCount > 0 && selectedCount < customerOptions.length; renderCustomerFilterList(); } function renderCustomerFilterList() { const list = document.getElementById(CUSTOMER_FILTER_LIST_ID); if (!list) { return; } const normalizedSearch = opportunitiesFilterState.customerSearch.trim().toLocaleLowerCase(); const customerOptions = getCustomerOptions().filter((customer) => { return customer.label.toLocaleLowerCase().includes(normalizedSearch); }); list.textContent = ""; if (customerOptions.length === 0) { const empty = document.createElement("p"); empty.className = "opportunities-extension-customer-filter-empty"; empty.textContent = opportunitiesFilterState.customerSearch ? "No matching customers." : "No customers available."; list.append(empty); return; } customerOptions.forEach((customer) => { const option = document.createElement("label"); option.className = "opportunities-extension-customer-filter-option"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = opportunitiesFilterState.selectedCustomerKeys.has(customer.key); checkbox.addEventListener("change", () => { if (checkbox.checked) { opportunitiesFilterState.selectedCustomerKeys.add(customer.key); } else { opportunitiesFilterState.selectedCustomerKeys.delete(customer.key); } opportunitiesFilterState.hasSavedCustomerFilter = true; saveFilterPreferences(); renderCustomerFilter(); renderOpportunitiesTable(); }); const label = document.createElement("span"); label.textContent = customer.label; option.append(checkbox, label); list.append(option); }); } function renderOwnerFilter() { const trigger = document.getElementById(OWNER_FILTER_BUTTON_ID); const searchInput = document.getElementById(OWNER_FILTER_SEARCH_ID); const selectAllCheckbox = document.getElementById(OWNER_FILTER_SELECT_ALL_ID); const ownerOptions = getOwnerOptions(); if (!trigger || !searchInput || !selectAllCheckbox) { return; } const selectedCount = ownerOptions.filter((owner) => opportunitiesFilterState.selectedOwnerKeys.has(owner.key)).length; trigger.disabled = ownerOptions.length === 0; trigger.textContent = getCustomerFilterSummary(ownerOptions.length, selectedCount) .replace("customer", "owner"); searchInput.value = opportunitiesFilterState.ownerSearch; selectAllCheckbox.disabled = ownerOptions.length === 0; selectAllCheckbox.checked = ownerOptions.length > 0 && selectedCount === ownerOptions.length; selectAllCheckbox.indeterminate = selectedCount > 0 && selectedCount < ownerOptions.length; renderOwnerFilterList(); } function renderOwnerFilterList() { const list = document.getElementById(OWNER_FILTER_LIST_ID); if (!list) { return; } const normalizedSearch = opportunitiesFilterState.ownerSearch.trim().toLocaleLowerCase(); const ownerOptions = getOwnerOptions().filter((owner) => { return owner.label.toLocaleLowerCase().includes(normalizedSearch); }); list.textContent = ""; if (ownerOptions.length === 0) { const empty = document.createElement("p"); empty.className = "opportunities-extension-customer-filter-empty"; empty.textContent = opportunitiesFilterState.ownerSearch ? "No matching owners." : "No owners available."; list.append(empty); return; } ownerOptions.forEach((owner) => { const option = document.createElement("label"); option.className = "opportunities-extension-customer-filter-option"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = opportunitiesFilterState.selectedOwnerKeys.has(owner.key); checkbox.addEventListener("change", () => { if (checkbox.checked) { opportunitiesFilterState.selectedOwnerKeys.add(owner.key); } else { opportunitiesFilterState.selectedOwnerKeys.delete(owner.key); } opportunitiesFilterState.hasSavedOwnerFilter = true; saveFilterPreferences(); renderOwnerFilter(); renderOpportunitiesTable(); }); const label = document.createElement("span"); label.textContent = owner.label; option.append(checkbox, label); list.append(option); }); } function getSortedOpportunities() { const normalizedSearch = opportunitiesFilterState.tableSearch.trim().toLocaleLowerCase(); const allStages = STAGE_OPTIONS.every(stage => opportunitiesFilterState.selectedStages.has(stage)); const allCustomers = getCustomerOptions().every(option => opportunitiesFilterState.selectedCustomerKeys.has(option.key)); const allOwners = getOwnerOptions().every(option => opportunitiesFilterState.selectedOwnerKeys.has(option.key)); const items = opportunitiesTableState.opportunitiesTableState.items.filter((item) => { return (allStages || opportunitiesFilterState.selectedStages.has(item.ForecastGroup_c)) && matchesSelectedStatus(item.StatusCode) && (allCustomers || opportunitiesFilterState.selectedCustomerKeys.has(getCustomerKey(item))) && (allOwners || opportunitiesFilterState.selectedOwnerKeys.has(getOwnerKey(item))) && matchesTableSearch(item, normalizedSearch) && matchesActivities(item); }); const column = OPPORTUNITIES_COLUMNS.find((candidate) => candidate.key === opportunitiesTableState.opportunitiesTableState.sortKey); if (!column) { return items; } const direction = opportunitiesTableState.opportunitiesTableState.sortDirection === "ascending" ? 1 : -1; return items.sort((first, second) => { const firstValue = column.value(first); const secondValue = column.value(second); const firstIsEmpty = firstValue === null || firstValue === undefined || firstValue === ""; const secondIsEmpty = secondValue === null || secondValue === undefined || secondValue === ""; if (firstIsEmpty || secondIsEmpty) { if (firstIsEmpty && secondIsEmpty) { return 0; } return firstIsEmpty ? 1 : -1; } const firstNumber = Number(firstValue); const secondNumber = Number(secondValue); if (Number.isFinite(firstNumber) && Number.isFinite(secondNumber)) { return (firstNumber - secondNumber) * direction; } return String(firstValue).localeCompare(String(secondValue), undefined, { numeric: true, sensitivity: "base" }) * direction; }); } function matchesTableSearch(item, normalizedSearch) { if (!normalizedSearch) { return true; } return OPPORTUNITIES_COLUMNS.some((column) => { const rawValue = stringifySearchValue(column.value(item)); const displayValue = stringifySearchValue(column.display(item)); return `${rawValue} ${displayValue}`.toLocaleLowerCase().includes(normalizedSearch); }); } function matchesSelectedStatus(statusCode) { if (STATUS_OPTIONS.every(status => opportunitiesFilterState.selectedStatuses.has(status))) return true; if (statusCode === "WON_PENDING") { return opportunitiesFilterState.selectedStatuses.has("WON"); } return opportunitiesFilterState.selectedStatuses.has(statusCode); } return { resetCustomerAndOwnerFiltersForPeriod, renderStageFilterButtons, toggleStageFilter, renderStatusFilterButtons, toggleStatusFilter, toggleCustomerFilterPanel, closeCustomerFilterPanel, toggleOwnerFilterPanel, closeOwnerFilterPanel, resetCustomerFilter, resetOwnerFilter, renderCustomerFilter, renderCustomerFilterList, renderOwnerFilter, renderOwnerFilterList, getSortedOpportunities, matchesTableSearch, matchesSelectedStatus }; } // Source: src/features/opportunities/formatters.js function createOpportunitiesFormatters({ opportunitiesTableState }) { function getCustomerOptions(items) { const optionsByKey = new Map(); (items || opportunitiesTableState.opportunitiesTableState.items).forEach((item) => { const key = getCustomerKey(item); const label = getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]); if (key && label && !optionsByKey.has(key)) { optionsByKey.set(key, { key, label: String(label) }); } }); return Array.from(optionsByKey.values()).sort((first, second) => { return first.label.localeCompare(second.label, undefined, { sensitivity: "base" }); }); } function getCustomerKey(item) { const partyId = getNestedValue(item, ["CustomerAccount", "PartyId"]); const partyName = getNestedValue(item, ["CustomerAccount", "PartyUniqueName"]); if (partyId !== null && partyId !== undefined && partyId !== "") { return `party:${partyId}`; } return partyName ? `name:${partyName}` : ""; } function getOwnerName(item) { const owner = item.Owner; if (typeof owner === "string" || typeof owner === "number") { return owner; } if (owner && typeof owner === "object") { const firstName = owner.PersonFirstName; const lastName = owner.PersonLastName; if (firstName || lastName) { return [firstName, lastName].filter(Boolean).join(" "); } return owner.PartyName || owner.PartyUniqueName || owner.Name || owner.DisplayName || owner.ResourceName || owner.OwnerName || ""; } return ""; } function getCustomerFilterSummary(total, selected) { if (total === 0) { return "No customers"; } if (selected === total) { return "All customers"; } if (selected === 0) { return "No customers"; } return `${selected} customer${selected === 1 ? "" : "s"}`; } function getOwnerOptions(items) { const optionsByKey = new Map(); (items || opportunitiesTableState.opportunitiesTableState.items).forEach((item) => { const key = getOwnerKey(item); const label = String(getOwnerName(item)).trim() || "-"; if (key && label && !optionsByKey.has(key)) { optionsByKey.set(key, { key, label: String(label) }); } }); return Array.from(optionsByKey.values()).sort((first, second) => { return first.label.localeCompare(second.label, undefined, { sensitivity: "base" }); }); } function getOwnerKey(item) { const owner = item.Owner; const partyId = owner && typeof owner === "object" ? owner.PartyId : ""; const name = String(getOwnerName(item)).trim(); if (!name) return "owner:unassigned"; if (partyId !== null && partyId !== undefined && partyId !== "") { return `party:${partyId}`; } return name ? `name:${name}` : ""; } function stringifySearchValue(value) { if (value === null || value === undefined) { return ""; } if (typeof value === "object") { return JSON.stringify(value); } return String(value); } function displayOpportunityValue(column, item) { const value = column.display(item); return value === null || value === undefined || value === "" ? "-" : String(value); } function getNestedValue(value, path) { return path.reduce((result, key) => result && result[key], value); } function formatWinProbability(item) { const value = getNestedValue(item, ["PrimaryRevenue", "WinProb"]); const number = Number(value); return Number.isFinite(number) ? `${number}%` : value; } function formatRevenue(item) { const amount = getNestedValue(item, ["PrimaryRevenue", "RevnAmount"]); const currency = getNestedValue(item, ["PrimaryRevenue", "RevnAmountCurcyCode"]); const number = Number(amount); if (!Number.isFinite(number)) { return amount; } if (!currency) { return number.toLocaleString(); } try { return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(number); } catch (error) { return `${currency} ${number.toLocaleString()}`; } } function formatOracleResponseDate(value, includeTime) { if (typeof value !== "string") { return value; } const match = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}))?/); if (!match) { return value; } const date = `${match[3]}/${match[2]}/${match[1]}`; return includeTime && match[4] ? `${date} ${match[4]}:${match[5]}` : date; } return { getCustomerOptions, getCustomerKey, getOwnerName, getCustomerFilterSummary, getOwnerOptions, getOwnerKey, stringifySearchValue, displayOpportunityValue, getNestedValue, formatWinProbability, formatRevenue, formatOracleResponseDate }; } // Source: src/features/opportunities/links.js function createOpportunitiesLinks({ MODAL_ID, environment }) { const { document, navigator, window } = environment; function createDetailLink(label, href) { if (label === null || label === undefined || label === "") { const placeholder = document.createElement("span"); placeholder.textContent = "-"; return placeholder; } if (!href) { const text = document.createElement("span"); text.textContent = String(label); return text; } const link = document.createElement("a"); link.href = href; link.target = "_blank"; link.rel = "noopener noreferrer"; link.textContent = String(label); return link; } function createCopyOpportunityButton(optyNumber, label = "opportunity number", confirmationLabel = "Opty Number") { const button = document.createElement("button"); const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); button.type = "button"; button.className = "opportunities-extension-copy-button"; button.setAttribute("aria-label", `Copy ${label} ${optyNumber}`); button.title = `Copy ${label}`; button.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); copyOpportunityNumber(String(optyNumber), label, confirmationLabel); }); icon.setAttribute("viewBox", "0 0 24 24"); icon.setAttribute("aria-hidden", "true"); path.setAttribute("d", "M8 8V5.5A2.5 2.5 0 0 1 10.5 3h8A2.5 2.5 0 0 1 21 5.5v8a2.5 2.5 0 0 1-2.5 2.5H16v2.5a2.5 2.5 0 0 1-2.5 2.5h-8A2.5 2.5 0 0 1 3 18.5v-8A2.5 2.5 0 0 1 5.5 8H8zm2 0h3.5A2.5 2.5 0 0 1 16 10.5v3.5h2.5a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5V8zm3.5 2h-8a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8a.5.5 0 0 0-.5-.5z"); icon.append(path); button.append(icon); return button; } async function copyOpportunityNumber(optyNumber, label = "opportunity number", confirmationLabel = "Opty Number") { try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(optyNumber); } else { const input = document.createElement("textarea"); input.value = optyNumber; input.setAttribute("readonly", "true"); input.style.position = "fixed"; input.style.opacity = "0"; document.body.append(input); input.select(); document.execCommand("copy"); input.remove(); } showCopyConfirmation(`${confirmationLabel} ${optyNumber} copied.`); } catch (error) { showCopyConfirmation(`Unable to copy ${label}.`, true); } } function showCopyConfirmation(message, isError = false) { const modal = document.getElementById(MODAL_ID); if (!modal) { return; } let toast = modal.querySelector(".opportunities-extension-copy-toast"); if (!toast) { toast = document.createElement("div"); toast.className = "opportunities-extension-copy-toast"; toast.setAttribute("role", "status"); modal.append(toast); } toast.classList.toggle("opportunities-extension-copy-toast-error", isError); toast.textContent = message; toast.hidden = false; window.clearTimeout(toast.copyTimeout); toast.copyTimeout = window.setTimeout(() => { toast.hidden = true; }, 2400); } function createStageBadge(stage) { const badge = document.createElement("span"); const normalizedStage = typeof stage === "string" ? stage.toUpperCase() : ""; badge.className = "opportunities-extension-stage-badge"; badge.setAttribute("data-stage", normalizedStage); badge.textContent = normalizedStage || "-"; return badge; } function createStatusBadge(status) { const badge = document.createElement("span"); const normalizedStatus = typeof status === "string" ? status.toUpperCase() : ""; badge.className = "opportunities-extension-status-badge"; badge.setAttribute("data-status", normalizedStatus); badge.textContent = normalizedStatus || "-"; return badge; } return { createDetailLink, createCopyOpportunityButton, copyOpportunityNumber, showCopyConfirmation, createStageBadge, createStatusBadge }; } // Source: src/features/preferences/storage.js function createPreferencesStorage({ DEFAULT_OPPORTUNITY_TYPE_VALUES, DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS, FILTER_PREFERENCES_STORAGE_KEY, MODAL_ID, OPPORTUNITY_TYPE_OPTIONS, OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, OPPTY_PRODUCTS_CACHE_OPTIONS, PERIOD_OPTIONS, STAGE_OPTIONS, STATUS_OPTIONS, opportunitiesFilterState, environment }) { const { document } = environment; const getLocalStorage = () => environment.localStorage; function restoreFilterPreferences(select) { try { const rawPreferences = getLocalStorage().getItem(FILTER_PREFERENCES_STORAGE_KEY); if (!rawPreferences) { return; } const preferences = JSON.parse(rawPreferences); const savedStages = Array.isArray(preferences.stages) ? preferences.stages.filter((stage) => STAGE_OPTIONS.includes(stage)) : null; const savedStatuses = Array.isArray(preferences.statuses) ? preferences.statuses.filter((status) => STATUS_OPTIONS.includes(status)) : null; if (savedStages) { opportunitiesFilterState.selectedStages = new Set(savedStages); } if (savedStatuses) { opportunitiesFilterState.selectedStatuses = new Set(savedStatuses); } if (Array.isArray(preferences.customerKeys)) { opportunitiesFilterState.selectedCustomerKeys = new Set(preferences.customerKeys.map(String)); opportunitiesFilterState.hasSavedCustomerFilter = true; opportunitiesFilterState.customerFilterInitialized = true; } if (Array.isArray(preferences.ownerKeys)) { opportunitiesFilterState.selectedOwnerKeys = new Set(preferences.ownerKeys.map(String)); opportunitiesFilterState.hasSavedOwnerFilter = true; opportunitiesFilterState.ownerFilterInitialized = true; } opportunitiesFilterState.customerSearch = typeof preferences.customerSearch === "string" ? preferences.customerSearch : ""; opportunitiesFilterState.ownerSearch = typeof preferences.ownerSearch === "string" ? preferences.ownerSearch : ""; opportunitiesFilterState.tableSearch = typeof preferences.tableSearch === "string" ? preferences.tableSearch : ""; if (PERIOD_OPTIONS.includes(preferences.period)) { select.value = preferences.period; } } catch (error) { // Ignore malformed or unavailable browser storage and use default filters. } } function saveFilterPreferences(period) { try { const currentPeriod = period || document.querySelector(`#${MODAL_ID} select`)?.value || PERIOD_OPTIONS[0]; getLocalStorage().setItem(FILTER_PREFERENCES_STORAGE_KEY, JSON.stringify({ period: currentPeriod, stages: Array.from(opportunitiesFilterState.selectedStages), statuses: Array.from(opportunitiesFilterState.selectedStatuses), customerKeys: opportunitiesFilterState.hasSavedCustomerFilter ? Array.from(opportunitiesFilterState.selectedCustomerKeys) : null, ownerKeys: opportunitiesFilterState.hasSavedOwnerFilter ? Array.from(opportunitiesFilterState.selectedOwnerKeys) : null, customerSearch: opportunitiesFilterState.customerSearch, ownerSearch: opportunitiesFilterState.ownerSearch, tableSearch: opportunitiesFilterState.tableSearch })); } catch (error) { // Ignore unavailable browser storage; filters still work for the current modal. } } function getSavedOpportunityTypeValues() { try { const rawPreferences = getLocalStorage().getItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY); if (!rawPreferences) { return DEFAULT_OPPORTUNITY_TYPE_VALUES; } const preferences = JSON.parse(rawPreferences); const values = Array.isArray(preferences.opportunityTypeValues) ? preferences.opportunityTypeValues : null; if (!values) { return DEFAULT_OPPORTUNITY_TYPE_VALUES; } const allowedValues = new Set(OPPORTUNITY_TYPE_OPTIONS.map((option) => option.value)); return values.filter((value) => allowedValues.has(value)); } catch (error) { return DEFAULT_OPPORTUNITY_TYPE_VALUES; } } function getSavedOpptyProductsCacheHours() { try { const rawPreferences = getLocalStorage().getItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY); if (!rawPreferences) { return DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS; } const preferences = JSON.parse(rawPreferences); const cacheHours = Number(preferences.opptyProductsCacheHours); return OPPTY_PRODUCTS_CACHE_OPTIONS.some((option) => option.value === cacheHours) ? cacheHours : DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS; } catch (error) { return DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS; } } return { restoreFilterPreferences, saveFilterPreferences, getSavedOpportunityTypeValues, getSavedOpptyProductsCacheHours }; } // Source: src/features/preferences/view.js function createPreferencesView({ DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS, MODAL_ID, OPPORTUNITY_TYPE_OPTIONS, OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, OPPTY_PRODUCTS_CACHE_OPTIONS, PERIOD_OPTIONS, PREFERENCES_MODAL_ID, createCloseIcon, getSavedOpportunityTypeValues, getSavedOpptyProductsCacheHours, preferencesState, requestOpportunitiesForPeriod, environment }) { const { document } = environment; const getLocalStorage = () => environment.localStorage; function openPreferencesModal() { const overlay = document.getElementById(MODAL_ID); if (!overlay) { return; } const existingModal = document.getElementById(PREFERENCES_MODAL_ID); if (existingModal) { const savedValues = new Set(getSavedOpportunityTypeValues()); existingModal.querySelectorAll("input[name='opportunityTypeView']").forEach((checkbox) => { checkbox.checked = savedValues.has(checkbox.value); }); const cacheSelect = existingModal.querySelector("select[name='opptyProductsCacheHours']"); if (cacheSelect) { cacheSelect.value = String(getSavedOpptyProductsCacheHours()); } existingModal.hidden = false; existingModal.querySelector("input")?.focus(); return; } const preferencesModal = document.createElement("section"); preferencesModal.id = PREFERENCES_MODAL_ID; preferencesModal.className = "opportunities-extension-preferences-modal"; preferencesModal.setAttribute("role", "dialog"); preferencesModal.setAttribute("aria-modal", "true"); preferencesModal.setAttribute("aria-labelledby", "opportunities-extension-preferences-title"); const dialog = document.createElement("div"); dialog.className = "opportunities-extension-preferences-dialog"; const header = document.createElement("header"); header.className = "opportunities-extension-preferences-header"; const title = document.createElement("h2"); title.id = "opportunities-extension-preferences-title"; title.textContent = "Preferences"; const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.className = "opportunities-extension-icon-button"; closeButton.setAttribute("aria-label", "Fechar preferências"); closeButton.title = "Close"; closeButton.append(createCloseIcon()); closeButton.addEventListener("click", closePreferencesModal); header.append(title, closeButton); const content = document.createElement("div"); content.className = "opportunities-extension-preferences-content"; const fieldset = document.createElement("fieldset"); fieldset.className = "opportunities-extension-preferences-fieldset"; const legend = document.createElement("legend"); legend.textContent = "Opportunity Type View"; fieldset.append(legend); const selectedValues = new Set(getSavedOpportunityTypeValues()); const options = document.createElement("div"); options.className = "opportunities-extension-preferences-options"; OPPORTUNITY_TYPE_OPTIONS.forEach((option) => { const label = document.createElement("label"); label.className = "opportunities-extension-preferences-option"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.name = "opportunityTypeView"; checkbox.value = option.value; checkbox.checked = selectedValues.has(option.value); const text = document.createElement("span"); text.textContent = option.label; label.append(checkbox, text); options.append(label); }); fieldset.append(options); const cacheField = document.createElement("label"); cacheField.className = "opportunities-extension-preferences-cache-field"; const cacheLabel = document.createElement("span"); cacheLabel.textContent = "Product cache duration"; const cacheSelect = document.createElement("select"); cacheSelect.name = "opptyProductsCacheHours"; cacheSelect.setAttribute("aria-label", "Product cache duration"); OPPTY_PRODUCTS_CACHE_OPTIONS.forEach((option) => { const optionElement = document.createElement("option"); optionElement.value = String(option.value); optionElement.textContent = option.label; optionElement.selected = option.value === getSavedOpptyProductsCacheHours(); cacheSelect.append(optionElement); }); cacheField.append(cacheLabel, cacheSelect); content.append(fieldset, cacheField); const footer = document.createElement("footer"); footer.className = "opportunities-extension-preferences-footer"; const saveButton = document.createElement("button"); saveButton.type = "button"; saveButton.className = "opportunities-extension-button opportunities-extension-button-primary"; saveButton.textContent = "Save"; saveButton.addEventListener("click", () => { const selectedValues = Array.from(preferencesModal.querySelectorAll("input[name='opportunityTypeView']:checked"), (checkbox) => checkbox.value); const selectedCacheHours = Number(preferencesModal.querySelector("select[name='opptyProductsCacheHours']")?.value); preferencesState.selectedOpportunityTypeValues = new Set(selectedValues); preferencesState.opptyProductsCacheHours = OPPTY_PRODUCTS_CACHE_OPTIONS.some((option) => option.value === selectedCacheHours) ? selectedCacheHours : DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS; try { getLocalStorage().setItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, JSON.stringify({ opportunityTypeValues: selectedValues, opptyProductsCacheHours: preferencesState.opptyProductsCacheHours })); } catch (error) { // Continue with the saved selection for the active modal when storage is unavailable. } closePreferencesModal(); const periodSelect = overlay.querySelector("select[name='opportunityPeriod']"); requestOpportunitiesForPeriod(periodSelect?.value || PERIOD_OPTIONS[0]); }); footer.append(saveButton); dialog.append(header, content, footer); preferencesModal.append(dialog); overlay.append(preferencesModal); preferencesModal.querySelector("input")?.focus(); } function closePreferencesModal() { const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID); if (preferencesModal) { preferencesModal.hidden = true; } } return { openPreferencesModal, closePreferencesModal }; } // Source: src/features/products/content-jobs.js function createProductsContentJobs({ messageTypes, PRODUCTS_CACHE_TIMESTAMP_ID, PRODUCTS_MESSAGE_LISTENER_KEY, PRODUCTS_PROGRESS_ID, appendBackgroundRequestLog, preferencesState, productsContentState, renderOpportunitiesTable, beginProductActivities, completeProductActivities, sendRuntimeMessage, environment }) { const { browser, chrome, document, window, setTimeout, clearTimeout, matchMedia } = environment; let completion = null; function updateCompletionVisibility(badge) { const requestId = productsContentState.currentProductsRequestId; const complete = productsContentState.productsProgress.state === "complete"; if (completion && (!complete || completion.requestId !== requestId || completion.badge !== badge)) { clearTimeout(completion.waitTimer); clearTimeout(completion.hideTimer); completion.badge.setAttribute("data-fading", "false"); completion = null; } if (!complete) { badge.setAttribute("data-fading", "false"); return; } if (completion) { badge.hidden = completion.hidden; return; } const current = { requestId, badge, hidden: false, waitTimer: null, hideTimer: null }; completion = current; badge.setAttribute("data-fading", "false"); current.waitTimer = setTimeout(() => { if (completion !== current) return; badge.setAttribute("data-fading", "true"); const reducedMotion = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches; current.hideTimer = setTimeout(() => { if (completion !== current) return; current.hidden = true; badge.hidden = true; }, reducedMotion ? 0 : 300); }, 5000); } function requestOpptyProductsInBackground(opportunities, token) { const optyNumbers = Array.from(new Set(opportunities .map((item) => item && item.OptyNumber) .filter(Boolean) .map(String))); const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`; productsContentState.currentProductsRequestId = requestId; beginProductActivities(requestId, token, optyNumbers); productsContentState.opptyProductsByNumber = new Map(); productsContentState.expandedProductOptyNumbers = new Set(); productsContentState.productsProgress = { state: optyNumbers.length ? "running" : "complete", completed: 0, total: optyNumbers.length, failed: 0, cacheHits: 0 }; renderProductsProgress(); if (optyNumbers.length === 0) { return; } sendRuntimeMessage({ type: messageTypes.requestOpptyProducts, requestId, accessToken: token, optyNumbers, cacheMaxAgeMs: preferencesState.opptyProductsCacheHours * 60 * 60 * 1000 }).then((response) => { if (response && response.ok === false && response.requestId === productsContentState.currentProductsRequestId && productsContentState.productsProgress.state === "running") { productsContentState.productsProgress.state = "error"; productsContentState.productsProgress.failed = productsContentState.productsProgress.total; renderProductsProgress(); } }).catch(() => { if (requestId === productsContentState.currentProductsRequestId && productsContentState.productsProgress.state === "running") { productsContentState.productsProgress.state = "error"; productsContentState.productsProgress.failed = productsContentState.productsProgress.total; renderProductsProgress(); } }); } function ensureProductsBackgroundMessageHandler() { if (window[PRODUCTS_MESSAGE_LISTENER_KEY]) { return; } const runtimeApi = typeof browser !== "undefined" ? browser : chrome; if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.onMessage) { return; } window[PRODUCTS_MESSAGE_LISTENER_KEY] = true; runtimeApi.runtime.onMessage.addListener((message) => { if (!message || message.requestId !== productsContentState.currentProductsRequestId) { return false; } if (message.type === messageTypes.opptyProductsRequestDebug) { if (message.entry && typeof message.entry.debugKey === "string") appendBackgroundRequestLog([message.entry]); return false; } if (message.type !== messageTypes.opptyProductsProgress) { return false; } if (message.optyNumber) { productsContentState.opptyProductsByNumber.set(String(message.optyNumber), { items: Array.isArray(message.items) ? message.items : [], fromCache: Boolean(message.fromCache), cachedAt: Number(message.cachedAt) || 0, error: message.error || "" }); } productsContentState.productsProgress = { state: message.state || productsContentState.productsProgress.state, completed: Number(message.completed) || 0, total: Number(message.total) || productsContentState.productsProgress.total, failed: Number(message.failed) || 0, cacheHits: Number(message.cacheHits) || 0 }; renderProductsProgress(); if (message.state === "complete" || message.state === "complete-with-errors") completeProductActivities(message.requestId); queueProductsTableRender(); return false; }); } function renderProductsProgress() { const badge = document.getElementById(PRODUCTS_PROGRESS_ID); if (!badge) { return; } badge.hidden = productsContentState.productsProgress.state === "idle"; badge.setAttribute("data-state", productsContentState.productsProgress.state); if (productsContentState.productsProgress.state === "running") { badge.textContent = `Products ${productsContentState.productsProgress.completed}/${productsContentState.productsProgress.total}`; badge.title = `${productsContentState.productsProgress.cacheHits} loaded from cache`; } else if (productsContentState.productsProgress.state === "complete") { badge.textContent = `Products ready ${productsContentState.productsProgress.total}`; badge.title = `${productsContentState.productsProgress.cacheHits} loaded from cache`; } else if (productsContentState.productsProgress.state === "complete-with-errors") { badge.textContent = `Products ${productsContentState.productsProgress.total - productsContentState.productsProgress.failed}/${productsContentState.productsProgress.total}`; badge.title = `${productsContentState.productsProgress.failed} product requests failed`; } else if (productsContentState.productsProgress.state === "error") { badge.textContent = "Products unavailable"; badge.title = "Unable to load opportunity products"; } else { badge.textContent = "Products ready 0"; } updateCompletionVisibility(badge); const timestamp = document.getElementById(PRODUCTS_CACHE_TIMESTAMP_ID); if (timestamp) { timestamp.hidden = !productsContentState.productsLastCachedAt; timestamp.textContent = productsContentState.productsLastCachedAt ? `Last update: ${formatProductsCacheTimestamp(productsContentState.productsLastCachedAt)}` : ""; timestamp.title = productsContentState.productsLastCachedAt ? `Latest product update: ${formatProductsCacheTimestamp(productsContentState.productsLastCachedAt)}` : ""; } } function formatProductsCacheTimestamp(timestamp) { try { return new Intl.DateTimeFormat("en-GB", { dateStyle: "short", timeStyle: "short" }).format(new Date(timestamp)); } catch (error) { return new Date(timestamp).toLocaleString(); } } function queueProductsTableRender() { if (productsContentState.productsRenderQueued) { return; } productsContentState.productsRenderQueued = true; window.requestAnimationFrame(() => { productsContentState.productsRenderQueued = false; renderOpportunitiesTable(); }); } return { requestOpptyProductsInBackground, ensureProductsBackgroundMessageHandler, renderProductsProgress, formatProductsCacheTimestamp, queueProductsTableRender }; } // Source: src/features/products/details.js function createProductsDetails({ OPPORTUNITIES_COLUMNS, setBadgeContent, createAiSummaryAlert, formatOracleResponseDate, productsContentState, renderOpportunitiesTable, requestGenerateAiSummary, environment }) { const { document } = environment; function createProductsRampBadge(productsResult) { const values = (Array.isArray(productsResult.items) ? productsResult.items : []) .map(item => item && item.RampMonths_c) .map(value => value === null || value === undefined || String(value).trim() === "" ? "N/D" : String(value).trim()) .filter((value, index, items) => value !== "N/D" || items.indexOf(value) === index); if (!values.length) return null; const badge = document.createElement("span"); badge.className = "opportunities-extension-products-ramp"; badge.setAttribute("data-missing", String(values.includes("N/D"))); setBadgeContent(badge, "ramp", values.join(" | ")); badge.title = `Ramp Months: ${values.join(", ")}`; badge.setAttribute("aria-label", badge.title); return badge; } function createProductsCountButton(optyNumber, productsResult) { const normalizedOptyNumber = String(optyNumber); const button = document.createElement("button"); const count = Array.isArray(productsResult.items) ? productsResult.items.length : 0; const isExpanded = productsContentState.expandedProductOptyNumbers.has(normalizedOptyNumber); button.type = "button"; button.className = "opportunities-extension-products-count"; setBadgeContent(button, "products", count); button.setAttribute("aria-expanded", String(isExpanded)); button.setAttribute("aria-label", `${count} product${count === 1 ? "" : "s"} for opportunity ${normalizedOptyNumber}`); button.title = productsResult.error ? productsResult.error : `${count} product${count === 1 ? "" : "s"}`; button.classList.toggle("opportunities-extension-products-count-error", Boolean(productsResult.error)); button.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); if (isExpanded) { productsContentState.expandedProductOptyNumbers.delete(normalizedOptyNumber); } else { productsContentState.expandedProductOptyNumbers.add(normalizedOptyNumber); requestGenerateAiSummary(normalizedOptyNumber); } renderOpportunitiesTable(); }); return button; } function createProductsDetailRow(opportunity) { const row = document.createElement("tr"); const cell = document.createElement("td"); const optyNumber = String(opportunity.OptyNumber); const productsResult = productsContentState.opptyProductsByNumber.get(optyNumber); const products = productsResult && Array.isArray(productsResult.items) ? productsResult.items : []; row.className = "opportunities-extension-products-detail-row"; cell.colSpan = OPPORTUNITIES_COLUMNS.length; cell.className = "opportunities-extension-products-detail-cell"; const panel = document.createElement("section"); panel.className = "opportunities-extension-products-detail"; const heading = document.createElement("div"); heading.className = "opportunities-extension-products-detail-heading"; const title = document.createElement("strong"); title.textContent = `Products for ${optyNumber}`; const summary = document.createElement("span"); summary.textContent = productsResult && productsResult.fromCache ? "Cached" : "Updated"; heading.append(title, summary); panel.append(heading); panel.append(createAiSummaryAlert(optyNumber)); if (productsResult && productsResult.error) { const error = document.createElement("p"); error.className = "opportunities-extension-products-empty"; error.textContent = productsResult.error; panel.append(error); } else if (products.length === 0) { const empty = document.createElement("p"); empty.className = "opportunities-extension-products-empty"; empty.textContent = "No products found for this opportunity."; panel.append(empty); } else { const table = document.createElement("table"); table.className = "opportunities-extension-products-table"; table.setAttribute("aria-label", `Products for opportunity ${optyNumber}`); const columns = [ { label: "Product Group", value: (item) => item.ProdGroupName || "-" }, { label: "Workload", value: (item) => item.WorkloadName_c || "-" }, { label: "Currency", value: (item) => item.RevnAmountCurcyCode || "-" }, { label: "Amount", value: (item) => formatProductAmount(item.RevnAmount, item.RevnAmountCurcyCode) }, { label: "Type", value: (item) => item.TypeCode || "-" }, { label: "Status", value: (item) => item.StatusCode || "-" }, { label: "Win Probability", value: (item) => formatProductWinProbability(item.WinProb) }, { label: "Close date", value: (item) => formatOracleResponseDate(item.EffectiveDate) || "-" }, { label: "Consumption Start", value: (item) => formatOracleResponseDate(item.ConsumptionStartDate_c) || "-" }, { label: "Ramp Months", value: (item) => item.RampMonths_c === null || item.RampMonths_c === undefined || String(item.RampMonths_c).trim() === "" ? "N/D" : formatProductNumber(item.RampMonths_c) } ]; const head = document.createElement("thead"); const headerRow = document.createElement("tr"); columns.forEach((column) => { const header = document.createElement("th"); header.scope = "col"; header.textContent = column.label; headerRow.append(header); }); head.append(headerRow); const body = document.createElement("tbody"); products.forEach((product) => { const productRow = document.createElement("tr"); columns.forEach((column) => { const productCell = document.createElement("td"); productCell.textContent = String(column.value(product)); productRow.append(productCell); }); body.append(productRow); }); table.append(head, body); panel.append(table); } cell.append(panel); row.append(cell); return row; } function formatProductNumber(value) { const number = Number(value); return Number.isFinite(number) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(number) : "-"; } function formatProductAmount(value, currency) { const amount = Number(value); if (!Number.isFinite(amount)) { return "-"; } try { return new Intl.NumberFormat("en-US", { style: "currency", currency: currency || "USD" }).format(amount); } catch (error) { return `${currency || ""} ${formatProductNumber(amount)}`.trim(); } } function formatProductWinProbability(value) { const number = Number(value); return Number.isFinite(number) ? `${number}%` : "-"; } return { createProductsRampBadge, createProductsCountButton, createProductsDetailRow, formatProductNumber, formatProductAmount, formatProductWinProbability }; } // Source: src/features/ai-summary/content.js function createAiSummaryContent({ aiSummaryState, invalidateSalesCommonToken, logRequestFailure, logRequestStart, logRequestSuccess, readResponseBodyForDebug, requestTokenRelaySalesCommon, environment }) { const { document, fetch } = environment; function requestGenerateAiSummary(optyNumber) { const key = String(optyNumber); const existing = aiSummaryState.aiSummariesByOptyNumber.get(key); if (existing && existing.status === "loading") { return existing.promise; } const state = { status: "loading", summaryText: "", error: "", promise: null }; aiSummaryState.aiSummariesByOptyNumber.set(key, state); state.promise = (async () => { let entry; try { let token = await requestTokenRelaySalesCommon(); if (!token) { throw new Error("Authentication is required to generate the summary."); } const path = "/api/sales-common/gen-ai/v1/summarizations/generate"; const url = `https://eeho.fa.us2.oraclecloud.com${path}`; const payload = { path, promptCode: "cx.sales.generate_opportunity_summary", object: "opportunities", puid: key }; let response; for (let attempt = 0; attempt < 2; attempt += 1) { const headers = { Accept: "application/json", Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; entry = logRequestStart({ name: "requestGenerateAiSummary", method: "POST", url, headers, body: JSON.stringify(payload, null, 2), metadata: { optyNumber: key, attempt: attempt + 1 } }); response = await fetch(url, { method: "POST", credentials: "include", headers, body: JSON.stringify(payload) }); logRequestSuccess(entry, response, await readResponseBodyForDebug(response)); if (response.status !== 401 && response.status !== 403) { break; } // A late failure for an old token must not discard a newly refreshed token. invalidateSalesCommonToken(token); if (attempt === 0) { token = await requestTokenRelaySalesCommon(); } } if (!response.ok) { throw new Error(`requestGenerateAiSummary failed with status ${response.status}.`); } let data; try { data = await response.json(); } catch (error) { throw new Error("requestGenerateAiSummary returned an invalid JSON response."); } if (!data || typeof data.summaryText !== "string" || !data.summaryText.trim()) { throw new Error("requestGenerateAiSummary returned an empty summary."); } state.summaryText = data.summaryText; state.status = "ready"; } catch (error) { state.status = "error"; state.error = error.message || "Unable to generate the AI summary."; if (entry) { logRequestFailure(entry, state.error); } } finally { // Update only visible summaries, without reopening rows or rebuilding the table. document.querySelectorAll(".opportunities-extension-ai-summary").forEach((alert) => { if (alert.dataset.optyNumber === key) { alert.replaceWith(createAiSummaryAlert(key)); } }); } })(); return state.promise; } function createAiSummaryAlert(optyNumber) { const state = aiSummaryState.aiSummariesByOptyNumber.get(optyNumber); const alert = document.createElement("div"); alert.className = "opportunities-extension-ai-summary"; alert.dataset.optyNumber = optyNumber; alert.setAttribute("role", "status"); alert.setAttribute("aria-live", "polite"); alert.setAttribute("aria-busy", String(!state || state.status === "loading")); const title = document.createElement("strong"); title.textContent = "Summary generated by AI"; const description = document.createElement("p"); description.textContent = !state || state.status === "loading" ? "Generating summary..." : state.status === "error" ? state.error : state.summaryText; alert.classList.toggle("opportunities-extension-ai-summary-error", Boolean(state && state.status === "error")); alert.append(title, description); return alert; } return { requestGenerateAiSummary, createAiSummaryAlert }; } // Source: src/features/consumption/plans-action.js function createConsumptionPlansAction({ messageTypes, appendBackgroundRequestLog, consumptionPlansState, getNestedValue, openAccountPlansModal, renderOpportunitiesTable, requestConsumerTokenServiceOnce, sendRuntimeMessage, showCopyConfirmation, environment }) { const { document } = environment; function createCloudConsumptionButton(opportunity) { const button = document.createElement("button"); const optyNumber = String(opportunity.OptyNumber || ""); const partyNumber = getNestedValue(opportunity, ["CustomerAccount", "PartyNumber"]); const requestState = consumptionPlansState.accountPlansRequestState.get(optyNumber); const isLoading = requestState && requestState.status === "loading"; button.type = "button"; button.className = "opportunities-extension-action-button"; button.setAttribute("aria-label", `View cloud consumption for ${optyNumber || "opportunity"}`); button.title = isLoading ? "Loading customer consumption" : "View customer consumption"; button.disabled = Boolean(isLoading); button.classList.toggle("opportunities-extension-action-button-loading", Boolean(isLoading)); button.append(createCloudConsumptionIcon()); button.addEventListener("click", () => { if (!partyNumber) { showCopyConfirmation("Customer Party Number is unavailable.", true); return; } const currentRequests = consumptionPlansState.accountPlansRequestState; currentRequests.set(optyNumber, { status: "loading" }); openAccountPlansModal(opportunity, [], "loading"); renderOpportunitiesTable(); requestConsumerTokenServiceOnce() .then((authorizations) => sendRuntimeMessage({ type: messageTypes.requestAccountPlans, partyNumber: String(partyNumber), authorizations })) .then((response) => { if (consumptionPlansState.accountPlansRequestState !== currentRequests) return; appendBackgroundRequestLog(response && response.debugRequests); if (!response || !response.ok) { throw new Error(response && response.error ? response.error : "Unable to load cloud consumption."); } consumptionPlansState.accountPlansRequestState.set(optyNumber, { status: "ready", plans: Array.isArray(response.plans) ? response.plans : [] }); renderOpportunitiesTable(); openAccountPlansModal(opportunity, response.plans || [], "ready"); }).catch((error) => { if (consumptionPlansState.accountPlansRequestState !== currentRequests) return; const errorMessage = error.message || "Unable to load cloud consumption."; consumptionPlansState.accountPlansRequestState.set(optyNumber, { status: "error", error: errorMessage }); renderOpportunitiesTable(); openAccountPlansModal(opportunity, [], "error", errorMessage); showCopyConfirmation(errorMessage, true); }); }); return button; } function createCloudConsumptionIcon() { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const cloud = document.createElementNS("http://www.w3.org/2000/svg", "path"); const usage = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); cloud.setAttribute("d", "M7.2 18.5h10.1a4.2 4.2 0 0 0 .5-8.37A6.25 6.25 0 0 0 5.9 8.75a4.9 4.9 0 0 0 1.3 9.75Z"); usage.setAttribute("d", "M9 15v-2m3 2v-4m3 4V9"); [cloud, usage].forEach((path) => { path.setAttribute("fill", "none"); path.setAttribute("stroke", "currentColor"); path.setAttribute("stroke-linecap", "round"); path.setAttribute("stroke-linejoin", "round"); path.setAttribute("stroke-width", "1.7"); }); svg.append(cloud, usage); return svg; } return { createCloudConsumptionButton, createCloudConsumptionIcon }; } // Source: src/features/install-base/action.js function createInstallBaseAction({ messageTypes, requestConsumerTokenServiceOnce, sendRuntimeMessage, appendBackgroundRequestLog, openInstallBaseModal, environment }) { const { document } = environment; const pending = new Map(); async function load(partyNumber) { const authorizations = await requestConsumerTokenServiceOnce(); const response = await sendRuntimeMessage({ type: messageTypes.requestInstallBase, partyNumber, authorizations: { accountSummary: authorizations.accountSummary, installBase: authorizations.installBase } }); appendBackgroundRequestLog(response?.debugRequests); if (!response?.ok) throw new Error(response?.error || "Unable to load installed base."); return response.items; } function createInstallBaseButton(opportunity) { const button = document.createElement("button"); button.type = "button"; button.className = "opportunities-extension-action-button"; button.title = "View customer installed base"; button.setAttribute("aria-label", `View installed base for ${opportunity.CustomerAccount?.PartyUniqueName || "customer"}`); const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); icon.setAttribute("viewBox", "0 0 24 24"); icon.setAttribute("aria-hidden", "true"); icon.setAttribute("focusable", "false"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", "M4 3h16v7H4z M4 14h16v7H4z M7 6.5h.01 M7 17.5h.01 M11 6.5h6 M11 17.5h6 M8 10v4 M16 10v4"); path.setAttribute("fill", "none"); path.setAttribute("stroke", "currentColor"); path.setAttribute("stroke-width", "1.7"); path.setAttribute("stroke-linecap", "round"); path.setAttribute("stroke-linejoin", "round"); icon.append(path); button.append(icon); button.addEventListener("click", async () => { const view = openInstallBaseModal(opportunity, button); const partyNumber = String(opportunity.CustomerAccount?.PartyNumber || ""); try { if (!partyNumber) throw new Error("Customer Party Number is unavailable."); if (!pending.has(partyNumber)) { const request = load(partyNumber).finally(() => { if (pending.get(partyNumber) === request) pending.delete(partyNumber); }); pending.set(partyNumber, request); } view.render(await pending.get(partyNumber)); } catch (error) { view.render([], error.message || "Unable to load installed base."); } }); return button; } return { createInstallBaseButton, resetRequests: () => pending.clear() }; } // Source: src/features/install-base/view.js function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, createSearch, environment }) { const { document } = environment; let active = null; const columns = [ ["partyName", "Customer"], ["productGroupName", "Product Group"], ["productName", "Product"], ["currentSupport", "Current Support", "amount"], ["orderNumber", "Order Number"], ["supportStatus", "Support Status", "status"], ["quantityType", "Quantity Type"], ["quantity", "Quantity", "quantity"], ["supportStartDate", "Support Start Date", "date"], ["supportEndDate", "Support End Date", "date"], ["licenseValue", "License Value", "amount"], ["shipDate", "Ship Date", "date"] ]; function element(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function closeInstallBaseModal() { if (!active) return false; const previous = active; active = null; previous.modal.remove(); if (previous.trigger?.isConnected) previous.trigger.focus(); return true; } function formatValue(value, type) { if (value === null || value === undefined || value === "") return "-"; if (type === "date") return formatOracleResponseDate(value); if ((type === "amount" || type === "quantity") && Number.isFinite(Number(value))) { return new Intl.NumberFormat("en-US", { minimumFractionDigits: type === "amount" ? 2 : 0, maximumFractionDigits: type === "amount" ? 2 : 4 }).format(Number(value)); } return String(value).trim(); } function createTable(items, loading = false) { const table = element("table", "opportunities-extension-install-base-table"); table.setAttribute("aria-label", "Installed base items"); const head = element("thead"); const header = element("tr"); const group = element("colgroup"); columns.forEach(([key, label]) => { const col = element("col"); col.className = `install-base-col-${key}`; group.append(col); const th = element("th", "", label); th.scope = "col"; header.append(th); }); head.append(header); const body = element("tbody"); (loading ? Array.from({ length: 8 }, () => ({})) : items).forEach(item => { const row = element("tr"); columns.forEach(([key, , type]) => { const cell = element("td"); if (loading) { const bar = element("span", "opportunities-extension-account-plans-skeleton-bar"); bar.setAttribute("aria-hidden", "true"); cell.append(bar); } else { const value = item[key]; const text = formatValue(value, type); if (type === "status") { const badge = element("span", "opportunities-extension-install-base-status", text); badge.dataset.status = text.toUpperCase(); cell.append(badge); } else cell.textContent = text; if (type) cell.dataset.format = type; } row.append(cell); }); body.append(row); }); table.append(group, head, body); return table; } function openInstallBaseModal(opportunity, trigger) { closeInstallBaseModal(); const overlay = document.getElementById(MODAL_ID); if (!overlay || overlay.hidden) return { render() {} }; const modal = element("section", "opportunities-extension-account-plans-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-busy", "true"); modal.setAttribute("aria-labelledby", "opportunities-extension-install-base-title"); const dialog = element("div", "opportunities-extension-account-plans-dialog opportunities-extension-install-base-dialog"); const header = element("header", "opportunities-extension-account-plans-header"); const heading = element("div"); const title = element("h2", "", "Installed Base"); title.id = "opportunities-extension-install-base-title"; heading.append(title, element("p", "", opportunity.CustomerAccount?.PartyUniqueName || "Customer")); const close = element("button", "opportunities-extension-icon-button"); close.type = "button"; close.title = "Close"; close.setAttribute("aria-label", "Close installed base"); close.append(createCloseIcon()); close.addEventListener("click", closeInstallBaseModal); header.append(heading, close); const content = element("div", "opportunities-extension-install-base-content"); content.tabIndex = 0; content.setAttribute("role", "region"); content.setAttribute("aria-label", "Installed base results"); const status = element("p", "opportunities-extension-install-base-message", "Loading installed base..."); status.setAttribute("role", "status"); let loadedItems = [], loaded = false, failure = ""; const search = createSearch({ label: "Search installed base", onSearch: renderResults, getValues: item => columns.flatMap(([key, , type]) => [item[key], formatValue(item[key], type)]) }); const toolbar = element("div", "opportunities-extension-customer-table-toolbar"); toolbar.append(search.input); content.append(status, createTable([], true)); dialog.append(header, toolbar, content); modal.append(dialog); overlay.append(modal); active = { modal, trigger }; modal.addEventListener("keydown", event => { if (event.key === "Tab") { event.preventDefault(); const controls = [close, search.input, content]; const index = controls.indexOf(document.activeElement); controls[(index + (event.shiftKey ? -1 : 1) + controls.length) % controls.length].focus(); } }); close.focus(); function renderResults() { if (!loaded || active?.modal !== modal || overlay.hidden) return; const items = failure ? [] : search.filter(loadedItems); content.textContent = ""; status.textContent = failure || (loadedItems.length ? `${items.length} of ${loadedItems.length} installed base items${items.length ? "" : ". No matching records."}` : "No installed base items found for this customer."); status.setAttribute("role", failure ? "alert" : "status"); content.append(status); if (!failure && items.length) content.append(createTable(items)); content.scrollTop = 0; } return { render(items, error = "") { // A closed or replaced dialog must never receive a late response. if (active?.modal !== modal || overlay.hidden) return; modal.setAttribute("aria-busy", "false"); loadedItems = items; failure = error; loaded = true; search.reset(); renderResults(); } }; } return { openInstallBaseModal, closeInstallBaseModal, createTable }; } // Source: src/features/opportunities/customer-table-search.js function createCustomerTableSearch({ environment }) { function createSearch({ label, onSearch, getValues }) { const input = environment.document.createElement("input"); input.type = "search"; input.className = "opportunities-extension-customer-table-search"; input.placeholder = label; input.setAttribute("aria-label", label); input.addEventListener("input", onSearch); const normalize = value => String(value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); let index = new WeakMap(); return { input, reset() { index = new WeakMap(); }, filter(items) { const terms = normalize(input.value).trim().split(/\s+/).filter(Boolean); if (!terms.length) return items; return items.filter(item => { if (!index.has(item)) index.set(item, getValues(item).map(normalize)); return terms.every(term => index.get(item).some(value => value.includes(term))); }); } }; } return { createSearch }; } // Source: src/features/service-requests/action.js function createServiceRequestsAction({ messageTypes, requestConsumerTokenServiceOnce, sendRuntimeMessage, appendBackgroundRequestLog, openServiceRequestsModal, environment }) { const { document } = environment; const pending = new Map(); async function load(partyNumber) { const authorizations = await requestConsumerTokenServiceOnce(); const response = await sendRuntimeMessage({ type: messageTypes.requestCustomerServiceRequest, partyNumber, authorizations: { accountSummary: authorizations.accountSummary, customerServiceRequests: authorizations.customerServiceRequests } }); appendBackgroundRequestLog(response?.debugRequests); if (!response?.ok) throw new Error(response?.error || "Unable to load service requests."); return response.items; } function createServiceRequestsButton(opportunity) { const button = document.createElement("button"); button.type = "button"; button.className = "opportunities-extension-action-button"; button.title = "View customer service requests"; button.setAttribute("aria-label", `View service requests for ${opportunity.CustomerAccount?.PartyUniqueName || "customer"}`); const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); icon.setAttribute("viewBox", "0 0 24 24"); icon.setAttribute("aria-hidden", "true"); icon.setAttribute("focusable", "false"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", "M4 14v-3a8 8 0 0 1 16 0v3 M4 11H3v7h4v-7H4 M20 11h1v7h-4v-7h3 M20 18v1a2 2 0 0 1-2 2h-6"); path.setAttribute("fill", "none"); path.setAttribute("stroke", "currentColor"); path.setAttribute("stroke-width", "1.7"); path.setAttribute("stroke-linecap", "round"); path.setAttribute("stroke-linejoin", "round"); icon.append(path); button.append(icon); button.addEventListener("click", async () => { const view = openServiceRequestsModal(opportunity, button); const partyNumber = String(opportunity.CustomerAccount?.PartyNumber || ""); try { if (!partyNumber) throw new Error("Customer Party Number is unavailable."); if (!pending.has(partyNumber)) { const request = load(partyNumber).finally(() => { if (pending.get(partyNumber) === request) pending.delete(partyNumber); }); pending.set(partyNumber, request); } view.render(await pending.get(partyNumber)); } catch (error) { view.render([], error.message || "Unable to load service requests."); } }); return button; } return { createServiceRequestsButton, resetRequests: () => pending.clear() }; } // Source: src/features/service-requests/view.js function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, createSearch, environment }) { const { document } = environment; let active = null; const columns = [ ["name", "Name", item => item.customer?.name], ["registryId", "Registry ID", item => item.customer?.registryId], ["id", "SR Number", item => item.id], ["severity", "Severity", item => item.severity], ["status", "Status", item => item.status], ["subStatus", "Substatus", item => item.subStatus], ["productName", "Product Name", item => item.component?.productName], ["os", "OS", item => item.os], ["csi", "CSI", item => item.csi], ["created", "Creation date", item => item.created], ["lastUpdated", "Modified", item => item.lastUpdated] ]; function element(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function closeServiceRequestsModal() { if (!active) return false; const previous = active; active = null; previous.modal.remove(); if (previous.trigger?.isConnected) previous.trigger.focus(); return true; } function formatValue(value, key) { const text = value === null || value === undefined || value === "" ? "-" : String(value); return text !== "-" && (key === "created" || key === "lastUpdated") ? formatOracleResponseDate(value) : text; } function createTable(items, loading = false) { const table = element("table", "opportunities-extension-service-requests-table"); table.setAttribute("aria-label", "Customer service requests"); const group = element("colgroup"); const head = element("thead"); const header = element("tr"); columns.forEach(([key, label]) => { group.append(element("col", `service-requests-col-${key}`)); const th = element("th", "", label); th.scope = "col"; header.append(th); }); head.append(header); const body = element("tbody"); (loading ? Array.from({ length: 8 }, () => ({})) : items).forEach(item => { const row = element("tr"); columns.forEach(([key, , getValue]) => { const cell = element("td"); cell.dataset.field = key; if (loading) { const bar = element("span", "opportunities-extension-account-plans-skeleton-bar"); bar.setAttribute("aria-hidden", "true"); cell.append(bar); } else { const value = getValue(item); const text = formatValue(value, key); if (key === "status") { const badge = element("span", "opportunities-extension-service-request-status", text); badge.dataset.status = text.trim().toUpperCase(); cell.append(badge); } else cell.textContent = text; } row.append(cell); }); body.append(row); }); table.append(group, head, body); return table; } function openServiceRequestsModal(opportunity, trigger) { closeServiceRequestsModal(); const overlay = document.getElementById(MODAL_ID); if (!overlay || overlay.hidden) return { render() {} }; const modal = element("section", "opportunities-extension-account-plans-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-busy", "true"); modal.setAttribute("aria-labelledby", "opportunities-extension-service-requests-title"); const dialog = element("div", "opportunities-extension-account-plans-dialog opportunities-extension-service-requests-dialog"); const header = element("header", "opportunities-extension-account-plans-header"); const heading = element("div"); const title = element("h2", "", "Service Requests"); title.id = "opportunities-extension-service-requests-title"; heading.append(title, element("p", "", opportunity.CustomerAccount?.PartyUniqueName || "Customer")); const close = element("button", "opportunities-extension-icon-button"); close.type = "button"; close.title = "Close"; close.setAttribute("aria-label", "Close service requests"); close.append(createCloseIcon()); close.addEventListener("click", closeServiceRequestsModal); header.append(heading, close); const content = element("div", "opportunities-extension-service-requests-content"); content.tabIndex = 0; content.setAttribute("role", "region"); content.setAttribute("aria-label", "Service requests results"); const status = element("p", "opportunities-extension-service-requests-message", "Loading service requests..."); status.setAttribute("role", "status"); let loadedItems = [], loaded = false, failure = ""; const search = createSearch({ label: "Search service requests", onSearch: renderResults, getValues: item => columns.flatMap(([key, , getValue]) => [getValue(item), formatValue(getValue(item), key)]) }); const toolbar = element("div", "opportunities-extension-customer-table-toolbar"); toolbar.append(search.input); content.append(status, createTable([], true)); dialog.append(header, toolbar, content); modal.append(dialog); overlay.append(modal); active = { modal, trigger }; modal.addEventListener("keydown", event => { if (event.key === "Tab") { event.preventDefault(); const controls = [close, search.input, content]; const index = controls.indexOf(document.activeElement); controls[(index + (event.shiftKey ? -1 : 1) + controls.length) % controls.length].focus(); } }); close.focus(); function renderResults() { if (!loaded || active?.modal !== modal || overlay.hidden) return; const items = failure ? [] : search.filter(loadedItems); content.textContent = ""; status.textContent = failure || (loadedItems.length ? `${items.length} of ${loadedItems.length} service requests${items.length ? "" : ". No matching records."}` : "No open service requests found for this customer."); status.setAttribute("role", failure ? "alert" : "status"); content.append(status); if (!failure && items.length) content.append(createTable(items)); content.scrollTop = 0; } return { render(items, error = "") { if (active?.modal !== modal || overlay.hidden) return; modal.setAttribute("aria-busy", "false"); loadedItems = items; failure = error; loaded = true; search.reset(); renderResults(); } }; } return { openServiceRequestsModal, closeServiceRequestsModal, createTable }; } // Source: src/features/consumption/plans-view.js function createConsumptionPlansView({ ACCOUNT_PLANS_MODAL_ID, DEFAULT_CONSUMPTION_TREND_PERIOD, MODAL_ID, createConsumptionHeader, createConsumptionTrendSection, loadPlanDetailForPanel, closePlanDetailPopover, createRatecardButton, closeRatecardDrawer, createSvgElement, createPlanDetails, getNestedValue, loadConsumptionTrendForPanel, environment }) { const { document, requestAnimationFrame, setTimeout, window } = environment; function openAccountPlansModal(opportunity, plans, state = "ready", errorMessage = "") { const overlay = document.getElementById(MODAL_ID); const optyNumber = String(opportunity.OptyNumber || ""); const existingModal = document.getElementById(ACCOUNT_PLANS_MODAL_ID); if (!overlay) { return; } if (state !== "loading" && ( !existingModal || existingModal.hidden || existingModal.dataset.optyNumber !== optyNumber )) { return; } closePlanDetailPopover(); closeRatecardDrawer({ immediate: true, restoreFocus: false }); existingModal?.remove(); const modal = document.createElement("section"); modal.id = ACCOUNT_PLANS_MODAL_ID; modal.className = "opportunities-extension-account-plans-modal"; modal.dataset.optyNumber = optyNumber; modal.dataset.customerLabel = getNestedValue(opportunity, ["CustomerAccount", "PartyUniqueName"]) || opportunity.Name || opportunity.OptyNumber || "Opportunity"; modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "opportunities-extension-account-plans-title"); modal.setAttribute("aria-busy", String(state === "loading")); const dialog = document.createElement("div"); dialog.className = "opportunities-extension-account-plans-dialog"; dialog.setAttribute("data-consumption-layout", "compact"); const header = createConsumptionHeader({ titleId: "opportunities-extension-account-plans-title", customerLabel: modal.dataset.customerLabel, closeLabel: "Close cloud consumption", onClose: closeAccountPlansModal }); const content = document.createElement("div"); content.className = "opportunities-extension-account-plans-content"; if (state === "loading") { content.append(createAccountPlansSkeleton()); } else if (state === "error") { const error = document.createElement("p"); error.className = "opportunities-extension-account-plans-error"; error.setAttribute("role", "alert"); error.textContent = errorMessage || "Unable to load cloud consumption."; content.append(error); } else if (!Array.isArray(plans) || plans.length === 0) { const empty = document.createElement("p"); empty.className = "opportunities-extension-account-plans-empty"; empty.textContent = "No account plans found for this customer."; content.append(empty); } else { const tabList = document.createElement("div"); tabList.className = "opportunities-extension-account-plans-tabs"; tabList.setAttribute("role", "tablist"); tabList.setAttribute("aria-label", "Account plans"); const tabsNavigation = createAccountPlansTabsNavigation(tabList); const panels = document.createElement("div"); panels.className = "opportunities-extension-account-plans-panels"; plans.forEach((plan, index) => { const tabId = `opportunities-extension-account-plan-tab-${index}`; const panelId = `opportunities-extension-account-plan-panel-${index}`; const tab = document.createElement("button"); tab.id = tabId; tab.type = "button"; tab.className = "opportunities-extension-account-plan-tab"; tab.setAttribute("role", "tab"); tab.setAttribute("aria-controls", panelId); tab.setAttribute("aria-selected", String(index === 0)); tab.tabIndex = index === 0 ? 0 : -1; const planLabel = [plan.planType, plan.subPlanNum] .filter((value) => value !== null && value !== undefined && value !== "") .join(" ") || `Plan ${index + 1}`; const headerStatus = plan.hdrStatusCode === null || plan.hdrStatusCode === undefined || plan.hdrStatusCode === "" ? "Unknown" : String(plan.hdrStatusCode); tab.setAttribute("aria-label", `${planLabel}. Header Status: ${headerStatus}`); tab.append(createAccountPlanStatusIndicator(headerStatus), document.createTextNode(planLabel)); const panel = document.createElement("section"); panel.id = panelId; panel.className = "opportunities-extension-account-plan-panel"; panel.setAttribute("role", "tabpanel"); panel.setAttribute("aria-labelledby", tabId); panel.hidden = index !== 0; panel.append(createAccountPlanDetails(plan), createConsumptionTrendSection(plan)); tab.addEventListener("click", () => { closePlanDetailPopover(); closeRatecardDrawer({ immediate: true, restoreFocus: false }); tabList.querySelectorAll("[role='tab']").forEach((candidate) => { const isSelected = candidate === tab; candidate.setAttribute("aria-selected", String(isSelected)); candidate.tabIndex = isSelected ? 0 : -1; }); panels.querySelectorAll("[role='tabpanel']").forEach((candidate) => { candidate.hidden = candidate !== panel; }); const trendSection = panel.querySelector(".opportunities-extension-consumption-trend"); loadConsumptionTrendForPanel( panel, plan, trendSection ? trendSection.dataset.period : DEFAULT_CONSUMPTION_TREND_PERIOD ); loadPlanDetailForPanel(panel, plan); }); tabList.append(tab); panels.append(panel); }); content.append(tabsNavigation, panels); } dialog.append(header, content); modal.append(dialog); overlay.append(modal); modal.querySelector("[role='tab'], .opportunities-extension-icon-button")?.focus(); if (state === "ready" && Array.isArray(plans) && plans.length > 0) { const firstPanel = modal.querySelector("[role='tabpanel']"); if (firstPanel) { loadConsumptionTrendForPanel(firstPanel, plans[0], DEFAULT_CONSUMPTION_TREND_PERIOD); loadPlanDetailForPanel(firstPanel, plans[0]); } } } function createAccountPlansTabsNavigation(tabList) { const navigation = document.createElement("div"); navigation.className = "opportunities-extension-account-plans-tabs-navigation"; const previousButton = createAccountPlansTabNavigationButton("previous"); const nextButton = createAccountPlansTabNavigationButton("next"); const updateNavigation = () => { const maximumScroll = Math.max(0, tabList.scrollWidth - tabList.clientWidth); const tabs = Array.from(tabList.querySelectorAll("[role='tab']")); const viewportStart = tabList.scrollLeft; const viewportEnd = viewportStart + tabList.clientWidth; const hasContentBefore = tabList.scrollLeft > 1 || tabs.some((tab) => tab.offsetLeft < viewportStart - 1); const hasContentAfter = tabList.scrollLeft < maximumScroll - 1 || tabs.some((tab) => (tab.offsetLeft + tab.offsetWidth) > viewportEnd + 1); previousButton.hidden = !hasContentBefore; nextButton.hidden = !hasContentAfter; }; const scrollTabs = (direction) => { tabList.scrollBy({ left: direction * Math.max(160, Math.floor(tabList.clientWidth * .72)), behavior: "smooth" }); }; previousButton.addEventListener("click", () => scrollTabs(-1)); nextButton.addEventListener("click", () => scrollTabs(1)); tabList.addEventListener("scroll", updateNavigation, { passive: true }); const handleResize = () => { if (!navigation.isConnected) { window.removeEventListener("resize", handleResize); return; } updateNavigation(); }; window.addEventListener("resize", handleResize, { passive: true }); requestAnimationFrame(() => requestAnimationFrame(updateNavigation)); setTimeout(updateNavigation, 0); navigation.append(previousButton, tabList, nextButton); return navigation; } function createAccountPlansTabNavigationButton(direction) { const isPrevious = direction === "previous"; const button = document.createElement("button"); button.type = "button"; button.className = "opportunities-extension-account-plans-tab-navigation-button"; button.setAttribute("aria-label", isPrevious ? "Show previous account plans" : "Show next account plans"); button.title = isPrevious ? "Previous plans" : "Next plans"; const icon = createSvgElement("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false" }); icon.append(createSvgElement("path", { d: isPrevious ? "m14.5 5-7 7 7 7" : "m9.5 5 7 7-7 7", fill: "none", stroke: "currentColor", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2" })); button.append(icon); return button; } function createAccountPlansSkeleton() { const skeleton = document.createElement("div"); skeleton.className = "opportunities-extension-account-plans-skeleton"; skeleton.setAttribute("role", "status"); const announcement = document.createElement("span"); announcement.className = "opportunities-extension-visually-hidden"; announcement.textContent = "Loading cloud consumption"; const tabs = document.createElement("div"); tabs.className = "opportunities-extension-account-plans-skeleton-tabs"; const tab = document.createElement("span"); tab.className = "opportunities-extension-account-plans-skeleton-tab"; const status = document.createElement("span"); status.className = "opportunities-extension-account-plans-skeleton-status"; const tabLabel = document.createElement("span"); tabLabel.className = "opportunities-extension-account-plans-skeleton-bar"; tabLabel.style.width = "104px"; tab.append(status, tabLabel); tabs.append(tab); const details = document.createElement("div"); details.className = "opportunities-extension-account-plans-skeleton-details"; [72, 88, 64, 78, 58, 68].forEach((width) => { const row = document.createElement("div"); const label = document.createElement("span"); const value = document.createElement("span"); label.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-account-plans-skeleton-label"; value.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-account-plans-skeleton-value"; value.style.width = `${width}%`; row.append(label, value); details.append(row); }); const trend = document.createElement("div"); trend.className = "opportunities-extension-account-plans-skeleton-trend"; const trendHeader = document.createElement("div"); trendHeader.className = "opportunities-extension-account-plans-skeleton-trend-header"; const periods = document.createElement("div"); periods.className = "opportunities-extension-account-plans-skeleton-periods"; [58, 52, 56, 38].forEach((width, index) => { const period = document.createElement("span"); period.className = "opportunities-extension-account-plans-skeleton-bar"; period.style.width = `${width}px`; if (index === 2) period.classList.add("opportunities-extension-account-plans-skeleton-period-active"); periods.append(period); }); trendHeader.append(periods); const navigation = document.createElement("div"); navigation.className = "opportunities-extension-account-plans-skeleton-navigation"; [32, 124, 32].forEach((width) => { const item = document.createElement("span"); item.className = "opportunities-extension-account-plans-skeleton-bar"; item.style.width = `${width}px`; navigation.append(item); }); const chart = document.createElement("div"); chart.className = "opportunities-extension-account-plans-skeleton-chart"; const plot = document.createElement("div"); plot.className = "opportunities-extension-account-plans-skeleton-plot"; [34, 42, 38, 48, 44, 58, 64, 55, 76, 68, 86, 62].forEach((height) => { const bar = document.createElement("span"); bar.className = "opportunities-extension-account-plans-skeleton-bar"; bar.style.height = `${height}%`; plot.append(bar); }); const legend = document.createElement("div"); legend.className = "opportunities-extension-account-plans-skeleton-legend"; [112, 68, 132].forEach((width) => { const item = document.createElement("span"); item.className = "opportunities-extension-account-plans-skeleton-bar"; item.style.width = `${width}px`; legend.append(item); }); chart.append(plot, legend); trend.append(trendHeader, navigation, chart); const panel = document.createElement("div"); panel.className = "opportunities-extension-account-plan-panel opportunities-extension-account-plans-skeleton-panel"; panel.append(details, trend); skeleton.append(announcement, tabs, panel); return skeleton; } function createAccountPlanStatusIndicator(statusValue) { const indicator = document.createElement("span"); const isActive = String(statusValue).toUpperCase() === "ACTIVE"; indicator.className = "opportunities-extension-account-plan-status-indicator"; indicator.dataset.active = String(isActive); indicator.setAttribute("aria-hidden", "true"); indicator.title = `Header Status: ${statusValue}`; return indicator; } function createAccountPlanDetails(plan) { return createPlanDetails(plan, { contractAction: createRatecardButton(plan) }); } function closeAccountPlansModal() { closePlanDetailPopover(); closeRatecardDrawer({ immediate: true, restoreFocus: false }); const modal = document.getElementById(ACCOUNT_PLANS_MODAL_ID); if (modal) { modal.hidden = true; } } return { openAccountPlansModal, createAccountPlansTabsNavigation, createAccountPlansTabNavigationButton, createAccountPlansSkeleton, createAccountPlanStatusIndicator, createAccountPlanDetails, closeAccountPlansModal }; } // Source: src/features/consumption/header-view.js function createConsumptionHeaderView({ createCloseIcon, formatOracleResponseDate, environment }) { const { document } = environment; function createHeader({ titleId, customerLabel, closeLabel, onClose, titleText = "Cloud Consumption" }) { const header = document.createElement("header"); header.className = "opportunities-extension-account-plans-header"; const heading = document.createElement("div"); const title = document.createElement("h2"); title.id = titleId; title.textContent = titleText; const subtitle = document.createElement("p"); subtitle.textContent = customerLabel; heading.append(title, subtitle); const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.className = "opportunities-extension-icon-button"; closeButton.setAttribute("aria-label", closeLabel); closeButton.title = "Close"; closeButton.append(createCloseIcon()); closeButton.addEventListener("click", onClose); header.append(heading, closeButton); return header; } function createDetails(plan, { contractAction = null } = {}) { const details = document.createElement("dl"); details.className = "opportunities-extension-account-plan-details"; const fields = [ { label: "Booking Customer", value: plan.bookingCustomer }, { label: "Sold To Party Name", value: plan.soldToPartyName }, { label: "Contract ID", value: plan.contractId }, { label: "Header Status", value: plan.hdrStatusCode }, { label: "Start Date", value: formatOracleResponseDate(plan.startDate) }, { label: "End Date", value: formatOracleResponseDate(plan.endDate) } ]; fields.forEach(field => { const group = document.createElement("div"); const term = document.createElement("dt"); const description = document.createElement("dd"); term.textContent = field.label; description.textContent = field.value === null || field.value === undefined || field.value === "" ? "-" : String(field.value); if (field.label === "Contract ID" && contractAction) { group.className = "opportunities-extension-contract-row"; const value = document.createElement("span"); value.textContent = description.textContent; description.textContent = ""; description.className = "opportunities-extension-contract-ratecard"; description.append(value, contractAction); } group.append(term, description); details.append(group); }); return details; } return { createHeader, createDetails }; } // Source: src/features/ratecard/model.js function createRatecardModel() { const columns = [ { key: "orderNumber", label: "Order Number", width: 7 }, { key: "partNum", label: "Part Number", width: 6 }, { key: "productName", label: "Product Name", width: 22 }, { key: "uom", label: "UOM", width: 10 }, { key: "subLnStartSt", label: "Start Date", type: "date", width: 8 }, { key: "subLnEndDt", label: "End Date", type: "date", width: 8 }, { key: "usageMax", label: "Usage Max", type: "number", width: 5 }, { key: "rateCardDiscountPerc", label: "Discount (%)", type: "percent", width: 7 }, { key: "listPriceUsd", label: "List price (USD)", type: "price", width: 9 }, { key: "netUnitPriceUsd", label: "Net Unit Price (USD)", type: "price", width: 9 }, { key: "overagePriceUsd", label: "Overage Net Unit Price (USD)", type: "price", width: 9 } ]; const collator = new Intl.Collator("en", { numeric: true, sensitivity: "base" }); const prices = new Intl.NumberFormat("en-US", { minimumFractionDigits: 4, maximumFractionDigits: 20 }); const numbers = new Intl.NumberFormat("en-US", { maximumFractionDigits: 20 }); const months = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]; const missing = value => value === null || value === undefined || String(value).trim() === "" || String(value).trim() === "-"; const numeric = value => !missing(value) && Number.isFinite(Number(value)) ? Number(value) : null; const normalize = value => String(value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim(); function formatValue(item, column) { const value = item[column.key]; if (missing(value)) return "-"; const number = numeric(value); if (number !== null) { if (column.type === "price") return prices.format(number); if (column.type === "percent") return `${numbers.format(number)}%`; if (column.type === "number") return numbers.format(number); } return String(value).trim(); } function dateValue(value) { const match = String(value).match(/^(\d{1,2})-([a-z]{3})-(\d{4})$/i); if (match) { const month = months.indexOf(match[2].toLowerCase()); return month < 0 ? null : Date.UTC(Number(match[3]), month, Number(match[1])); } const iso = String(value).match(/^(\d{4})-(\d{2})-(\d{2})(?:T.*)?$/); return iso ? Date.UTC(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3])) : null; } function selectItems(items, search = "", sortKey = "", direction = "asc") { const query = normalize(search); const filtered = items.filter(item => !query || columns.some(column => normalize(item[column.key]).includes(query) || normalize(formatValue(item, column)).includes(query))); const column = columns.find(candidate => candidate.key === sortKey); if (!column) return filtered; const sortValue = item => { const value = item[column.key]; if (missing(value)) return null; if (column.type === "date") return dateValue(value); return ["price", "number", "percent"].includes(column.type) ? numeric(value) : String(value); }; return filtered.sort((a, b) => { const left = sortValue(a), right = sortValue(b); if (left === null || right === null) return left === right ? 0 : left === null ? 1 : -1; const compared = typeof left === "number" ? left - right : collator.compare(left, right); return direction === "desc" ? -compared : compared; }); } return { columns, formatValue, selectItems }; } // Source: src/features/ratecard/controller.js function createRatecardController({ messageTypes, requestConsumerTokenServiceOnce, sendRuntimeMessage, appendBackgroundRequestLog, openRatecardDrawer, closePlanDetailPopover, environment }) { const pending = new Map(); async function request(planNumber) { const authorizations = await requestConsumerTokenServiceOnce(); const response = await sendRuntimeMessage({ type: messageTypes.requestRatecardByPlan, planNumber, authorization: authorizations && authorizations.accountPlans }); appendBackgroundRequestLog(response?.debugRequests); if (!response?.ok) throw new Error(response?.error || "Unable to load rate card."); return response.items; } async function showRatecard(plan, trigger) { closePlanDetailPopover(); const handle = openRatecardDrawer(plan, trigger); if (!handle) return; try { const planNumber = String(plan.subPlanNum ?? "").trim(); if (!planNumber) throw new Error("Plan number is unavailable for this account plan."); if (!pending.has(planNumber)) { const promise = request(planNumber).finally(() => { if (pending.get(planNumber) === promise) pending.delete(planNumber); }); pending.set(planNumber, promise); } handle.showItems(await pending.get(planNumber)); } catch (error) { handle.showError(error.message || "Unable to load rate card."); } } function createRatecardButton(plan) { const button = environment.document.createElement("button"); button.type = "button"; button.className = "opportunities-extension-ratecard-button"; button.textContent = "Rate Card"; button.title = "View rate card"; button.setAttribute("aria-haspopup", "dialog"); button.addEventListener("click", () => showRatecard(plan, button)); return button; } return { createRatecardButton, showRatecard, resetRequests: () => pending.clear() }; } // Source: src/features/ratecard/view.js function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, selectItems, createConsumptionHeader, createPlanDetails, environment }) { const { document, setTimeout, clearTimeout, requestAnimationFrame, cancelAnimationFrame } = environment; let active = null; let closing = null; let closeTimer = null; function closeRatecardDrawer({ immediate = false, restoreFocus = true } = {}) { if (closeTimer !== null) clearTimeout(closeTimer); closeTimer = null; closing?.remove(); closing = null; if (!active) return; const previous = active; active = null; if (previous.openFrame !== null) cancelAnimationFrame(previous.openFrame); previous.background.inert = previous.wasInert; if (immediate) previous.root.remove(); else { closing = previous.root; closing.dataset.closing = "true"; closing.inert = true; closeTimer = setTimeout(() => { closing?.remove(); closing = null; closeTimer = null; }, 200); } if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus({ preventScroll: true }); } function openRatecardDrawer(plan, trigger) { const parent = document.getElementById(ACCOUNT_PLANS_MODAL_ID); const background = parent?.querySelector(".opportunities-extension-account-plans-dialog"); if (!parent || parent.hidden || !background) return null; closeRatecardDrawer({ immediate: true, restoreFocus: false }); const element = (tag, className, text) => { const node = document.createElement(tag); if (className) node.className = `opportunities-extension-ratecard-${className}`; if (text !== undefined) node.textContent = text; return node; }; const root = element("div", "overlay"); root.dataset.open = "false"; const panel = element("section", "drawer"); panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "true"); panel.setAttribute("aria-labelledby", "opportunities-extension-ratecard-title"); panel.setAttribute("aria-busy", "true"); const header = createConsumptionHeader({ titleId: "opportunities-extension-ratecard-title", titleText: "Rate Card", customerLabel: parent.dataset.customerLabel, closeLabel: "Close rate card", onClose: () => closeRatecardDrawer() }); header.classList.add("opportunities-extension-ratecard-header"); const close = header.querySelector("button"); const details = createPlanDetails(plan); details.classList.add("opportunities-extension-ratecard-plan-details"); const toolbar = element("div", "toolbar"); const search = element("input", "search"); search.type = "search"; search.placeholder = "Search rate card"; search.setAttribute("aria-label", "Search rate card"); const count = element("span", "count", "Loading rate card..."); count.setAttribute("role", "status"); toolbar.append(search, count); const viewport = element("div", "viewport"); viewport.tabIndex = 0; viewport.setAttribute("role", "region"); viewport.setAttribute("aria-label", "Rate card items"); const table = element("table", "table"); const caption = element("caption", null, "Rate card items"); caption.className = "opportunities-extension-visually-hidden"; const colgroup = element("colgroup"); const head = element("thead"); const headRow = element("tr"); const sortHeaders = []; const current = { root, background, wasInert: background.inert, trigger, items: [], status: "loading", sortKey: "", direction: "asc", openFrame: null }; active = current; background.inert = true; const alive = () => active === current && root.isConnected && !parent.hidden; columns.forEach(column => { const col = element("col"); col.style.width = `${column.width}%`; colgroup.append(col); const th = element("th"); th.scope = "col"; th.setAttribute("aria-sort", "none"); const sort = element("button", "sort"); sort.type = "button"; sort.title = `Sort by ${column.label}`; const label = element("span", null, column.label); const icon = element("span", "sort-icon", "\u2195"); icon.setAttribute("aria-hidden", "true"); sort.append(label, icon); sort.addEventListener("click", () => { current.direction = current.sortKey === column.key && current.direction === "asc" ? "desc" : "asc"; current.sortKey = column.key; updateSortHeaders(); if (current.status === "ready") renderRows(); }); sortHeaders.push({ th, icon, key: column.key }); th.append(sort); headRow.append(th); }); head.append(headRow); const body = element("tbody"); table.append(caption, colgroup, head, body); viewport.append(table); panel.append(header, details, toolbar, viewport); root.append(panel); parent.append(root); function updateSortHeaders() { sortHeaders.forEach(({ th, icon, key }) => { const selected = key === current.sortKey; th.setAttribute("aria-sort", selected ? current.direction === "asc" ? "ascending" : "descending" : "none"); icon.textContent = !selected ? "\u2195" : current.direction === "asc" ? "\u2191" : "\u2193"; }); } function showNotice(message, error = false) { body.textContent = ""; const row = element("tr"); const cell = element("td", "notice"); cell.colSpan = columns.length; const text = element("p", null, message); if (error) text.setAttribute("role", "alert"); cell.append(text); row.append(cell); body.append(row); } function renderRows() { if (!alive()) return; body.textContent = ""; const visible = selectItems(current.items, search.value, current.sortKey, current.direction); count.textContent = `${visible.length} of ${current.items.length} items`; if (!visible.length) { showNotice(current.items.length ? "No matching rate card items." : "No rate card items found for this plan."); } visible.forEach(item => { const row = element("tr"); columns.forEach(column => { const cell = element("td", null, formatValue(item, column)); if (["price", "number", "percent"].includes(column.type)) cell.className = "opportunities-extension-ratecard-numeric"; row.append(cell); }); body.append(row); }); viewport.scrollTop = 0; } for (let index = 0; index < 7; index += 1) { const row = element("tr"); row.setAttribute("aria-hidden", "true"); columns.forEach(() => { const cell = element("td"); const skeleton = element("span", "skeleton"); cell.append(skeleton); row.append(cell); }); body.append(row); } search.addEventListener("input", () => { if (current.status === "ready") renderRows(); }); root.addEventListener("click", event => { if (event.target === root) closeRatecardDrawer(); }); root.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); closeRatecardDrawer(); } else if (event.key === "Tab") { const controls = [close, search, viewport, ...headRow.querySelectorAll("button")]; const index = controls.indexOf(document.activeElement); if (index < 0 || (event.shiftKey && index === 0) || (!event.shiftKey && index === controls.length - 1)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus({ preventScroll: true }); } } }); // Focusing offscreen content must not scroll the clipped overlay and cancel the slide. search.focus({ preventScroll: true }); // Paint the offscreen position before transitioning to the fully open position. current.openFrame = requestAnimationFrame(() => { if (!alive()) return; current.openFrame = requestAnimationFrame(() => { current.openFrame = null; if (alive()) root.dataset.open = "true"; }); }); return { showItems(items) { if (!alive()) return; current.items = items; current.status = "ready"; panel.setAttribute("aria-busy", "false"); renderRows(); }, showError(message) { if (!alive()) return; current.status = "error"; panel.setAttribute("aria-busy", "false"); count.textContent = "Rate card unavailable"; showNotice(message, true); } }; } return { openRatecardDrawer, closeRatecardDrawer }; } // Source: src/features/consumption/plan-detail-controller.js function createConsumptionPlanDetailController({ messageTypes, requestConsumerTokenServiceOnce, sendRuntimeMessage, appendBackgroundRequestLog, renderPlanDetailBadges, environment }) { const { document } = environment; const states = new WeakMap(); const pending = new Map(); function createPlanDetailSection(plan) { const section = document.createElement("div"); section.className = "opportunities-extension-plan-detail-badges"; section.dataset.subPlanNum = String(plan.subPlanNum || "").trim(); section.setAttribute("aria-live", "polite"); states.set(section, { status: "idle" }); return section; } async function request(subPlanNum) { const authorizations = await requestConsumerTokenServiceOnce(); const response = await sendRuntimeMessage({ type: messageTypes.requestConsumptionPlanDetail, subPlanNum, authorization: authorizations && authorizations.accountPlans }); appendBackgroundRequestLog(response?.debugRequests); if (!response?.ok) throw new Error(response?.error || "Unable to load consumption plan details."); return response.items; } function loadPlanDetailForPanel(panel, plan) { const section = panel?.querySelector(".opportunities-extension-plan-detail-badges"); if (!section) return Promise.resolve(); const state = states.get(section); if (state?.status === "ready") return Promise.resolve(); if (state?.status === "loading") return state.promise; const subPlanNum = String(plan.subPlanNum || "").trim(); section.textContent = ""; section.setAttribute("aria-busy", "true"); const skeleton = document.createElement("span"); skeleton.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-plan-detail-skeleton"; skeleton.setAttribute("role", "status"); skeleton.setAttribute("aria-label", "Loading plan projection"); section.append(skeleton); const current = { status: "loading", promise: null }; states.set(section, current); current.promise = (async () => { try { if (!subPlanNum) throw new Error("Sub plan number is unavailable."); if (!pending.has(subPlanNum)) { const promise = request(subPlanNum).finally(() => { if (pending.get(subPlanNum) === promise) pending.delete(subPlanNum); }); pending.set(subPlanNum, promise); } const items = await pending.get(subPlanNum); if (!section.isConnected || states.get(section) !== current) return; current.status = "ready"; renderPlanDetailBadges(section, items); } catch (error) { if (!section.isConnected || states.get(section) !== current) return; current.status = "error"; section.textContent = ""; const notice = document.createElement("span"); notice.className = "opportunities-extension-plan-detail-error"; notice.setAttribute("role", "alert"); notice.textContent = error.message || "Unable to load plan details."; section.append(notice); } finally { if (states.get(section) === current) section.setAttribute("aria-busy", "false"); } })(); return current.promise; } return { createPlanDetailSection, loadPlanDetailForPanel, resetRequests: () => pending.clear() }; } // Source: src/features/consumption/plan-detail-view.js function createConsumptionPlanDetailView({ createCloseIcon, environment }) { const { document, window, setTimeout, clearTimeout } = environment; let active = null; let sequence = 0; const fields = [ ["pricingModel", "Pricing model"], ["lineStatus", "Line status"], ["startDate", "Start date"], ["endDate", "End date"], ["committedCredits", "Committed credits", true], ["consumed", "Consumed", true], ["consumptionL30Days", "Consumption last 30 days", true], ["monthsRemaining", "Months remaining"], ["projectedConsumption", "Projected consumption", true], ["projectedDelta", "Projected delta", true], ["orderId", "Order number"] ]; function node(tag, className, text) { const result = document.createElement(tag); if (className) result.className = className; if (text !== undefined) result.textContent = text; return result; } function statusBadge(record, tag = "span") { const text = String(record.projectedStatus || "Unknown"); const badge = node(tag, "opportunities-extension-projection-badge", text); badge.dataset.status = text.trim().toLowerCase(); return badge; } function orderLink(record, text) { try { const url = new URL(record.orderTrackerURL); if (url.origin !== "https://accountdashboard.oracle.com" || url.username || url.password) return null; const link = node("a", "", text); link.href = url.href; link.target = "_blank"; link.rel = "noopener noreferrer"; return link; } catch { return null; } } function createLineDetails(record) { const section = node("section", "opportunities-extension-projection-line"); const heading = node("h3"); heading.append(statusBadge(record)); section.append(heading); const details = node("dl", "opportunities-extension-projection-fields"); fields.forEach(([key, label, numeric]) => { const raw = record[key]; let text = raw === null || raw === undefined || raw === "" ? "-" : String(raw); if (numeric && text !== "-" && Number.isFinite(Number(raw))) { text = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Number(raw)); } const group = node("div"); const value = node("dd"); const link = key === "orderId" && text !== "-" ? orderLink(record, text) : null; if (link) value.append(link); else value.textContent = text; group.append(node("dt", "", label), value); details.append(group); }); section.append(details); if (Array.isArray(record.clines) && record.clines.length) { const children = node("div", "opportunities-extension-projection-children"); record.clines.forEach(child => children.append(createLineDetails(child))); section.append(children); } return section; } function closePlanDetailPopover(restoreFocus = false) { if (!active) return; const previous = active; active = null; clearTimeout(previous.timer); previous.popover.hidden = true; previous.button.setAttribute("aria-expanded", "false"); document.removeEventListener("pointerdown", previous.outside, true); window.removeEventListener("resize", previous.resize); if (restoreFocus && previous.button.isConnected) { previous.suppressFocus = true; previous.button.focus(); previous.suppressFocus = false; } } function positionPopover(entry) { const rect = entry.button.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const width = Math.min(560, viewportWidth - 24); const height = Math.min(520, viewportHeight - 24); entry.popover.style.width = `${width}px`; entry.popover.style.maxHeight = `${height}px`; const below = viewportHeight - rect.bottom - 12; const actualHeight = Math.min(entry.popover.getBoundingClientRect().height, height); entry.popover.style.left = `${Math.max(12, Math.min(rect.left, viewportWidth - width - 12))}px`; entry.popover.style.top = `${Math.max(12, below >= actualHeight ? rect.bottom + 4 : Math.min(rect.top - actualHeight - 4, viewportHeight - actualHeight - 12))}px`; } function createInteractiveBadge(record) { const wrapper = node("div", "opportunities-extension-projection-wrapper"); const button = statusBadge(record, "button"); button.type = "button"; button.setAttribute("aria-label", `Projected status: ${button.textContent}. View plan details`); button.setAttribute("aria-haspopup", "dialog"); button.setAttribute("aria-expanded", "false"); const popover = node("div", "opportunities-extension-projection-popover"); popover.id = `opportunities-extension-projection-${++sequence}`; popover.hidden = true; popover.setAttribute("role", "dialog"); popover.setAttribute("aria-label", "Consumption plan details"); popover.tabIndex = -1; button.setAttribute("aria-controls", popover.id); const dismiss = node("button", "opportunities-extension-projection-dismiss"); dismiss.type = "button"; dismiss.setAttribute("aria-label", "Close plan details"); dismiss.title = "Close plan details"; dismiss.append(createCloseIcon()); dismiss.addEventListener("click", () => closePlanDetailPopover(true)); popover.append(dismiss, createLineDetails(record)); const entry = { button, popover, wrapper, pinned: false, timer: null, suppressFocus: false, outside: event => { if (!wrapper.contains(event.target)) closePlanDetailPopover(); }, resize: () => positionPopover(entry) }; function open() { if (active !== entry) { closePlanDetailPopover(); entry.pinned = false; active = entry; document.addEventListener("pointerdown", entry.outside, true); window.addEventListener("resize", entry.resize); } clearTimeout(entry.timer); popover.hidden = false; button.setAttribute("aria-expanded", "true"); positionPopover(entry); } wrapper.addEventListener("pointerenter", open); wrapper.addEventListener("pointerleave", () => { if (entry.pinned || wrapper.contains(document.activeElement)) return; entry.timer = setTimeout(() => { if (active === entry) closePlanDetailPopover(); }, 180); }); wrapper.addEventListener("focusin", () => { if (!entry.suppressFocus) open(); }); wrapper.addEventListener("focusout", event => { if (!wrapper.contains(event.relatedTarget) && active === entry) closePlanDetailPopover(); }); button.addEventListener("click", () => { if (active === entry && entry.pinned) closePlanDetailPopover(); else { open(); entry.pinned = true; } }); wrapper.addEventListener("keydown", event => { if (event.key === "Escape" && active === entry) { event.preventDefault(); event.stopPropagation(); closePlanDetailPopover(true); } }); wrapper.append(button, popover); return wrapper; } function renderPlanDetailBadges(section, items) { if (active && section.contains(active.button)) closePlanDetailPopover(); section.textContent = ""; if (!items.length) { section.append(node("span", "opportunities-extension-projection-empty", "No projection data")); return; } items.forEach(record => section.append(createInteractiveBadge(record))); } return { renderPlanDetailBadges, createLineDetails, closePlanDetailPopover }; } // Source: src/features/consumption/trend-controller.js function createConsumptionTrendController({ messageTypes, CONSUMPTION_TREND_PERIODS, DEFAULT_CONSUMPTION_TREND_PERIOD, appendBackgroundRequestLog, consumptionTrendState, createPlanDetailSection, renderConsumptionTrendChart, requestConsumerTokenServiceOnce, sendRuntimeMessage, environment }) { const { document } = environment; function createConsumptionTrendSection(plan) { const section = document.createElement("section"); section.className = "opportunities-extension-consumption-trend"; section.dataset.subPlanNum = String(plan.subPlanNum || ""); section.dataset.period = DEFAULT_CONSUMPTION_TREND_PERIOD; const header = document.createElement("header"); header.className = "opportunities-extension-consumption-trend-header"; const periodControls = document.createElement("div"); periodControls.className = "opportunities-extension-consumption-periods"; periodControls.setAttribute("role", "group"); periodControls.setAttribute("aria-label", "Consumption period"); CONSUMPTION_TREND_PERIODS.forEach((periodOption) => { const button = document.createElement("button"); button.type = "button"; button.dataset.period = periodOption.value; button.setAttribute("aria-pressed", String(periodOption.value === DEFAULT_CONSUMPTION_TREND_PERIOD)); button.textContent = periodOption.label; button.addEventListener("click", () => { section.dataset.period = periodOption.value; consumptionTrendState.consumptionTrendWindowOffsets.set( createConsumptionTrendKey(section.dataset.subPlanNum, periodOption.value), 0 ); loadConsumptionTrendForPanel(section.closest("[role='tabpanel']"), plan, periodOption.value); }); periodControls.append(button); }); const chart = document.createElement("div"); chart.className = "opportunities-extension-consumption-chart"; chart.setAttribute("aria-live", "polite"); header.append(createPlanDetailSection(plan), periodControls); section.append(header, chart); return section; } function loadConsumptionTrendForPanel(panel, plan, period) { if (!panel) { return; } const section = panel.querySelector(".opportunities-extension-consumption-trend"); if (!section) { return; } const subPlanNum = String(plan.subPlanNum || "").trim(); const selectedPeriod = CONSUMPTION_TREND_PERIODS.some((item) => item.value === period) ? period : DEFAULT_CONSUMPTION_TREND_PERIOD; const key = createConsumptionTrendKey(subPlanNum, selectedPeriod); const chart = section.querySelector(".opportunities-extension-consumption-chart"); section.dataset.period = selectedPeriod; chart.dataset.requestKey = key; updateConsumptionPeriodButtons(section, selectedPeriod); if (!subPlanNum) { renderConsumptionTrendError(chart, "Sub plan number is unavailable."); return; } const cached = consumptionTrendState.consumptionTrendCache.get(key); renderConsumptionTrendSkeleton(chart); if (cached && cached.status === "loading") { cached.promise .then((items) => renderConsumptionTrendIfCurrent(section, chart, items, selectedPeriod, subPlanNum, key)) .catch((error) => renderConsumptionTrendErrorIfCurrent(chart, error.message, key)); return; } const request = requestConsumerTokenServiceOnce() .then((authorizations) => sendRuntimeMessage({ type: messageTypes.requestConsumptionTrend, subPlanNum, period: selectedPeriod, authorization: authorizations && authorizations.accountPlans })) .then((response) => { appendBackgroundRequestLog(response && response.debugRequests); if (!response || !response.ok) { throw new Error(response && response.error ? response.error : "Unable to load consumption trend."); } return Array.isArray(response.items) ? response.items : []; }) .finally(() => { if (consumptionTrendState.consumptionTrendCache.get(key)?.promise === request) { consumptionTrendState.consumptionTrendCache.delete(key); } }); consumptionTrendState.consumptionTrendCache.set(key, { status: "loading", promise: request }); request .then((items) => renderConsumptionTrendIfCurrent(section, chart, items, selectedPeriod, subPlanNum, key)) .catch((error) => renderConsumptionTrendErrorIfCurrent(chart, error.message, key)); } function createConsumptionTrendKey(subPlanNum, period) { return `${subPlanNum}:${period}`; } function updateConsumptionPeriodButtons(section, period) { section.querySelectorAll(".opportunities-extension-consumption-periods button").forEach((button) => { button.setAttribute("aria-pressed", String(button.dataset.period === period)); }); } function renderConsumptionTrendIfCurrent(section, chart, items, period, subPlanNum, key) { if (chart.dataset.requestKey === key) { renderConsumptionTrendChart(section, chart, items, period, subPlanNum); } } function renderConsumptionTrendErrorIfCurrent(chart, errorMessage, key) { if (chart.dataset.requestKey === key) { renderConsumptionTrendError(chart, errorMessage || "Unable to load consumption trend."); } } function renderConsumptionTrendSkeleton(chart) { chart.textContent = ""; chart.setAttribute("aria-busy", "true"); const skeleton = document.createElement("div"); skeleton.className = "opportunities-extension-consumption-chart-skeleton"; const announcement = document.createElement("span"); announcement.className = "opportunities-extension-visually-hidden"; announcement.textContent = "Loading consumption trend"; const plot = document.createElement("div"); plot.className = "opportunities-extension-consumption-chart-skeleton-plot"; for (let index = 0; index < 13; index += 1) { const bar = document.createElement("span"); bar.style.height = `${28 + ((index * 17) % 58)}%`; plot.append(bar); } skeleton.append(announcement, plot); chart.append(skeleton); } function renderConsumptionTrendError(chart, errorMessage) { chart.textContent = ""; chart.setAttribute("aria-busy", "false"); const error = document.createElement("p"); error.className = "opportunities-extension-consumption-chart-error"; error.setAttribute("role", "alert"); error.textContent = errorMessage; chart.append(error); } return { createConsumptionTrendSection, loadConsumptionTrendForPanel, createConsumptionTrendKey, updateConsumptionPeriodButtons, renderConsumptionTrendIfCurrent, renderConsumptionTrendErrorIfCurrent, renderConsumptionTrendSkeleton, renderConsumptionTrendError }; } // Source: src/features/consumption/chart.js function createConsumptionChart({ CONSUMPTION_TREND_PERIODS, CONSUMPTION_TREND_SERIES, SVG_NAMESPACE, calculateAverageConsumption, consumptionTrendState, createConsumptionTrendKey, createNiceConsumptionMaximum, formatCompactConsumptionNumber, formatConsumptionCurrency, formatConsumptionPointLabel, formatConsumptionRange, normalizeConsumptionTrendItems, environment }) { const { document, window } = environment; function renderConsumptionTrendChart(section, chart, items, period, subPlanNum) { chart.textContent = ""; chart.setAttribute("aria-busy", "false"); const points = normalizeConsumptionTrendItems(items); if (points.length === 0) { const empty = document.createElement("p"); empty.className = "opportunities-extension-consumption-chart-empty"; empty.textContent = "No consumption data found for this period."; chart.append(empty); return; } const option = CONSUMPTION_TREND_PERIODS.find((item) => item.value === period); const windowSize = option ? option.windowSize : points.length; const offsetKey = createConsumptionTrendKey(subPlanNum, period); const maxOffset = Math.max(0, Math.ceil(points.length / windowSize) - 1); const offset = Math.min(consumptionTrendState.consumptionTrendWindowOffsets.get(offsetKey) || 0, maxOffset); const end = Math.max(0, points.length - (offset * windowSize)); const start = Math.max(0, end - windowSize); const visiblePoints = points.slice(start, end); consumptionTrendState.consumptionTrendWindowOffsets.set(offsetKey, offset); const navigation = document.createElement("div"); navigation.className = "opportunities-extension-consumption-navigation"; const olderButton = createConsumptionNavigationButton("older", start === 0); const newerButton = createConsumptionNavigationButton("newer", end === points.length); const range = document.createElement("strong"); range.textContent = formatConsumptionRange(visiblePoints, period); olderButton.addEventListener("click", () => { consumptionTrendState.consumptionTrendWindowOffsets.set(offsetKey, Math.min(maxOffset, offset + 1)); renderConsumptionTrendChart(section, chart, items, period, subPlanNum); }); newerButton.addEventListener("click", () => { consumptionTrendState.consumptionTrendWindowOffsets.set(offsetKey, Math.max(0, offset - 1)); renderConsumptionTrendChart(section, chart, items, period, subPlanNum); }); navigation.append(olderButton, range, newerButton); const figure = document.createElement("figure"); figure.className = "opportunities-extension-consumption-figure"; const visual = document.createElement("div"); visual.className = "opportunities-extension-consumption-visual"; const tooltip = document.createElement("div"); tooltip.className = "opportunities-extension-consumption-tooltip"; tooltip.hidden = true; tooltip.setAttribute("role", "tooltip"); const activeSeries = getConsumptionTrendSeriesFilter(subPlanNum); const svg = createConsumptionSvg( visiblePoints, period, end === points.length, activeSeries, tooltip ); const legend = createConsumptionLegend( section, chart, items, period, subPlanNum, activeSeries ); visual.append(svg, legend); figure.append(visual, tooltip); chart.append(navigation, figure); } function getConsumptionTrendSeriesFilter(subPlanNum) { if (!consumptionTrendState.consumptionTrendSeriesFilters.has(subPlanNum)) { consumptionTrendState.consumptionTrendSeriesFilters.set( subPlanNum, new Set(CONSUMPTION_TREND_SERIES.map((series) => series.value)) ); } return consumptionTrendState.consumptionTrendSeriesFilters.get(subPlanNum); } function createConsumptionLegend(section, chart, items, period, subPlanNum, activeSeries) { const legend = document.createElement("div"); legend.className = "opportunities-extension-consumption-legend"; legend.setAttribute("role", "group"); legend.setAttribute("aria-label", "Chart series filters"); CONSUMPTION_TREND_SERIES.forEach((series) => { const button = document.createElement("button"); const swatch = document.createElement("span"); const label = document.createElement("span"); button.type = "button"; button.dataset.series = series.value; button.setAttribute("aria-pressed", String(activeSeries.has(series.value))); button.setAttribute("aria-label", `${activeSeries.has(series.value) ? "Hide" : "Show"} ${series.label}`); swatch.className = `opportunities-extension-consumption-legend-swatch opportunities-extension-consumption-legend-${series.value}`; swatch.setAttribute("aria-hidden", "true"); label.textContent = series.label; button.append(swatch, label); button.addEventListener("click", () => { if (activeSeries.has(series.value)) { activeSeries.delete(series.value); } else { activeSeries.add(series.value); } renderConsumptionTrendChart(section, chart, items, period, subPlanNum); chart.querySelector(`[data-series="${series.value}"]`)?.focus(); }); legend.append(button); }); return legend; } function createConsumptionNavigationButton(direction, disabled) { const button = document.createElement("button"); button.type = "button"; button.className = "opportunities-extension-consumption-navigation-button"; button.disabled = disabled; button.setAttribute("aria-label", direction === "older" ? "Show older consumption data" : "Show newer consumption data"); button.title = direction === "older" ? "Older" : "Newer"; const icon = createSvgElement("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false" }); icon.append(createSvgElement("path", { d: direction === "older" ? "m14.5 5-7 7 7 7" : "m9.5 5 7 7-7 7", fill: "none", stroke: "currentColor", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2" })); button.append(icon); return button; } function createConsumptionSvg(points, period, hasPartialLatestPoint, activeSeries, tooltip) { const width = 1000; const height = 350; const plot = { left: 62, top: 24, right: 980, bottom: 258 }; const plotWidth = plot.right - plot.left; const plotHeight = plot.bottom - plot.top; const average = calculateAverageConsumption(points, hasPartialLatestPoint, activeSeries); const maximum = createNiceConsumptionMaximum(Math.max(average, ...points.map((point) => point.total))); const svg = createSvgElement("svg", { viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": `${period} cloud consumption chart` }); svg.classList.add("opportunities-extension-consumption-svg"); for (let tick = 0; tick <= 5; tick += 1) { const value = (maximum / 5) * tick; const y = plot.bottom - ((value / maximum) * plotHeight); svg.append( createSvgElement("line", { x1: plot.left, y1: y, x2: plot.right, y2: y, class: "consumption-grid-line" }), createSvgText(plot.left - 10, y + 4, formatCompactConsumptionNumber(value), "consumption-axis-label", "end") ); } const step = plotWidth / Math.max(points.length, 1); const barWidth = Math.min(44, Math.max(10, step * 0.58)); points.forEach((point, index) => { const x = plot.left + (step * index) + ((step - barWidth) / 2); const fundedAmount = activeSeries.has("funded") ? point.funded : 0; const overageAmount = activeSeries.has("overage") ? point.overage : 0; const visibleTotal = fundedAmount + overageAmount; const fundedHeight = (fundedAmount / maximum) * plotHeight; const overageHeight = (overageAmount / maximum) * plotHeight; const group = createSvgElement("g", { class: "consumption-bar-group", tabindex: "0", role: "img", "aria-label": createConsumptionPointAriaLabel(point, period) }); ["pointerenter", "pointermove", "focus"].forEach((eventName) => { group.addEventListener(eventName, (event) => { showConsumptionTooltip(tooltip, point, period, event); }); }); ["pointerleave", "blur"].forEach((eventName) => { group.addEventListener(eventName, () => { tooltip.hidden = true; }); }); if (fundedHeight > 0) { const fundedBar = createSvgElement("rect", { x, y: plot.bottom - fundedHeight, width: barWidth, height: fundedHeight, class: "consumption-bar-funded" }); appendConsumptionBarAnimation( fundedBar, plot.bottom, plot.bottom - fundedHeight, fundedHeight, index * 34 ); group.append(fundedBar); } if (overageHeight > 0) { const overageTop = plot.bottom - fundedHeight - overageHeight; const overageBar = createSvgElement("rect", { x, y: overageTop, width: barWidth, height: overageHeight, class: "consumption-bar-overage" }); appendConsumptionBarAnimation( overageBar, plot.bottom - fundedHeight, overageTop, overageHeight, (index * 34) + 90 ); group.append(overageBar); } const top = plot.bottom - fundedHeight - overageHeight; if (visibleTotal > 0) { const valueLabel = createSvgText( x + (barWidth / 2), Math.max(plot.top + 11, top - 7), formatCompactConsumptionNumber(visibleTotal), "consumption-value-label consumption-animated-label", "middle" ); valueLabel.style.animationDelay = `${(index * 34) + 360}ms`; group.append(valueLabel); } const label = createSvgText( x + (barWidth / 2), plot.bottom + 18, formatConsumptionPointLabel(point, period), "consumption-x-label", period === "monthly" || period === "quarterly" ? "middle" : "end" ); if (period === "weekly" || period === "daily") { label.setAttribute("transform", `rotate(-65 ${x + (barWidth / 2)} ${plot.bottom + 18})`); } group.append(label); svg.append(group); }); if (activeSeries.has("average")) { const averageY = plot.bottom - ((average / maximum) * plotHeight); const averageLine = createSvgElement("line", { x1: plot.left, y1: averageY, x2: plot.right, y2: averageY, class: "consumption-average-line consumption-animated-average" }); svg.append(averageLine); } return svg; } function appendConsumptionBarAnimation(element, fromY, toY, height, delayMs) { if (prefersReducedMotion()) { return; } element.append( createSvgElement("animate", { attributeName: "y", from: fromY, to: toY, dur: "0.52s", begin: `${delayMs}ms`, fill: "freeze", calcMode: "spline", keyTimes: "0;1", keySplines: "0.2 0.8 0.2 1" }), createSvgElement("animate", { attributeName: "height", from: "0", to: height, dur: "0.52s", begin: `${delayMs}ms`, fill: "freeze", calcMode: "spline", keyTimes: "0;1", keySplines: "0.2 0.8 0.2 1" }) ); } function prefersReducedMotion() { return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; } function createConsumptionPointAriaLabel(point, period) { return `${formatConsumptionPointLabel(point, period)}. ` + `Funded Allocation: ${formatConsumptionCurrency(point.funded, point.currencyCode)}. ` + `Overage: ${formatConsumptionCurrency(point.overage, point.currencyCode)}. ` + `Total: ${formatConsumptionCurrency(point.total, point.currencyCode)}.`; } function showConsumptionTooltip(tooltip, point, period, event) { const figure = tooltip.parentElement; if (!figure) { return; } tooltip.textContent = ""; const title = document.createElement("strong"); title.textContent = formatConsumptionPointLabel(point, period); const details = document.createElement("span"); details.textContent = `Funded Allocation: ${formatConsumptionCurrency(point.funded, point.currencyCode)}`; const overage = document.createElement("span"); overage.textContent = `Overage: ${formatConsumptionCurrency(point.overage, point.currencyCode)}`; const total = document.createElement("span"); total.textContent = `Total: ${formatConsumptionCurrency(point.total, point.currencyCode)}`; tooltip.append(title, details, overage, total); tooltip.hidden = false; const figureBounds = figure.getBoundingClientRect(); const targetBounds = event.currentTarget.getBoundingClientRect(); const pointerX = Number.isFinite(event.clientX) && event.clientX > 0 ? event.clientX : targetBounds.left + (targetBounds.width / 2); const pointerY = Number.isFinite(event.clientY) && event.clientY > 0 ? event.clientY : targetBounds.top; const left = Math.min( Math.max(8, pointerX - figureBounds.left + 12), Math.max(8, figureBounds.width - tooltip.offsetWidth - 8) ); const top = Math.max(8, pointerY - figureBounds.top - tooltip.offsetHeight - 10); tooltip.style.left = `${left}px`; tooltip.style.top = `${top}px`; } function createSvgElement(name, attributes = {}) { const element = document.createElementNS(SVG_NAMESPACE, name); Object.entries(attributes).forEach(([key, value]) => { element.setAttribute(key, String(value)); }); return element; } function createSvgText(x, y, value, className, anchor) { const text = createSvgElement("text", { x, y, class: className, "text-anchor": anchor }); text.textContent = value; return text; } return { renderConsumptionTrendChart, getConsumptionTrendSeriesFilter, createConsumptionLegend, createConsumptionNavigationButton, createConsumptionSvg, appendConsumptionBarAnimation, prefersReducedMotion, createConsumptionPointAriaLabel, showConsumptionTooltip, createSvgElement, createSvgText }; } // Source: src/features/consumption/model.js function createConsumptionModel() { function normalizeConsumptionTrendItems(items) { const points = new Map(); (Array.isArray(items) ? items : []).forEach((item) => { const dateKey = String(item.fyDate || (Array.isArray(item.fiscalTime) ? item.fiscalTime.join("|") : "")); if (!dateKey) { return; } if (!points.has(dateKey)) { points.set(dateKey, { dateKey, date: parseConsumptionDate(item.fyDate), fiscalTime: Array.isArray(item.fiscalTime) ? item.fiscalTime.slice() : [], fiscalQtr: item.fiscalQtr || "", funded: 0, overage: 0, arr: 0, currencyCode: item.currencyCode || "USD" }); } const point = points.get(dateKey); const amount = toFiniteNumber(item.usedAmount); const category = String(item.usageCategory || "Funded Allocation").toUpperCase(); if (category === "OVERAGE") { point.overage += amount; } else { point.funded += amount; } point.arr = Math.max(point.arr, toFiniteNumber(item.arrCd)); }); return Array.from(points.values()) .map((point) => ({ ...point, total: point.funded + point.overage })) .sort((left, right) => left.dateKey.localeCompare(right.dateKey)); } function getConsumptionPointTotal(point, activeSeries) { const selectedSeries = activeSeries && typeof activeSeries.has === "function" ? activeSeries : new Set(["funded", "overage"]); return (selectedSeries.has("funded") ? point.funded : 0) + (selectedSeries.has("overage") ? point.overage : 0); } function calculateAverageConsumption(points, hasPartialLatestPoint, activeSeries) { const completePoints = hasPartialLatestPoint && points.length > 1 ? points.slice(0, -1) : points; return completePoints.reduce( (sum, point) => sum + getConsumptionPointTotal(point, activeSeries), 0 ) / Math.max(completePoints.length, 1); } function createNiceConsumptionMaximum(value) { if (!Number.isFinite(value) || value <= 0) { return 1; } const magnitude = 10 ** Math.floor(Math.log10(value)); const normalized = value / magnitude; const step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10; return step * magnitude; } function formatConsumptionPointLabel(point, period) { if (period === "quarterly") { return point.fiscalTime[0] || point.fiscalQtr || point.dateKey; } if (period === "monthly") { return point.fiscalTime[1] || formatConsumptionDate(point.date, { month: "short" }); } return point.fiscalTime[0] || formatConsumptionDate(point.date, { day: "2-digit", month: "short", year: "2-digit" }); } function formatConsumptionRange(points, period) { if (points.length === 0) { return "No data"; } if (period === "quarterly") { return `${formatConsumptionPointLabel(points[0], period)} - ${formatConsumptionPointLabel(points[points.length - 1], period)}`; } if (period === "monthly") { const options = { month: "short", year: "numeric" }; return `${formatConsumptionDate(points[0].date, options)} - ${formatConsumptionDate(points[points.length - 1].date, options)}`; } const options = { day: "2-digit", month: "short" }; return `${formatConsumptionDate(points[0].date, options)} - ${formatConsumptionDate(points[points.length - 1].date, options)}`; } function parseConsumptionDate(value) { if (!value) { return null; } const parts = String(value).split("-").map(Number); return parts.length === 3 && parts.every(Number.isFinite) ? new Date(parts[0], parts[1] - 1, parts[2]) : null; } function formatConsumptionDate(date, options) { if (!(date instanceof Date) || Number.isNaN(date.getTime())) { return "-"; } return new Intl.DateTimeFormat("en-US", options).format(date).replace(",", ""); } function formatCompactConsumptionNumber(value) { const amount = toFiniteNumber(value); if (Math.abs(amount) >= 1000000) { return `${(amount / 1000000).toFixed(amount >= 10000000 ? 0 : 1).replace(".0", "")}M`; } if (Math.abs(amount) >= 1000) { return `${Math.round(amount / 1000)}K`; } return String(Math.round(amount)); } function formatConsumptionCurrency(value, currencyCode) { try { return new Intl.NumberFormat("en-US", { style: "currency", currency: currencyCode || "USD", maximumFractionDigits: 0 }).format(toFiniteNumber(value)); } catch (error) { return `${currencyCode || "USD"} ${Math.round(toFiniteNumber(value)).toLocaleString("en-US")}`; } } function toFiniteNumber(value) { const number = Number(value); return Number.isFinite(number) ? number : 0; } return { normalizeConsumptionTrendItems, getConsumptionPointTotal, calculateAverageConsumption, createNiceConsumptionMaximum, formatConsumptionPointLabel, formatConsumptionRange, parseConsumptionDate, formatConsumptionDate, formatCompactConsumptionNumber, formatConsumptionCurrency, toFiniteNumber }; } // Source: src/features/debug/content-log.js function createDebugContentLog({ previewDebugBody, DEBUG_BODY_LIMIT, debugState, renderDebugPanel }) { function logRequestStart(details) { const entry = { id: debugState.requestLog.length + 1, name: details.name, method: details.method, url: details.url, headers: details.headers, status: "pending", startedAt: new Date(), completedAt: null, durationMs: null, httpStatus: null, statusText: "", requestHeaders: details.headers, requestBody: details.body || "", metadata: details.metadata || {}, responseHeaders: {}, responseBody: "", responseBodyTruncated: false, error: "" }; debugState.requestLog.push(entry); renderDebugPanel(); return entry; } function logRequestSuccess(entry, response, responseBody) { entry.status = response.ok ? "success" : "failed"; entry.completedAt = new Date(); entry.durationMs = entry.completedAt.getTime() - entry.startedAt.getTime(); entry.httpStatus = response.status; entry.statusText = response.statusText || ""; entry.responseHeaders = headersToObject(response.headers); entry.responseBody = responseBody.value; entry.responseBodyTruncated = responseBody.truncated; renderDebugPanel(); } function logRequestFailure(entry, errorMessage) { entry.status = "failed"; entry.completedAt = new Date(); entry.durationMs = entry.completedAt.getTime() - entry.startedAt.getTime(); entry.error = errorMessage; renderDebugPanel(); } function appendBackgroundRequestLog(entries) { if (!Array.isArray(entries) || entries.length === 0) { return; } entries.forEach((entry) => { const existing = entry.debugKey && debugState.requestLog.find(item => item.debugKey === entry.debugKey); if (existing && existing.status !== "pending" && entry.status === "pending") return; const normalized = { id: existing ? existing.id : debugState.requestLog.length + 1, debugKey: entry.debugKey, name: entry.name || "backgroundRequest", method: entry.method || "GET", url: entry.url || "", headers: entry.requestHeaders || {}, status: entry.status || "failed", startedAt: new Date(entry.startedAt), completedAt: entry.completedAt ? new Date(entry.completedAt) : null, durationMs: entry.durationMs, httpStatus: entry.httpStatus, statusText: entry.statusText || "", requestHeaders: entry.requestHeaders || {}, requestBody: entry.requestBody || "", metadata: entry.metadata || { executionContext: "background" }, responseHeaders: entry.responseHeaders || {}, responseBody: entry.responseBody || "", responseBodyTruncated: Boolean(entry.responseBodyTruncated), error: entry.error || "" }; if (existing) Object.assign(existing, normalized); else debugState.requestLog.push(normalized); }); renderDebugPanel(); } async function readResponseBodyForDebug(response) { try { const text = await response.clone().text(); return createDebugBody(text); } catch (error) { return { value: `Unable to read response body: ${error.message || "unknown error"}`, truncated: false }; } } function createDebugBody(text) { return previewDebugBody(text, DEBUG_BODY_LIMIT); } function headersToObject(headers) { const headerMap = {}; headers.forEach((value, key) => { headerMap[key] = value; }); return headerMap; } function formatHeaderBlock(headers) { const entries = Object.entries(headers); if (entries.length === 0) { return "(none)"; } return entries .map(([key, value]) => `${key}: ${debugState.debugSecretsVisible || !isSensitiveDebugKey(key) ? value : formatHiddenDebugValue(value)}`) .join("\n"); } function formatDebugBody(body) { const text = body || "(none)"; try { const normalized = String(text).replace(/^\uFEFF/, "").trimStart() .replace(/^\)\]\}',?\s*/, "") .replace(/^while\s*\(1\);\s*/, ""); let value = JSON.parse(normalized); // Some services serialize their JSON document inside a JSON string. for (let depth = 0; depth < 2 && typeof value === "string" && /^[\s]*[\[{]/.test(value); depth += 1) { try { value = JSON.parse(value); } catch { break; } } return JSON.stringify(debugState.debugSecretsVisible ? value : maskSensitiveDebugValue(value), null, 2); } catch (error) { return text; } } function maskSensitiveDebugValue(value, parentKey = "") { if (Array.isArray(value)) { return value.map((item) => maskSensitiveDebugValue(item, parentKey)); } if (!value || typeof value !== "object") { return value; } return Object.fromEntries(Object.entries(value).map(([key, child]) => { const isSensitive = isSensitiveDebugKey(key) || (key === "value" && /uiaasHeader|uiassHeader/i.test(parentKey)); return [key, isSensitive ? formatHiddenDebugValue(child) : maskSensitiveDebugValue(child, key)]; })); } function isSensitiveDebugKey(key) { return /authorization|cookie|password|secret|(^|[_-])token($|[_-])|token$/i.test(key); } function formatHiddenDebugValue(value) { if (value === null || value === undefined || value === "") { return "(missing)"; } return `(present, ${String(value).length} characters)`; } function formatMetadataBlock(metadata) { const entries = Object.entries(metadata); if (entries.length === 0) { return "(none)"; } return entries .map(([key, value]) => `${key}: ${value || "(none)"}`) .join("\n"); } function maskToken(token) { if (token.length <= 10) { return "(present)"; } return `${token.slice(0, 4)}...${token.slice(-4)}`; } return { logRequestStart, logRequestSuccess, logRequestFailure, appendBackgroundRequestLog, readResponseBodyForDebug, createDebugBody, headersToObject, formatHeaderBlock, formatDebugBody, maskSensitiveDebugValue, isSensitiveDebugKey, formatHiddenDebugValue, formatMetadataBlock, maskToken }; } // Source: src/features/debug/content-view.js function createDebugContentView({ DEBUG_PANEL_ID, MODAL_ID, debugState, formatDebugBody, formatHeaderBlock, formatMetadataBlock, createCloseIcon, environment }) { const { document } = environment; const categories = [ ["all", "All"], ["authentication", "Authentication"], ["opportunities", "Opportunities"], ["products", "Products"], ["ai", "AI"], ["consumption", "Consumption"], ["installBase", "Installed Base"], ["serviceRequests", "Service Requests"], ["actions", "Actions"], ["other", "Other"] ]; let selectedCategory = "all"; const expandedIds = new Set(); let returnFocus = null; function getRequestCategory(entry) { if (/^request(?:CurrentUser|TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication"; if (entry.name === "requestOpportunities") return "opportunities"; if (entry.name === "requestOpptyProducts") return "products"; if (["requestWlActions", "requestActionTeamMembers", "requestActionFindLookup", "requestMgmtAction"].includes(entry.name)) return "actions"; if (entry.name === "requestGenerateAiSummary") return "ai"; if (entry.name === "requestInstallBase") return "installBase"; if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestResourceUser", "requestSrCatalog", "requestSrTokenCatalog", "requestPostServiceRequest"].includes(entry.name)) return "serviceRequests"; if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption"; return "other"; } function getChronologicalRequests() { return [...debugState.requestLog].sort((a, b) => (new Date(a.startedAt).getTime() || 0) - (new Date(b.startedAt).getTime() || 0) || a.id - b.id); } function toggleDebugPanel() { const modal = document.getElementById(MODAL_ID); if (!modal || modal.hidden) { return; } const panel = document.getElementById(DEBUG_PANEL_ID); if (!panel) { return; } if (!panel.hidden) { closeDebugPanel(); return; } returnFocus = document.activeElement; panel.hidden = false; renderDebugPanel(); panel.querySelectorAll(".opportunities-extension-debug-close-button")[0]?.focus({ preventScroll: true }); } function closeDebugPanel() { const panel = document.getElementById(DEBUG_PANEL_ID); if (!panel) return; panel.hidden = true; if (returnFocus?.isConnected) returnFocus.focus({ preventScroll: true }); returnFocus = null; } function renderDebugPanel() { const panel = document.getElementById(DEBUG_PANEL_ID); if (!panel || panel.hidden) { return; } for (const item of panel.querySelectorAll("details[data-request-id]")) { const id = item.getAttribute("data-request-id"); if (item.open) expandedIds.add(id); else expandedIds.delete(id); } const focusedCategory = document.activeElement && document.activeElement.getAttribute("data-debug-category"); const focusedControl = document.activeElement && document.activeElement.getAttribute("data-debug-focus"); const listScrollTop = panel.querySelectorAll(".opportunities-extension-debug-list")[0]?.scrollTop || 0; const scrollTop = panel.scrollTop; panel.textContent = ""; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "true"); panel.setAttribute("aria-labelledby", `${DEBUG_PANEL_ID}-title`); const focusTargets = new Map(); const panelHeader = document.createElement("header"); panelHeader.className = "opportunities-extension-debug-panel-header"; const heading = document.createElement("div"); const title = document.createElement("h2"); title.id = `${DEBUG_PANEL_ID}-title`; title.textContent = "Debug Requests"; const summary = document.createElement("p"); summary.textContent = `${debugState.requestLog.length} request${debugState.requestLog.length === 1 ? "" : "s"} since modal opened.`; const running = debugState.requestLog.filter(entry => entry.status === "pending").length; if (running) summary.textContent += ` ${running} running.`; heading.append(title, summary); const visibilityButton = document.createElement("button"); visibilityButton.type = "button"; visibilityButton.className = "opportunities-extension-icon-button opportunities-extension-debug-visibility-button"; visibilityButton.setAttribute("data-debug-focus", "visibility"); focusTargets.set("visibility", visibilityButton); visibilityButton.setAttribute("aria-pressed", String(debugState.debugSecretsVisible)); visibilityButton.setAttribute("aria-label", debugState.debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values"); visibilityButton.title = debugState.debugSecretsVisible ? "Hide sensitive values" : "Show sensitive values"; visibilityButton.append(createDebugVisibilityIcon(debugState.debugSecretsVisible)); visibilityButton.addEventListener("click", () => { debugState.debugSecretsVisible = !debugState.debugSecretsVisible; renderDebugPanel(); }); const closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.className = "opportunities-extension-icon-button opportunities-extension-debug-close-button"; closeButton.title = "Close debug"; closeButton.setAttribute("aria-label", "Close debug"); closeButton.setAttribute("data-debug-focus", "close"); closeButton.append(createCloseIcon()); closeButton.addEventListener("click", closeDebugPanel); focusTargets.set("close", closeButton); const actions = document.createElement("div"); actions.className = "opportunities-extension-debug-actions"; actions.append(visibilityButton, closeButton); panelHeader.append(heading, actions); const entries = getChronologicalRequests(); const availableCategories = categories.filter(([key]) => key === "all" || entries.some(entry => getRequestCategory(entry) === key)); if (!availableCategories.some(([key]) => key === selectedCategory)) selectedCategory = "all"; const tabs = document.createElement("div"); tabs.className = "opportunities-extension-debug-tabs"; tabs.setAttribute("role", "tablist"); tabs.setAttribute("aria-label", "Request categories"); const tabButtons = []; availableCategories.forEach(([key, label], index) => { const categoryEntries = key === "all" ? entries : entries.filter(entry => getRequestCategory(entry) === key); const pending = categoryEntries.filter(entry => entry.status === "pending").length; const button = document.createElement("button"); button.type = "button"; button.id = `${DEBUG_PANEL_ID}-tab-${key}`; button.setAttribute("role", "tab"); button.setAttribute("data-debug-category", key); button.setAttribute("aria-selected", String(key === selectedCategory)); button.setAttribute("aria-controls", `${DEBUG_PANEL_ID}-requests`); button.tabIndex = key === selectedCategory ? 0 : -1; button.textContent = `${label} (${categoryEntries.length})${pending ? ` / ${pending} running` : ""}`; button.addEventListener("click", () => { selectedCategory = key; renderDebugPanel(); }); button.addEventListener("keydown", event => { let nextIndex; if (event.key === "ArrowRight") nextIndex = (index + 1) % availableCategories.length; else if (event.key === "ArrowLeft") nextIndex = (index + availableCategories.length - 1) % availableCategories.length; else if (event.key === "Home") nextIndex = 0; else if (event.key === "End") nextIndex = availableCategories.length - 1; else return; event.preventDefault(); selectedCategory = availableCategories[nextIndex][0]; renderDebugPanel(); }); tabs.append(button); tabButtons.push(button); }); const list = document.createElement("div"); list.className = "opportunities-extension-debug-list"; list.id = `${DEBUG_PANEL_ID}-requests`; list.setAttribute("role", "tabpanel"); list.setAttribute("aria-labelledby", `${DEBUG_PANEL_ID}-tab-${selectedCategory}`); list.tabIndex = 0; list.setAttribute("data-debug-focus", "list"); focusTargets.set("list", list); const summaries = []; if (debugState.requestLog.length === 0) { const empty = document.createElement("div"); empty.className = "opportunities-extension-debug-empty"; empty.textContent = "No requests recorded yet."; list.append(empty); } entries.filter(entry => selectedCategory === "all" || getRequestCategory(entry) === selectedCategory).forEach((entry) => { const item = document.createElement("details"); item.className = `opportunities-extension-debug-item opportunities-extension-debug-item-${entry.status}`; item.setAttribute("data-request-id", String(entry.id)); item.open = expandedIds.has(String(entry.id)); const heading = document.createElement("summary"); heading.className = "opportunities-extension-debug-heading"; heading.setAttribute("data-debug-focus", `request-${entry.id}`); focusTargets.set(`request-${entry.id}`, heading); summaries.push(heading); const name = document.createElement("strong"); name.textContent = `${entry.id}. ${entry.name}`; if (entry.name === "requestOpptyProducts" && entry.metadata.optyNumber) { name.textContent += ` (${entry.metadata.optyNumber}, offset ${entry.metadata.offset})`; } const httpStatus = document.createElement("span"); httpStatus.className = "opportunities-extension-debug-http-status"; httpStatus.textContent = entry.httpStatus ? String(entry.httpStatus) : "pending"; httpStatus.setAttribute("data-http-class", entry.httpStatus ? String(Math.floor(entry.httpStatus / 100)) : "pending"); const status = document.createElement("span"); status.className = "opportunities-extension-debug-status"; status.textContent = entry.status === "pending" ? "running" : entry.status; const summaryMeta = document.createElement("span"); summaryMeta.className = "opportunities-extension-debug-summary-meta"; const startedTime = document.createElement("time"); startedTime.className = "opportunities-extension-debug-time"; startedTime.textContent = formatDebugTime(entry.startedAt); startedTime.title = `Started: ${entry.startedAt.toLocaleString()}`; const method = document.createElement("span"); method.className = "opportunities-extension-debug-method"; method.textContent = entry.method || "GET"; method.setAttribute("data-method", method.textContent.toUpperCase()); const duration = document.createElement("span"); duration.className = "opportunities-extension-debug-duration"; const hasDuration = typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs) && entry.durationMs >= 0; duration.textContent = hasDuration ? entry.durationMs < 1000 ? `${Math.round(entry.durationMs)} ms` : `${(entry.durationMs / 1000).toFixed(2)} s` : entry.status === "pending" ? "In progress" : "N/D"; duration.title = hasDuration ? `Request duration: ${entry.durationMs} ms` : "Request duration not yet available"; summaryMeta.append(startedTime, method, httpStatus, status, duration); heading.append(name, summaryMeta); const lines = [ `${entry.method} ${entry.url}`, `Started: ${formatDebugTime(entry.startedAt)}`, entry.completedAt ? `Completed: ${formatDebugTime(entry.completedAt)} (${entry.durationMs}ms)` : "Completed: pending", entry.httpStatus ? `HTTP: ${entry.httpStatus} ${entry.statusText}`.trim() : "", entry.error ? `Error: ${entry.error}` : "", `Response body truncated: ${entry.responseBodyTruncated ? "yes" : "no"}` ].filter(Boolean); const details = document.createElement("div"); details.className = "opportunities-extension-debug-details"; appendDebugBlock(details, "Request", lines.join("\n")); appendDebugBlock(details, "Request Metadata", formatMetadataBlock(entry.metadata)); appendDebugBlock(details, "Request Headers", formatHeaderBlock(entry.requestHeaders)); appendDebugBlock(details, "Request Body", formatDebugBody(entry.requestBody)); appendDebugBlock(details, "Response Headers", formatHeaderBlock(entry.responseHeaders)); appendDebugBlock(details, "Response Body", formatDebugBody(entry.responseBody)); item.append(heading, details); list.append(item); }); panel.append(panelHeader, tabs, list); panel.onkeydown = event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); closeDebugPanel(); } else if (event.key === "Tab") { const controls = [visibilityButton, closeButton, ...tabButtons.filter(button => button.tabIndex === 0), list, ...summaries]; const index = controls.indexOf(document.activeElement); if (index < 0 || (event.shiftKey && index === 0) || (!event.shiftKey && index === controls.length - 1)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus({ preventScroll: true }); } } }; if (focusedCategory) tabButtons.find(button => button.getAttribute("data-debug-category") === selectedCategory).focus({ preventScroll: true }); else if (focusedControl) focusTargets.get(focusedControl)?.focus({ preventScroll: true }); list.scrollTop = listScrollTop; panel.scrollTop = scrollTop; } function createDebugVisibilityIcon(secretsVisible) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const eye = document.createElementNS("http://www.w3.org/2000/svg", "path"); const pupil = document.createElementNS("http://www.w3.org/2000/svg", "circle"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); eye.setAttribute("d", "M2.5 12s3.5-6.5 9.5-6.5 9.5 6.5 9.5 6.5-3.5 6.5-9.5 6.5S2.5 12 2.5 12Z"); pupil.setAttribute("cx", "12"); pupil.setAttribute("cy", "12"); pupil.setAttribute("r", "2.75"); [eye, pupil].forEach((shape) => { shape.setAttribute("fill", "none"); shape.setAttribute("stroke", "currentColor"); shape.setAttribute("stroke-linecap", "round"); shape.setAttribute("stroke-linejoin", "round"); shape.setAttribute("stroke-width", "1.7"); }); svg.append(eye, pupil); if (secretsVisible) { const slash = document.createElementNS("http://www.w3.org/2000/svg", "path"); slash.setAttribute("d", "M4 4l16 16"); slash.setAttribute("fill", "none"); slash.setAttribute("stroke", "currentColor"); slash.setAttribute("stroke-linecap", "round"); slash.setAttribute("stroke-width", "1.9"); svg.append(slash); } return svg; } function appendDebugBlock(container, label, value) { const block = document.createElement("section"); block.className = "opportunities-extension-debug-block"; block.setAttribute("data-direction", label.startsWith("Response") ? "response" : "request"); const heading = document.createElement("h3"); const content = document.createElement("pre"); heading.textContent = label; appendJsonTokens(content, value); block.append(heading, content); container.append(block); } function appendJsonTokens(content, value) { const text = String(value ?? ""); try { JSON.parse(text); } catch { content.textContent = text; return; } // Highlight only already-masked display text; never interpret response content as HTML. const tokens = [...text.matchAll(/"(?:[^"\\]|\\.)*"\s*:|"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\b(?:true|false|null)\b/g)]; let offset = 0; const append = (textValue, kind) => { if (!textValue) return; const span = document.createElement("span"); span.textContent = textValue; if (kind) span.className = `opportunities-extension-debug-json-${kind}`; content.append(span); }; tokens.forEach(token => { append(text.slice(offset, token.index)); const value = token[0]; const kind = value.startsWith('"') ? value.endsWith(":") ? "key" : "string" : /^(true|false|null)$/.test(value) ? "literal" : "number"; append(value, kind); offset = token.index + value.length; }); append(text.slice(offset)); } function formatDebugTime(date) { return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" }); } return { toggleDebugPanel, closeDebugPanel, renderDebugPanel, createDebugVisibilityIcon, appendDebugBlock, formatDebugTime, getRequestCategory, getChronologicalRequests }; } // Source: src/content/main.js /** Preserve the existing document_idle initialization and observer behavior. */ function startContentApplication({ environment, page, styles, products }) { if (!page.isAllowedPage()) return; styles.ensureExtensionStyles(); products.ensureProductsBackgroundMessageHandler(); if (page.insertTile()) return; const observer = new environment.MutationObserver(() => page.insertTile()); observer.observe(environment.document.documentElement, { childList: true, subtree: true }); } // Source: src/content/application.js function createContentApplication(overrides = {}) { const environment = { document: globalThis.document, window: globalThis.window, navigator: globalThis.navigator, get localStorage() { return globalThis.localStorage; }, fetch: globalThis.fetch, browser: globalThis.browser, chrome: globalThis.chrome, MutationObserver: globalThis.MutationObserver, Node: globalThis.Node, requestAnimationFrame: globalThis.requestAnimationFrame, performance: globalThis.performance, cancelAnimationFrame: globalThis.cancelAnimationFrame, setTimeout: globalThis.setTimeout, clearTimeout: globalThis.clearTimeout, matchMedia: globalThis.matchMedia, ...overrides }; const messageTypes = createMessageTypes(); const currentUser = createCurrentUser({ messageTypes, sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args) }); const contracts = createContentContracts(); const searchableSelect = createSearchableSelect({ environment }); const authSessionState = createAuthSessionState(contracts); const authSalesCommonState = createAuthSalesCommonState(); const opportunitiesQueryState = createOpportunitiesQueryState(); const opportunitiesTableState = createOpportunitiesTableState(); const opportunitiesFilterState = createOpportunitiesFilterState(contracts); const preferencesState = createPreferencesState(contracts); const activityCache = createActivityCache({ authSessionState, preferencesState, environment }); const productsContentState = createProductsContentState(); const aiSummaryState = createAiSummaryState(); const consumptionPlansState = createConsumptionPlansState(); const consumptionTrendState = createConsumptionTrendState(); const opportunitiesDashboardState = createOpportunitiesDashboardState(contracts); const debugState = createDebugState(); const columns = createOpportunitiesColumns({ formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args), formatRevenue: (...args) => opportunitiesFormatters.formatRevenue(...args), formatWinProbability: (...args) => opportunitiesFormatters.formatWinProbability(...args), getNestedValue: (...args) => opportunitiesFormatters.getNestedValue(...args), getOwnerName: (...args) => opportunitiesFormatters.getOwnerName(...args) }); const contentFusionPage = createContentFusionPage({ ADD_ICON_SELECTOR: contracts.ADD_ICON_SELECTOR, ALLOWED_PATHS: contracts.ALLOWED_PATHS, SALES_GROUP_SELECTOR: contracts.SALES_GROUP_SELECTOR, TILE_ID: contracts.TILE_ID, TILE_LABEL_ID: contracts.TILE_LABEL_ID, appendPath: (...args) => svgPaths.appendPath(...args), openOpportunitiesModal: (...args) => contentModal.openOpportunitiesModal(...args), environment }); const contentModal = createContentModal({ prepareSrSession: () => srSessionPreparation.ensure(), closeActionForm: (...args) => crmSrForm.close(...args) || workloadActionForm.close(...args), createActivityFilterControls: (...args) => activityFilters.createControls(...args), closeCrmSrPopover: () => crmSrPopover.close(), ensureCurrentUser: () => currentUser.ensure(), ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID, AUTH_STATUS: contracts.AUTH_STATUS, CUSTOMER_FILTER_BUTTON_ID: contracts.CUSTOMER_FILTER_BUTTON_ID, CUSTOMER_FILTER_LIST_ID: contracts.CUSTOMER_FILTER_LIST_ID, CUSTOMER_FILTER_PANEL_ID: contracts.CUSTOMER_FILTER_PANEL_ID, CUSTOMER_FILTER_SEARCH_ID: contracts.CUSTOMER_FILTER_SEARCH_ID, CUSTOMER_FILTER_SELECT_ALL_ID: contracts.CUSTOMER_FILTER_SELECT_ALL_ID, DEBUG_PANEL_ID: contracts.DEBUG_PANEL_ID, ESCAPE_LISTENER_KEY: contracts.ESCAPE_LISTENER_KEY, MODAL_ID: contracts.MODAL_ID, OPPORTUNITIES_TABLE_ID: contracts.OPPORTUNITIES_TABLE_ID, OWNER_FILTER_BUTTON_ID: contracts.OWNER_FILTER_BUTTON_ID, OWNER_FILTER_LIST_ID: contracts.OWNER_FILTER_LIST_ID, OWNER_FILTER_PANEL_ID: contracts.OWNER_FILTER_PANEL_ID, OWNER_FILTER_SEARCH_ID: contracts.OWNER_FILTER_SEARCH_ID, OWNER_FILTER_SELECT_ALL_ID: contracts.OWNER_FILTER_SELECT_ALL_ID, PERIOD_OPTIONS: contracts.PERIOD_OPTIONS, PREFERENCES_MODAL_ID: contracts.PREFERENCES_MODAL_ID, PRODUCTS_CACHE_TIMESTAMP_ID: contracts.PRODUCTS_CACHE_TIMESTAMP_ID, PRODUCTS_PROGRESS_ID: contracts.PRODUCTS_PROGRESS_ID, STAGE_DASHBOARD_ID: contracts.STAGE_DASHBOARD_ID, STAGE_FILTERS_ID: contracts.STAGE_FILTERS_ID, STATUS_FILTERS_ID: contracts.STATUS_FILTERS_ID, TABLE_SEARCH_ID: contracts.TABLE_SEARCH_ID, applyTheme: (...args) => contentTheme.applyTheme(...args), authSessionState, closeAccountPlansModal: (...args) => consumptionPlansView.closeAccountPlansModal(...args), closeInstallBaseModal: (...args) => installBaseView.closeInstallBaseModal(...args), closeServiceRequestsModal: (...args) => serviceRequestsView.closeServiceRequestsModal(...args), closeCustomerFilterPanel: (...args) => opportunitiesFilters.closeCustomerFilterPanel(...args), closeOwnerFilterPanel: (...args) => opportunitiesFilters.closeOwnerFilterPanel(...args), closePreferencesModal: (...args) => preferencesView.closePreferencesModal(...args), createSettingsIcon: (...args) => contentIcons.createSettingsIcon(...args), createRefreshAllButton: (...args) => refreshAll.createButton(...args), debugState, ensureExtensionStyles: (...args) => contentStyles.ensureExtensionStyles(...args), getCustomerOptions: (...args) => opportunitiesFormatters.getCustomerOptions(...args), getOwnerOptions: (...args) => opportunitiesFormatters.getOwnerOptions(...args), getSavedOpportunityTypeValues: (...args) => preferencesStorage.getSavedOpportunityTypeValues(...args), getSavedOpptyProductsCacheHours: (...args) => preferencesStorage.getSavedOpptyProductsCacheHours(...args), getSavedTheme: (...args) => contentTheme.getSavedTheme(...args), openPreferencesModal: (...args) => preferencesView.openPreferencesModal(...args), opportunitiesFilterState, opportunitiesQueryState, preferencesState, renderCustomerFilter: (...args) => opportunitiesFilters.renderCustomerFilter(...args), renderCustomerFilterList: (...args) => opportunitiesFilters.renderCustomerFilterList(...args), renderOpportunitiesTable: (...args) => opportunitiesTable.renderOpportunitiesTable(...args), renderOwnerFilter: (...args) => opportunitiesFilters.renderOwnerFilter(...args), renderOwnerFilterList: (...args) => opportunitiesFilters.renderOwnerFilterList(...args), renderStageFilterButtons: (...args) => opportunitiesFilters.renderStageFilterButtons(...args), renderStatusFilterButtons: (...args) => opportunitiesFilters.renderStatusFilterButtons(...args), requestOpportunitiesForPeriod: (...args) => opportunitiesController.requestOpportunitiesForPeriod(...args), requestTokenRelayOnce: (...args) => authContentSession.requestTokenRelayOnce(...args), resetCustomerAndOwnerFiltersForPeriod: (...args) => opportunitiesFilters.resetCustomerAndOwnerFiltersForPeriod(...args), resetOpportunitiesTable: (...args) => opportunitiesController.resetOpportunitiesTable(...args), restoreFilterPreferences: (...args) => preferencesStorage.restoreFilterPreferences(...args), saveFilterPreferences: (...args) => preferencesStorage.saveFilterPreferences(...args), toggleCustomerFilterPanel: (...args) => opportunitiesFilters.toggleCustomerFilterPanel(...args), toggleDebugPanel: (...args) => debugContentView.toggleDebugPanel(...args), toggleOwnerFilterPanel: (...args) => opportunitiesFilters.toggleOwnerFilterPanel(...args), environment }); const contentTheme = createContentTheme({ THEME_STORAGE_KEY: contracts.THEME_STORAGE_KEY, createThemeIcon: (...args) => contentIcons.createThemeIcon(...args), environment }); const contentIcons = createContentIcons({ appendPath: (...args) => svgPaths.appendPath(...args), environment }); const svgPaths = createSvgPaths({ environment }); const contentStyles = createContentStyles({ STYLE_ID: contracts.STYLE_ID, redwoodStyles, environment }); const authContentSession = createAuthContentSession({ messageTypes, AUTH_STATUS: contracts.AUTH_STATUS, appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), authSessionState, requestTokenRelay: (...args) => authTokenRelay.requestTokenRelay(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), updateAuthBadge: (...args) => contentModal.updateAuthBadge(...args) }); const authSalesCommon = createAuthSalesCommon({ TOKEN_RELAY_URL: contracts.TOKEN_RELAY_URL, authSalesCommonState, requestTokenRelay: (...args) => authTokenRelay.requestTokenRelay(...args) }); const authTokenRelay = createAuthTokenRelay({ authSessionState, TOKEN_RELAY_URL: contracts.TOKEN_RELAY_URL, createDebugBody: (...args) => debugContentLog.createDebugBody(...args), getXsrfToken: (...args) => authXsrfContent.getXsrfToken(...args), logRequestFailure: (...args) => debugContentLog.logRequestFailure(...args), logRequestStart: (...args) => debugContentLog.logRequestStart(...args), logRequestSuccess: (...args) => debugContentLog.logRequestSuccess(...args), maskToken: (...args) => debugContentLog.maskToken(...args), readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args), refreshXsrfCookie: (...args) => platformContentMessages.refreshXsrfCookie(...args), renderDebugPanel: (...args) => debugContentView.renderDebugPanel(...args), environment }); const authXsrfContent = createAuthXsrfContent({ getXsrfTokenFromExtensionCookies: (...args) => platformContentMessages.getXsrfTokenFromExtensionCookies(...args), environment }); const platformContentMessages = createPlatformContentMessages({ messageTypes, environment }); const opportunitiesController = createOpportunitiesController({ ensureCurrentUser: () => currentUser.ensure(), resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args), STAGE_OPTIONS: contracts.STAGE_OPTIONS, STATUS_OPTIONS: contracts.STATUS_OPTIONS, authSessionState, consumptionPlansState, consumptionTrendState, getFiscalPeriodRange: (...args) => opportunitiesFiscalPeriods.getFiscalPeriodRange(...args), opportunitiesDashboardState, opportunitiesFilterState, opportunitiesQueryState, opportunitiesTableState, preferencesState, productsContentState, renderProductsProgress: (...args) => productsContentJobs.renderProductsProgress(...args), requestOpportunitiesPage: (...args) => fusionOpportunities.requestOpportunitiesPage(...args), resetCrmServiceRequests: () => opportunityCrmServiceRequests.reset(), requestOpptyProductsInBackground: (...args) => productsContentJobs.requestOpptyProductsInBackground(...args), requestTokenRelayOnce: (...args) => authContentSession.requestTokenRelayOnce(...args), setOpportunitiesTableState: (...args) => opportunitiesTable.setOpportunitiesTableState(...args) }); const fusionOpportunities = createFusionOpportunities({ OPPORTUNITIES_PAGE_LIMIT: contracts.OPPORTUNITIES_PAGE_LIMIT, OPPORTUNITIES_QUERY_URL: contracts.OPPORTUNITIES_QUERY_URL, isCurrentPeriodRequest: (...args) => opportunitiesController.isCurrentPeriodRequest(...args), logRequestFailure: (...args) => debugContentLog.logRequestFailure(...args), logRequestStart: (...args) => debugContentLog.logRequestStart(...args), logRequestSuccess: (...args) => debugContentLog.logRequestSuccess(...args), maskToken: (...args) => debugContentLog.maskToken(...args), getOpportunityTypeValues: () => preferencesState.selectedOpportunityTypeValues, readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args), environment }); const opportunitiesFiscalPeriods = createOpportunitiesFiscalPeriods(); const fusionCrmServiceRequests = createFusionCrmServiceRequests({ isCurrentPeriodRequest: (...args) => opportunitiesController.isCurrentPeriodRequest(...args), logRequestStart: (...args) => debugContentLog.logRequestStart(...args), logRequestSuccess: (...args) => debugContentLog.logRequestSuccess(...args), logRequestFailure: (...args) => debugContentLog.logRequestFailure(...args), readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args), maskToken: (...args) => debugContentLog.maskToken(...args), environment }); const internalSrSession = createInternalSrSession({ environment, query: async srId => { const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); return result; } }); const crmSrPopover = createCrmSrPopover({ requestDetails: srId => internalSrSession.request(srId), MODAL_ID: contracts.MODAL_ID, createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args), createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment }); const opportunityBadgeContent = createOpportunityBadgeContent({ environment }); async function requestSrFormLookup(type, fields = {}) { const result = await platformContentMessages.sendRuntimeMessage({ type, ...fields }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to load Service Request options."); return result.items; } const crmSrForm = createCrmSrForm({ requestSave: async fields => { const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostServiceRequest, ...fields, emailAddress: currentUser.getUserId() }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to save Service Request."); return result; }, notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args), getOpportunity: number => opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)), formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), requestLanguage: () => requestSrFormLookup(messageTypes.requestSrLookupLanguage), requestPlatform: () => requestSrFormLookup(messageTypes.requestSrLookupTechPlatform), requestServices: () => requestSrFormLookup(messageTypes.requestSrLookupOpportunityBusiness, { emailAddress: currentUser.getUserId() }), searchableSelect, MODAL_ID: contracts.MODAL_ID, environment, allowsCrmActivities: () => currentUser.allowsCrmActivities(), isMissing: number => opportunityCrmServiceRequests.getSummary(number)?.state === "missing", createCloseIcon: () => contentIcons.createCloseIcon(), requestPillars: () => requestSrFormLookup(messageTypes.requestSrLookupPrimaryPillar) }); const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({ createSrButton: number => crmSrForm.createButton(number), closeSrForm: () => crmSrForm.close(false), setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args), activityCache, reportProgress: (...args) => activityProgress.update("sr", ...args), allowsCrmActivities: () => currentUser.allowsCrmActivities(), bindPopover: (...args) => crmSrPopover.bind(...args), closePopover: (...args) => crmSrPopover.close(...args), requestCrmServiceRequest: (...args) => fusionCrmServiceRequests.requestCrmServiceRequest(...args), isCurrentPeriodRequest: (...args) => opportunitiesController.isCurrentPeriodRequest(...args), environment }); const opportunitiesTable = createOpportunitiesTable({ renderActivityFilters: () => activityFilters.render(), closeCrmSrPopover: (...args) => crmSrPopover.close(...args), createWorkloadActionsBadge: (...args) => opportunityWorkloadActions.createBadge(...args), ACCOUNT_DETAIL_URL: contracts.ACCOUNT_DETAIL_URL, OPPORTUNITIES_COLUMNS: columns, OPPORTUNITIES_TABLE_ID: contracts.OPPORTUNITIES_TABLE_ID, OPPORTUNITY_DETAIL_URL: contracts.OPPORTUNITY_DETAIL_URL, createCloudConsumptionButton: (...args) => consumptionPlansAction.createCloudConsumptionButton(...args), createInstallBaseButton: (...args) => installBaseAction.createInstallBaseButton(...args), createServiceRequestsButton: (...args) => serviceRequestsAction.createServiceRequestsButton(...args), createCopyOpportunityButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args), createDetailLink: (...args) => opportunitiesLinks.createDetailLink(...args), createProductsCountButton: (...args) => productsDetails.createProductsCountButton(...args), createProductsRampBadge: (...args) => productsDetails.createProductsRampBadge(...args), createCrmServiceRequestBadge: (...args) => opportunityCrmServiceRequests.createBadge(...args), createProductsDetailRow: (...args) => productsDetails.createProductsDetailRow(...args), createStageBadge: (...args) => opportunitiesLinks.createStageBadge(...args), createStatusBadge: (...args) => opportunitiesLinks.createStatusBadge(...args), displayOpportunityValue: (...args) => opportunitiesFormatters.displayOpportunityValue(...args), getNestedValue: (...args) => opportunitiesFormatters.getNestedValue(...args), getSortedOpportunities: (...args) => opportunitiesFilters.getSortedOpportunities(...args), opportunitiesTableState, productsContentState, renderCustomerFilter: (...args) => opportunitiesFilters.renderCustomerFilter(...args), renderOwnerFilter: (...args) => opportunitiesFilters.renderOwnerFilter(...args), renderStageDashboard: (...args) => opportunitiesDashboard.renderStageDashboard(...args), resetCustomerFilter: (...args) => opportunitiesFilters.resetCustomerFilter(...args), resetOwnerFilter: (...args) => opportunitiesFilters.resetOwnerFilter(...args), environment }); const opportunitiesDashboard = createOpportunitiesDashboard({ STAGE_DASHBOARD_ID: contracts.STAGE_DASHBOARD_ID, STAGE_OPTIONS: contracts.STAGE_OPTIONS, getNestedValue: (...args) => opportunitiesFormatters.getNestedValue(...args), opportunitiesDashboardState, environment }); const opportunitiesFilters = createOpportunitiesFilters({ matchesActivities: item => activityFilters.matches(item), CUSTOMER_FILTER_BUTTON_ID: contracts.CUSTOMER_FILTER_BUTTON_ID, CUSTOMER_FILTER_LIST_ID: contracts.CUSTOMER_FILTER_LIST_ID, CUSTOMER_FILTER_PANEL_ID: contracts.CUSTOMER_FILTER_PANEL_ID, CUSTOMER_FILTER_SEARCH_ID: contracts.CUSTOMER_FILTER_SEARCH_ID, CUSTOMER_FILTER_SELECT_ALL_ID: contracts.CUSTOMER_FILTER_SELECT_ALL_ID, OPPORTUNITIES_COLUMNS: columns, OWNER_FILTER_BUTTON_ID: contracts.OWNER_FILTER_BUTTON_ID, OWNER_FILTER_LIST_ID: contracts.OWNER_FILTER_LIST_ID, OWNER_FILTER_PANEL_ID: contracts.OWNER_FILTER_PANEL_ID, OWNER_FILTER_SEARCH_ID: contracts.OWNER_FILTER_SEARCH_ID, OWNER_FILTER_SELECT_ALL_ID: contracts.OWNER_FILTER_SELECT_ALL_ID, STAGE_FILTERS_ID: contracts.STAGE_FILTERS_ID, STAGE_OPTIONS: contracts.STAGE_OPTIONS, STATUS_FILTERS_ID: contracts.STATUS_FILTERS_ID, STATUS_OPTIONS: contracts.STATUS_OPTIONS, getCustomerFilterSummary: (...args) => opportunitiesFormatters.getCustomerFilterSummary(...args), getCustomerKey: (...args) => opportunitiesFormatters.getCustomerKey(...args), getCustomerOptions: (...args) => opportunitiesFormatters.getCustomerOptions(...args), getOwnerKey: (...args) => opportunitiesFormatters.getOwnerKey(...args), getOwnerOptions: (...args) => opportunitiesFormatters.getOwnerOptions(...args), opportunitiesFilterState, opportunitiesTableState, renderOpportunitiesTable: (...args) => opportunitiesTable.renderOpportunitiesTable(...args), saveFilterPreferences: (...args) => preferencesStorage.saveFilterPreferences(...args), stringifySearchValue: (...args) => opportunitiesFormatters.stringifySearchValue(...args), environment }); const opportunitiesFormatters = createOpportunitiesFormatters({ opportunitiesTableState }); const opportunitiesLinks = createOpportunitiesLinks({ MODAL_ID: contracts.MODAL_ID, environment }); const preferencesStorage = createPreferencesStorage({ DEFAULT_OPPORTUNITY_TYPE_VALUES: contracts.DEFAULT_OPPORTUNITY_TYPE_VALUES, DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS: contracts.DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS, FILTER_PREFERENCES_STORAGE_KEY: contracts.FILTER_PREFERENCES_STORAGE_KEY, MODAL_ID: contracts.MODAL_ID, OPPORTUNITY_TYPE_OPTIONS: contracts.OPPORTUNITY_TYPE_OPTIONS, OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY: contracts.OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, OPPTY_PRODUCTS_CACHE_OPTIONS: contracts.OPPTY_PRODUCTS_CACHE_OPTIONS, PERIOD_OPTIONS: contracts.PERIOD_OPTIONS, STAGE_OPTIONS: contracts.STAGE_OPTIONS, STATUS_OPTIONS: contracts.STATUS_OPTIONS, opportunitiesFilterState, environment }); const preferencesView = createPreferencesView({ DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS: contracts.DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS, MODAL_ID: contracts.MODAL_ID, OPPORTUNITY_TYPE_OPTIONS: contracts.OPPORTUNITY_TYPE_OPTIONS, OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY: contracts.OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, OPPTY_PRODUCTS_CACHE_OPTIONS: contracts.OPPTY_PRODUCTS_CACHE_OPTIONS, PERIOD_OPTIONS: contracts.PERIOD_OPTIONS, PREFERENCES_MODAL_ID: contracts.PREFERENCES_MODAL_ID, createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), getSavedOpportunityTypeValues: (...args) => preferencesStorage.getSavedOpportunityTypeValues(...args), getSavedOpptyProductsCacheHours: (...args) => preferencesStorage.getSavedOpptyProductsCacheHours(...args), preferencesState, requestOpportunitiesForPeriod: (...args) => opportunitiesController.requestOpportunitiesForPeriod(...args), environment }); const refreshAll = createRefreshAll({ createRefreshIcon: () => contentIcons.createRefreshIcon(), messageTypes, activityCache, opportunitiesQueryState, productsContentState, consumptionPlansState, consumptionTrendState, aiSummaryState, environment, sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), reload: (...args) => opportunitiesController.requestOpportunitiesForPeriod(...args), onFailure: error => opportunitiesTable.setOpportunitiesTableState({ status: "error", message: error.message || "Unable to refresh cached data." }), notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args), resetRelatedData: () => { opportunityCrmServiceRequests.reset(); opportunityWorkloadActions.reset(); consumptionPlansView.closeAccountPlansModal(); installBaseView.closeInstallBaseModal(); serviceRequestsView.closeServiceRequestsModal(); installBaseAction.resetRequests(); serviceRequestsAction.resetRequests(); ratecardController.resetRequests(); consumptionPlanDetailController.resetRequests(); productsContentJobs.renderProductsProgress(); opportunitiesTable.setOpportunitiesTableState({ status: "loading", message: "Refreshing data..." }); } }); const activityProgress = createActivityProgress({ MODAL_ID: contracts.MODAL_ID, environment }); const srSessionPreparation = createSrSessionPreparation({ ensureCurrentUser: () => currentUser.ensure(), allowsCrmActivities: () => currentUser.allowsCrmActivities(), sendRequest: () => platformContentMessages.sendRuntimeMessage({ type: messageTypes.prepareSrSession }), reportProgress: state => activityProgress.update("session", state), appendLog: entries => debugContentLog.appendBackgroundRequestLog(entries) }); const activityFilters = createActivityFilters({ setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args), allowsCrmActivities: () => currentUser.allowsCrmActivities(), getSrSummary: number => opportunityCrmServiceRequests.getSummary(number), getActionSummary: number => opportunityWorkloadActions.getSummary(number), renderTable: () => opportunitiesTable.renderOpportunitiesTable(), environment }); const productActivities = createProductActivities({ productsContentState, opportunitiesQueryState, loadCrm: (...args) => opportunityCrmServiceRequests.load(...args), loadActions: (...args) => opportunityWorkloadActions.loadProducts(...args), blockActions: products => opportunityWorkloadActions.blockProducts(products), render: () => { productsContentJobs.renderProductsProgress(); productsContentJobs.queueProductsTableRender(); } }); const productsContentJobs = createProductsContentJobs({ beginProductActivities: (...args) => productActivities.begin(...args), completeProductActivities: (...args) => productActivities.complete(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), messageTypes, PRODUCTS_CACHE_TIMESTAMP_ID: contracts.PRODUCTS_CACHE_TIMESTAMP_ID, PRODUCTS_MESSAGE_LISTENER_KEY: contracts.PRODUCTS_MESSAGE_LISTENER_KEY, PRODUCTS_PROGRESS_ID: contracts.PRODUCTS_PROGRESS_ID, preferencesState, productsContentState, renderOpportunitiesTable: (...args) => opportunitiesTable.renderOpportunitiesTable(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), environment }); const workloadActionOptions = createWorkloadActionOptions({ requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), messageTypes }); const workloadActionForm = createWorkloadActionForm({ searchableSelect, onActionSaved: (...args) => opportunityWorkloadActions.applySavedAction(...args), saveAction: (...args) => workloadActionOptions.save(...args), notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args), getCurrentUserId: () => currentUser.getUserId(), requestTeamMembers: (...args) => workloadActionOptions.requestTeamMembers(...args), requestLookup: (...args) => workloadActionOptions.requestLookup(...args), getProducts: number => productsContentState.opptyProductsByNumber.get(String(number))?.items || [], getOpportunity: number => opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)), formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), MODAL_ID: contracts.MODAL_ID, allowsCrmActivities: () => currentUser.allowsCrmActivities(), isMissingAction: number => opportunityWorkloadActions.getSummary(number)?.state === "missing", createCloseIcon: () => contentIcons.createCloseIcon(), environment }); const opportunityWorkloadActions = createOpportunityWorkloadActions({ setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args), createActionButton: number => workloadActionForm.createButton(number), closeActionForm: (...args) => workloadActionForm.close(...args), activityCache, reportProgress: (...args) => activityProgress.update("actions", ...args), allowsCrmActivities: () => currentUser.allowsCrmActivities(), authSessionState, requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), queueProductsTableRender: () => productsContentJobs.queueProductsTableRender(), productsContentState, messageTypes, environment }); const productsDetails = createProductsDetails({ setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args), OPPORTUNITIES_COLUMNS: columns, createAiSummaryAlert: (...args) => aiSummaryContent.createAiSummaryAlert(...args), formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args), productsContentState, renderOpportunitiesTable: (...args) => opportunitiesTable.renderOpportunitiesTable(...args), requestGenerateAiSummary: (...args) => aiSummaryContent.requestGenerateAiSummary(...args), environment }); const aiSummaryContent = createAiSummaryContent({ aiSummaryState, invalidateSalesCommonToken: (...args) => authSalesCommon.invalidateSalesCommonToken(...args), logRequestFailure: (...args) => debugContentLog.logRequestFailure(...args), logRequestStart: (...args) => debugContentLog.logRequestStart(...args), logRequestSuccess: (...args) => debugContentLog.logRequestSuccess(...args), readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args), requestTokenRelaySalesCommon: (...args) => authSalesCommon.requestTokenRelaySalesCommon(...args), environment }); const customerTableSearch = createCustomerTableSearch({ environment }); const installBaseView = createInstallBaseView({ createSearch: (...args) => customerTableSearch.createSearch(...args), MODAL_ID: contracts.MODAL_ID, createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args), environment }); const installBaseAction = createInstallBaseAction({ messageTypes, requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), openInstallBaseModal: (...args) => installBaseView.openInstallBaseModal(...args), environment }); const serviceRequestsView = createServiceRequestsView({ createSearch: (...args) => customerTableSearch.createSearch(...args), MODAL_ID: contracts.MODAL_ID, createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args), environment }); const serviceRequestsAction = createServiceRequestsAction({ messageTypes, requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), openServiceRequestsModal: (...args) => serviceRequestsView.openServiceRequestsModal(...args), environment }); const consumptionPlansAction = createConsumptionPlansAction({ messageTypes, appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), consumptionPlansState, getNestedValue: (...args) => opportunitiesFormatters.getNestedValue(...args), openAccountPlansModal: (...args) => consumptionPlansView.openAccountPlansModal(...args), renderOpportunitiesTable: (...args) => opportunitiesTable.renderOpportunitiesTable(...args), requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), showCopyConfirmation: (...args) => opportunitiesLinks.showCopyConfirmation(...args), environment }); const consumptionPlanDetailView = createConsumptionPlanDetailView({ createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment }); const consumptionPlanDetailController = createConsumptionPlanDetailController({ messageTypes, requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), renderPlanDetailBadges: (...args) => consumptionPlanDetailView.renderPlanDetailBadges(...args), environment }); const ratecardModel = createRatecardModel(); const consumptionHeaderView = createConsumptionHeaderView({ createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args), environment }); const ratecardView = createRatecardView({ ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID, columns: ratecardModel.columns, formatValue: (...args) => ratecardModel.formatValue(...args), selectItems: (...args) => ratecardModel.selectItems(...args), createPlanDetails: (...args) => consumptionHeaderView.createDetails(...args), createConsumptionHeader: (...args) => consumptionHeaderView.createHeader(...args), environment }); const ratecardController = createRatecardController({ messageTypes, requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), openRatecardDrawer: (...args) => ratecardView.openRatecardDrawer(...args), closePlanDetailPopover: (...args) => consumptionPlanDetailView.closePlanDetailPopover(...args), environment }); const consumptionPlansView = createConsumptionPlansView({ ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID, DEFAULT_CONSUMPTION_TREND_PERIOD: contracts.DEFAULT_CONSUMPTION_TREND_PERIOD, MODAL_ID: contracts.MODAL_ID, createConsumptionHeader: (...args) => consumptionHeaderView.createHeader(...args), createConsumptionTrendSection: (...args) => consumptionTrendController.createConsumptionTrendSection(...args), loadPlanDetailForPanel: (...args) => consumptionPlanDetailController.loadPlanDetailForPanel(...args), closePlanDetailPopover: (...args) => consumptionPlanDetailView.closePlanDetailPopover(...args), createRatecardButton: (...args) => ratecardController.createRatecardButton(...args), closeRatecardDrawer: (...args) => ratecardView.closeRatecardDrawer(...args), createSvgElement: (...args) => consumptionChart.createSvgElement(...args), createPlanDetails: (...args) => consumptionHeaderView.createDetails(...args), getNestedValue: (...args) => opportunitiesFormatters.getNestedValue(...args), loadConsumptionTrendForPanel: (...args) => consumptionTrendController.loadConsumptionTrendForPanel(...args), environment }); const consumptionTrendController = createConsumptionTrendController({ createPlanDetailSection: (...args) => consumptionPlanDetailController.createPlanDetailSection(...args), messageTypes, CONSUMPTION_TREND_PERIODS: contracts.CONSUMPTION_TREND_PERIODS, DEFAULT_CONSUMPTION_TREND_PERIOD: contracts.DEFAULT_CONSUMPTION_TREND_PERIOD, appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), consumptionTrendState, renderConsumptionTrendChart: (...args) => consumptionChart.renderConsumptionTrendChart(...args), requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), environment }); const consumptionChart = createConsumptionChart({ CONSUMPTION_TREND_PERIODS: contracts.CONSUMPTION_TREND_PERIODS, CONSUMPTION_TREND_SERIES: contracts.CONSUMPTION_TREND_SERIES, SVG_NAMESPACE: contracts.SVG_NAMESPACE, calculateAverageConsumption: (...args) => consumptionModel.calculateAverageConsumption(...args), consumptionTrendState, createConsumptionTrendKey: (...args) => consumptionTrendController.createConsumptionTrendKey(...args), createNiceConsumptionMaximum: (...args) => consumptionModel.createNiceConsumptionMaximum(...args), formatCompactConsumptionNumber: (...args) => consumptionModel.formatCompactConsumptionNumber(...args), formatConsumptionCurrency: (...args) => consumptionModel.formatConsumptionCurrency(...args), formatConsumptionPointLabel: (...args) => consumptionModel.formatConsumptionPointLabel(...args), formatConsumptionRange: (...args) => consumptionModel.formatConsumptionRange(...args), normalizeConsumptionTrendItems: (...args) => consumptionModel.normalizeConsumptionTrendItems(...args), environment }); const consumptionModel = createConsumptionModel({ }); const debugContentLog = createDebugContentLog({ previewDebugBody: createDebugBodyPreview, DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT, debugState, renderDebugPanel: (...args) => debugContentView.renderDebugPanel(...args) }); const debugContentView = createDebugContentView({ createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), DEBUG_PANEL_ID: contracts.DEBUG_PANEL_ID, MODAL_ID: contracts.MODAL_ID, debugState, formatDebugBody: (...args) => debugContentLog.formatDebugBody(...args), formatHeaderBlock: (...args) => debugContentLog.formatHeaderBlock(...args), formatMetadataBlock: (...args) => debugContentLog.formatMetadataBlock(...args), environment }); function start() { startContentApplication({ environment, page: contentFusionPage, styles: contentStyles, products: productsContentJobs }); } return { workloadActionForm, activityFilters, start, contracts, opportunityWorkloadActions, fusionCrmServiceRequests, opportunityCrmServiceRequests, authSessionState, authSalesCommonState, opportunitiesQueryState, opportunitiesTableState, opportunitiesFilterState, preferencesState, productsContentState, aiSummaryState, consumptionPlansState, consumptionTrendState, opportunitiesDashboardState, debugState, contentFusionPage, contentModal, contentTheme, contentIcons, contentStyles, authContentSession, currentUser, authSalesCommon, authTokenRelay, authXsrfContent, platformContentMessages, opportunitiesController, fusionOpportunities, opportunitiesFiscalPeriods, opportunitiesTable, opportunitiesDashboard, opportunitiesFilters, opportunitiesFormatters, opportunitiesLinks, preferencesStorage, preferencesView, productsContentJobs, productActivities, activityProgress, srSessionPreparation, refreshAll, activityCache, productsDetails, aiSummaryContent, consumptionPlansAction, installBaseAction, installBaseView, serviceRequestsAction, serviceRequestsView, consumptionPlansView, consumptionPlanDetailController, consumptionPlanDetailView, ratecardModel, ratecardView, ratecardController, consumptionTrendController, consumptionChart, consumptionModel, debugContentLog, debugContentView }; } createContentApplication().start(); })();