8570 lines
508 KiB
JavaScript
8570 lines
508 KiB
JavaScript
(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-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",
|
|
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<string>}} */
|
|
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,
|
|
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;
|
|
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);
|
|
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;
|
|
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"
|
|
],
|
|
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, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, environment }) {
|
|
let status = "idle";
|
|
let byOpportunity = new Map();
|
|
let pending = null;
|
|
let generation = 0;
|
|
let loadedItems = [];
|
|
function reset() { 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);
|
|
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) {
|
|
cycle = { requestId, token, 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 products = [...productsContentState.opptyProductsByNumber.values()].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;
|
|
return Promise.resolve(loadCrm(current.token, current.version, { cachedAt, cacheOnly: products.every(result => result.fromCache) })).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: result.fromCache === true }));
|
|
}
|
|
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 === "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}`;
|
|
entry.badge.title = key === "sr" ? "CRM service request records loaded" : "Workload action requests completed";
|
|
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/internal-sr-session.js
|
|
function createInternalSrSession({ query, environment }) {
|
|
const { document, setTimeout } = environment;
|
|
let recovery = null;
|
|
const wait = duration => new Promise(resolve => setTimeout(resolve, duration));
|
|
const now = () => environment.performance?.now() ?? Date.now();
|
|
async function warmSession(id) {
|
|
const frame = document.createElement("iframe");
|
|
frame.src = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=SR0001421040&srId=302963804324543&view=showisr";
|
|
frame.title = "Internal SR 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 Internal SR 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 Internal SR session iframe could not be loaded. Sign in manually and try again.");
|
|
const result = await query(id);
|
|
if (result?.ok) return { id, result };
|
|
if (result?.httpStatus !== 401) throw new Error(result?.error || "Unable to load Internal SR details.");
|
|
}
|
|
throw new Error(`Internal SR 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);
|
|
if (!result?.ok && result?.httpStatus === 401) {
|
|
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 Internal SR details.");
|
|
return result.items;
|
|
}
|
|
return { request };
|
|
}
|
|
|
|
// 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." });
|
|
}
|
|
}
|
|
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.";
|
|
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, 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 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 };
|
|
}
|
|
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);
|
|
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"].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 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({
|
|
closeActionForm: (...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 });
|
|
const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({
|
|
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 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({
|
|
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,
|
|
refreshAll,
|
|
activityCache,
|
|
productsDetails,
|
|
aiSummaryContent,
|
|
consumptionPlansAction,
|
|
installBaseAction,
|
|
installBaseView,
|
|
serviceRequestsAction,
|
|
serviceRequestsView,
|
|
consumptionPlansView,
|
|
consumptionPlanDetailController,
|
|
consumptionPlanDetailView,
|
|
ratecardModel,
|
|
ratecardView,
|
|
ratecardController,
|
|
consumptionTrendController,
|
|
consumptionChart,
|
|
consumptionModel,
|
|
debugContentLog,
|
|
debugContentView
|
|
};
|
|
}
|
|
|
|
createContentApplication().start();
|
|
})();
|