(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-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: 2147483647;\n display: grid;\n grid-template-rows: auto auto minmax(0, 1fr);\n width: auto;\n height: auto;\n max-height: none;\n min-width: 0;\n box-sizing: border-box;\n overflow: hidden;\n margin: 0;\n padding: 20px;\n border: 0;\n border-radius: 0;\n background: var(--debug-surface);\n color: var(--debug-text);\n color-scheme: light;\n}\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-panel {\n --debug-surface: #292827;\n --debug-code: #222120;\n --debug-text: #f6f4f2;\n --debug-muted: #c9c5c1;\n --debug-border: #514c47;\n --debug-success: #a9e3bd;\n --debug-success-bg: #214d34;\n --debug-failed: #ffb4ab;\n --debug-failed-bg: #582c2a;\n --debug-pending: #ffe099;\n --debug-pending-bg: #59441e;\n --debug-info: #a8d9ff;\n --debug-info-bg: #233f52;\n --debug-key: #8ed6f0;\n --debug-string: #a9e3bd;\n --debug-number: #ffe099;\n --debug-literal: #d6b7f1;\n color: #f6f4f2;\n color-scheme: dark;\n}\n.opportunities-extension-debug-actions { display: flex; gap: 8px; flex: 0 0 auto; }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-list {\n overflow: auto;\n min-height: 0;\n min-width: 0;\n align-content: start;\n scrollbar-gutter: stable;\n padding: 2px 8px 12px 2px;\n}\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item {\n --debug-state: var(--debug-info);\n --debug-state-bg: var(--debug-info-bg);\n border-color: var(--debug-border);\n border-left-color: var(--debug-state);\n background: var(--debug-surface);\n min-width: 0;\n}\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-success { --debug-state: var(--debug-success); --debug-state-bg: var(--debug-success-bg); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-failed { --debug-state: var(--debug-failed); --debug-state-bg: var(--debug-failed-bg); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item-pending { --debug-state: var(--debug-pending); --debug-state-bg: var(--debug-pending-bg); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-status {\n color: var(--debug-state);\n background: var(--debug-state-bg);\n}\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status,\n.opportunities-extension-debug-method {\n color: var(--debug-info);\n background: var(--debug-info-bg);\n}\n.opportunities-extension-debug-method {\n display: inline-flex;\n align-items: center;\n min-height: 22px;\n padding: 0 8px;\n border-radius: 4px;\n font: 700 11px/1.2 Consolas, monospace;\n}\n.opportunities-extension-debug-method[data-method=\"POST\"] { color: var(--debug-literal); background: var(--debug-code); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class=\"2\"] { color: var(--debug-success); background: var(--debug-success-bg); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class=\"4\"],\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class=\"5\"] { color: var(--debug-failed); background: var(--debug-failed-bg); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-http-status[data-http-class=\"pending\"] { color: var(--debug-pending); background: var(--debug-pending-bg); }\n.opportunities-extension-debug-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; padding: 0 14px 16px; }\n.opportunities-extension-debug-block { min-width: 0; border-left: 2px solid var(--debug-info); padding-left: 10px; }\n.opportunities-extension-debug-block[data-direction=\"response\"] { border-left-color: var(--debug-success); }\n.opportunities-extension-modal .opportunities-extension-debug-block h3 { margin: 0 0 8px; color: var(--debug-info); font-size: 12px; }\n.opportunities-extension-modal .opportunities-extension-debug-block[data-direction=\"response\"] h3 { color: var(--debug-success); }\n.opportunities-extension-modal .opportunities-extension-debug-panel .opportunities-extension-debug-item pre {\n min-width: 0;\n margin: 0;\n padding: 12px;\n max-height: 380px;\n overflow: auto;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n color: var(--debug-text);\n background: var(--debug-code);\n line-height: 1.6;\n tab-size: 2;\n}\n.opportunities-extension-debug-json-key { color: var(--debug-key); }\n.opportunities-extension-debug-json-string { color: var(--debug-string); }\n.opportunities-extension-debug-json-number { color: var(--debug-number); }\n.opportunities-extension-debug-json-literal { color: var(--debug-literal); }\n@media (max-width: 760px) {\n .opportunities-extension-modal .opportunities-extension-debug-panel { padding: 12px; }\n .opportunities-extension-debug-details { grid-template-columns: minmax(0, 1fr); }\n}\n.opportunities-extension-debug-tabs {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n margin-bottom: 12px;\n padding-bottom: 8px;\n border-bottom: 1px solid #dedbd7;\n}\n\n.opportunities-extension-debug-tabs button {\n max-width: 100%;\n min-height: 32px;\n padding: 6px 8px;\n border: 0;\n border-bottom: 2px solid transparent;\n border-radius: 0;\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 12px;\n line-height: 1.35;\n overflow-wrap: anywhere;\n cursor: pointer;\n}\n\n.opportunities-extension-debug-tabs button:hover {\n background: #f5f4f2;\n}\n\n.opportunities-extension-debug-tabs button[aria-selected=\"true\"] {\n border-bottom-color: #00758f;\n background: #e8f4f6;\n color: #006b84;\n font-weight: 700;\n}\n\n.opportunities-extension-debug-tabs button:focus-visible,\n.opportunities-extension-debug-list[role=\"tabpanel\"]:focus-visible {\n outline: 2px solid #00758f;\n outline-offset: -2px;\n}\n\n.opportunities-extension-debug-time {\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.opportunities-extension-debug-heading {\n flex-wrap: wrap;\n}\n\n.opportunities-extension-debug-heading strong {\n flex: 1 1 180px;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.opportunities-extension-debug-summary-meta {\n flex-wrap: wrap;\n justify-content: flex-end;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-tabs button {\n color: #f6f4f2;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-heading::before {\n color: #c9c5c1;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-tabs {\n border-bottom-color: #514c47;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-tabs button:hover {\n background: #383633;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-tabs button[aria-selected=\"true\"] {\n background: #173f48;\n border-bottom-color: #8cc8d3;\n color: #bde7ee;\n}\n\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-tabs button:focus-visible,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-list[role=\"tabpanel\"]:focus-visible {\n outline-color: #8cc8d3;\n}\n.opportunities-extension-debug-duration {\n color: #55514d;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-debug-duration { color: #d6d2ce; }\n\n/* Compact filters use available CSS width, including Windows display scaling. */\n.opportunities-extension-products-progress,\n.opportunities-extension-auth-badge { opacity: 1; transition: opacity 300ms ease; }\n.opportunities-extension-products-progress[data-fading=\"true\"],\n.opportunities-extension-auth-badge[data-fading=\"true\"] { opacity: 0; }\n.opportunities-extension-auth-badge[hidden] { display: none; }\n@media (prefers-reduced-motion: reduce) {\n .opportunities-extension-products-progress,\n .opportunities-extension-auth-badge { transition: none; }\n}\n.opportunities-extension-crm-sr-badge {\n border: 1px solid #408254;\n color: #185b30;\n background: #e1f1e5;\n}\n.opportunities-extension-crm-sr-badge[data-state=\"missing\"] { color: #9f201b; background: #fde8e7; border-color: #c74640; }\n.opportunities-extension-crm-sr-badge[data-state=\"error\"] { color: #785000; background: #fff0cd; border-color: #987021; }\n.opportunities-extension-wl-actions-badge[data-state=\"loading\"] { color: #5f5a55; background: #eceae8; border-color: #8f8a85; }\n.opportunities-extension-crm-sr-badge:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-crm-sr-badge { color: #b8e4c6; background: #214d34; border-color: #6eac82; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-crm-sr-badge[data-state=\"missing\"] { color: #ffd9d5; background: #542824; border-color: #ef938b; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-crm-sr-badge[data-state=\"error\"] { color: #ffe0a3; background: #584219; border-color: #c99b4b; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-wl-actions-badge[data-state=\"loading\"] { color: #dedbd7; background: #484541; border-color: #8f8a85; }\n\n.opportunities-extension-form {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr)) max-content max-content;\n align-items: end;\n gap: 12px;\n min-height: 0;\n padding: 12px;\n}\n\n.opportunities-extension-form > *,\n.opportunities-extension-badge-filters > section {\n min-width: 0;\n width: 100%;\n max-width: none;\n grid-template-rows: auto 32px;\n gap: 4px;\n font-size: 12px;\n}\n\n.opportunities-extension-form > * > span:first-child,\n.opportunities-extension-badge-filters > section > span:first-child {\n font-size: 12px;\n line-height: 16px;\n}\n\n.opportunities-extension-form .opportunities-extension-select-wrap {\n width: 100%;\n}\n\n.opportunities-extension-form .opportunities-extension-field select,\n.opportunities-extension-form .opportunities-extension-customer-filter-trigger,\n.opportunities-extension-form .opportunities-extension-table-search-field input {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n height: 32px;\n min-height: 32px;\n padding: 4px 8px;\n font-size: 12px;\n line-height: 20px;\n}\n\n.opportunities-extension-form .opportunities-extension-field select,\n.opportunities-extension-form .opportunities-extension-customer-filter-trigger {\n padding-right: 26px;\n}\n\n.opportunities-extension-form .opportunities-extension-select-wrap::after,\n.opportunities-extension-form .opportunities-extension-customer-filter-trigger::after {\n right: 10px;\n border-left-width: 4px;\n border-right-width: 4px;\n border-top-width: 5px;\n}\n\n.opportunities-extension-form .opportunities-extension-stage-filter-controls,\n.opportunities-extension-form .opportunities-extension-status-filter-controls {\n flex-wrap: nowrap;\n gap: 4px;\n}\n\n.opportunities-extension-form .opportunities-extension-stage-filter-button,\n.opportunities-extension-form .opportunities-extension-status-filter-button,\n.opportunities-extension-form .opportunities-extension-activity-filter-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n height: 28px;\n min-height: 28px;\n padding: 4px 7px;\n border: 1px solid currentColor;\n border-radius: 18px;\n font-family: inherit;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0;\n line-height: 18px;\n white-space: nowrap;\n}\n\n.opportunities-extension-cell-actions > .opportunities-extension-products-count,\n.opportunities-extension-cell-actions > .opportunities-extension-products-ramp,\n.opportunities-extension-cell-actions > .opportunities-extension-crm-sr-badge {\n display: inline-flex;\n align-items: center;\n justify-content: flex-start;\n box-sizing: border-box;\n flex: 0 0 auto;\n gap: 4px;\n height: 20px;\n min-height: 20px;\n margin: 0;\n padding: 0 6px;\n border-width: 1px;\n border-style: solid;\n border-radius: 8px;\n font-family: inherit;\n font-size: 10px;\n font-style: normal;\n font-weight: 600;\n line-height: 16px;\n letter-spacing: 0;\n text-align: center;\n vertical-align: middle;\n white-space: nowrap;\n}\n\n.opportunities-extension-cell-actions .opportunities-extension-badge-icon,\n.opportunities-extension-activity-filter-button .opportunities-extension-badge-icon {\n display: block;\n order: -1;\n flex: 0 0 16px;\n width: 16px;\n height: 16px;\n margin: 0;\n padding: 0;\n border: 0;\n color: inherit;\n fill: none;\n stroke: currentColor;\n stroke-width: 2.25;\n stroke-linecap: round;\n stroke-linejoin: round;\n pointer-events: none;\n}\n\n@media (max-width: 1150px) {\n .opportunities-extension-form {\n grid-template-columns: repeat(4, minmax(0, 1fr));\n }\n .opportunities-extension-badge-filters > .opportunities-extension-status-filter,\n .opportunities-extension-badge-filters > .opportunities-extension-stage-filter {\n grid-column: span 2;\n }\n}\n\n@media (max-width: 700px) {\n .opportunities-extension-form {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n .opportunities-extension-form .opportunities-extension-stage-filter-controls,\n .opportunities-extension-form .opportunities-extension-status-filter-controls {\n flex-wrap: wrap;\n }\n .opportunities-extension-badge-filters > .opportunities-extension-status-filter,\n .opportunities-extension-badge-filters > .opportunities-extension-stage-filter {\n grid-template-rows: auto auto;\n }\n}\n\n@media (max-width: 420px) {\n .opportunities-extension-form { grid-template-columns: minmax(0, 1fr); }\n .opportunities-extension-badge-filters > .opportunities-extension-status-filter,\n .opportunities-extension-badge-filters > .opportunities-extension-stage-filter { grid-column: auto; }\n}\n\n.opportunities-extension-badge-filters { display: contents; }\n.opportunities-extension-activity-filter { display: grid; }\n.opportunities-extension-activity-filter[hidden] { display: none; }\n.opportunities-extension-activity-filter-controls { display: flex; align-items: center; flex-wrap: nowrap; gap: 4px; }\n/* Flexible fields share the space left by the four compact badge groups. */\n.opportunities-extension-form[data-activity-filters=\"true\"] {\n grid-template-columns: repeat(4, minmax(0, 1fr)) repeat(4, max-content);\n column-gap: 8px;\n}\n.opportunities-extension-activity-filter-button {\n min-width: 32px;\n gap: 4px;\n color: #185b30;\n background: #fff;\n cursor: pointer;\n}\n.opportunities-extension-activity-filter-button[data-value=\"missing\"] { color: #9f201b; }\n.opportunities-extension-activity-filter-button[data-value=\"under4\"] { color: #005f7d; }\n.opportunities-extension-activity-filter-button[aria-pressed=\"true\"] { color: #fff; background: #25653a; border-color: #25653a; }\n.opportunities-extension-activity-filter-button[aria-pressed=\"true\"][data-value=\"missing\"] { background: #a12d27; border-color: #a12d27; }\n.opportunities-extension-activity-filter-button[aria-pressed=\"true\"][data-value=\"under4\"] { background: #006b86; border-color: #006b86; }\n.opportunities-extension-activity-filter-button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter { color: #eeeae6; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter-button { color: #b8e4c6; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter-button:not([aria-pressed=\"true\"]) { background: #333130; border-color: #6a6560; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter-button[data-value=\"missing\"] { color: #ffd9d5; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter-button[data-value=\"under4\"] { color: #b5e9fc; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-activity-filter-button[aria-pressed=\"true\"] { border-color: currentColor; }\n@media (max-width: 1350px) {\n .opportunities-extension-form[data-activity-filters=\"true\"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }\n .opportunities-extension-form[data-activity-filters=\"true\"] .opportunities-extension-badge-filters {\n display: flex;\n grid-column: 1 / -1;\n flex-wrap: wrap;\n align-items: start;\n gap: 12px 20px;\n }\n .opportunities-extension-form[data-activity-filters=\"true\"] .opportunities-extension-badge-filters > section {\n width: auto;\n max-width: 100%;\n grid-template-rows: auto auto;\n }\n}\n@media (max-width: 700px) {\n .opportunities-extension-form[data-activity-filters=\"true\"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .opportunities-extension-activity-filter-controls { flex-wrap: wrap; }\n}\n@media (max-width: 420px) {\n .opportunities-extension-form[data-activity-filters=\"true\"] { grid-template-columns: minmax(0, 1fr); }\n}\n\n.opportunities-extension-add-action {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n flex: 0 0 16px;\n width: 16px;\n height: 16px;\n padding: 0;\n margin: 0 0 0 2px;\n border: 0;\n border-radius: 3px;\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n}\n.opportunities-extension-add-action:hover { background: rgba(127, 127, 127, .2); }\n.opportunities-extension-add-action:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-action-form-modal {\n position: absolute;\n inset: 0;\n z-index: 5;\n display: grid;\n place-items: center;\n padding: 12px;\n background: rgba(0, 0, 0, .42);\n}\n.opportunities-extension-action-form-dialog {\n box-sizing: border-box;\n width: min(560px, 100%);\n max-height: calc(100vh - 24px);\n overflow-y: auto;\n background: #fff;\n color: #312d2a;\n border: 1px solid #b9b4ae;\n border-radius: 6px;\n box-shadow: 0 12px 36px rgba(0, 0, 0, .25);\n}\n.opportunities-extension-action-form {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 14px;\n padding: 18px;\n}\n.opportunities-extension-action-form-heading { min-width: 0; overflow-wrap: anywhere; }\n.opportunities-extension-action-form-context {\n display: grid;\n grid-template-columns: max-content minmax(0, 1fr);\n gap: 6px 12px;\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 18px;\n}\n.opportunities-extension-action-form-context dt { font-weight: 600; }\n.opportunities-extension-action-form-context dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }\n.opportunities-extension-action-form-field { display: grid; gap: 6px; min-width: 0; font-size: 13px; }\n.opportunities-extension-action-form-field[hidden] { display: none; }\n.opportunities-extension-action-form-status { margin: 0; font-size: 13px; overflow-wrap: anywhere; }\n.opportunities-extension-action-form-status[role=\"alert\"] { color: #a12d27; }\n.opportunities-extension-action-form-wide,\n.opportunities-extension-action-form-footer { grid-column: 1 / -1; }\n.opportunities-extension-action-form-field input,\n.opportunities-extension-action-form-field select,\n.opportunities-extension-action-form-field textarea {\n box-sizing: border-box;\n min-width: 0;\n width: 100%;\n border: 1px solid #8f8983;\n border-radius: 4px;\n padding: 8px;\n font: inherit;\n line-height: 20px;\n color: inherit;\n background: #fff;\n}\n.opportunities-extension-action-form-field textarea { resize: vertical; min-height: 96px; }\n.opportunities-extension-action-form-field select { height: 38px; }\n.opportunities-extension-action-form-field select[hidden] { display: none; }\n.opportunities-extension-sr-form-dialog { width: min(900px, 100%); }\n.opportunities-extension-sr-form-dialog > .opportunities-extension-account-plans-header { padding: 20px 24px; }\n.opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { gap: 20px 24px; padding: 24px; align-items: start; }\n.opportunities-extension-sr-form-dialog .opportunities-extension-action-form-field { gap: 8px; }\n.opportunities-extension-sr-form-dialog textarea { min-height: 112px; }\n.opportunities-extension-sr-form-dialog .opportunities-extension-sr-service-option { padding: 12px 0; gap: 10px; }\n.opportunities-extension-sr-form-dialog .opportunities-extension-sr-service-abstract { font-size: 13px; line-height: 20px; }\n@media (min-width: 1000px) {\n .opportunities-extension-sr-form-dialog { width: min(1120px, 100%); }\n .opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 24px; }\n .opportunities-extension-sr-services { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 28px; align-items: start; }\n .opportunities-extension-sr-services .opportunities-extension-sr-service-option { margin: 0; border-top: 1px solid #aaa6a040; }\n .opportunities-extension-sr-services .opportunities-extension-sr-service-option:nth-child(-n+2) { border-top: 0; }\n}\n@media (max-width: 600px) {\n .opportunities-extension-sr-form-dialog > .opportunities-extension-account-plans-header { padding: 16px; }\n .opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { grid-template-columns: minmax(0, 1fr); gap: 18px; padding: 16px; }\n}\n.opportunities-extension-sr-lookup { min-width: 0; padding: 0; margin: 0; border: 0; font-size: 13px; }\n.opportunities-extension-sr-lookup legend { padding: 0 0 8px; font-weight: 600; }\n.opportunities-extension-sr-lookup[aria-busy=\"true\"] > p { opacity: .65; }\n.opportunities-extension-sr-service-option { display: flex; align-items: flex-start; gap: 8px; padding: 9px 0; cursor: pointer; }\n.opportunities-extension-sr-service-option + .opportunities-extension-sr-service-option { border-top: 1px solid #aaa6a040; }\n.opportunities-extension-sr-service-option input { flex: 0 0 16px; width: 16px; height: 16px; margin: 2px 0 0; accent-color: #00758f; }\n.opportunities-extension-sr-service-option > span { min-width: 0; overflow-wrap: anywhere; }\n.opportunities-extension-sr-service-name { display: block; font-weight: 600; line-height: 20px; }\n.opportunities-extension-sr-service-abstract { display: block; margin-top: 3px; color: #625d58; font-size: 12px; line-height: 18px; white-space: pre-wrap; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-sr-service-abstract { color: #c7c2bc; }\n.opportunities-extension-sr-lookup-retry { color: inherit; background: transparent; border: 1px solid #8f8983; border-radius: 4px; padding: 5px 10px; cursor: pointer; }\n.opportunities-extension-sr-lookup-retry[hidden] { display: none; }\n.opportunities-extension-action-form [aria-invalid=\"true\"] { border-color: #b3261e; outline: 1px solid #b3261e; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form [aria-invalid=\"true\"] { border-color: #ffaaa4; outline-color: #ffaaa4; }\n.opportunities-extension-modal .opportunities-extension-copy-toast { z-index: 20; }\n.opportunities-extension-action-combobox { position: relative; min-width: 0; }\n.opportunities-extension-action-combobox input { height: 38px; }\n.opportunities-extension-action-options {\n position: absolute;\n z-index: 10;\n top: calc(100% + 4px);\n left: 0;\n right: 0;\n box-sizing: border-box;\n max-height: 180px;\n overflow-y: auto;\n overscroll-behavior: contain;\n border: 1px solid #8f8983;\n border-radius: 4px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, .2);\n background: #fff;\n color: #292725;\n}\n.opportunities-extension-action-options[data-placement=\"above\"] { top: auto; bottom: calc(100% + 4px); }\n.opportunities-extension-action-options[hidden] { display: none; }\n.opportunities-extension-action-option { padding: 8px; line-height: 20px; overflow-wrap: anywhere; cursor: pointer; }\n.opportunities-extension-action-option:hover,\n.opportunities-extension-action-option[aria-selected=\"true\"] { background: #e3f2f5; color: #005b70; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-options { background: #211f1d; color: #eeeae6; border-color: #918a82; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-option:hover,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-option[aria-selected=\"true\"] { background: #174955; color: #c9f4ff; }\n.opportunities-extension-action-form[data-state=\"loading\"] .opportunities-extension-action-combobox input:disabled { animation: opportunities-action-options-loading 1s ease-in-out infinite alternate; }\n.opportunities-extension-action-form[data-state=\"loading\"] .opportunities-extension-action-form-field > :disabled {\n animation: opportunities-action-options-loading 1s ease-in-out infinite alternate;\n}\n@keyframes opportunities-action-options-loading { from { opacity: .45; } to { opacity: .85; } }\n@media (prefers-reduced-motion: reduce) {\n .opportunities-extension-action-form[data-state=\"loading\"] .opportunities-extension-action-form-field > :disabled,\n .opportunities-extension-action-form[data-state=\"loading\"] .opportunities-extension-action-combobox input:disabled { animation: none; }\n}\n.opportunities-extension-action-form-footer { display: flex; justify-content: flex-end; gap: 8px; }\n.opportunities-extension-action-form-footer button {\n border: 1px solid #8f8983;\n border-radius: 4px;\n padding: 7px 14px;\n font: inherit;\n font-size: 13px;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n.opportunities-extension-action-form-footer button:disabled { opacity: .55; cursor: not-allowed; }\n.opportunities-extension-action-form :focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form-dialog {\n color-scheme: dark;\n background: #292725;\n color: #eeeae6;\n border-color: #756f68;\n}\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form-field input,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form-field select,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form-field textarea { background: #211f1d; border-color: #918a82; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form-status[role=\"alert\"] { color: #ffd9d5; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-action-form :focus-visible { outline-color: #80d6e8; }\n@media (max-width: 420px) {\n .opportunities-extension-action-form { grid-template-columns: minmax(0, 1fr); }\n}\n\n/* Keep layout rules on the outer table, never on expanded product tables. */\n.opportunities-extension-table[data-opportunities-layout=\"inline-actions\"] {\n width: 100%;\n min-width: 1168px;\n table-layout: fixed;\n}\n\n.opportunities-extension-table-skeleton-row { height: 76px; }\n.opportunities-extension-table-skeleton-text { width: 85%; max-width: 100%; }\n.opportunities-extension-table-skeleton-row:nth-of-type(even) .opportunities-extension-table-skeleton-text { width: 70%; }\n.opportunities-extension-table-skeleton-badge { flex: 0 1 30px; width: 30px; min-width: 0; height: 16px; }\n.opportunities-extension-table-skeleton-row > td[data-column=\"customer\"] .opportunities-extension-table-skeleton-badge { flex-basis: 18px; width: 18px; }\n.opportunities-extension-table-skeleton-row > td[data-column=\"status\"] .opportunities-extension-table-skeleton-text,\n.opportunities-extension-table-skeleton-row > td[data-column=\"stage\"] .opportunities-extension-table-skeleton-text { height: 20px; max-width: 66px; border-radius: 8px; }\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: 114px;\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.opportunities-extension-option-description { display: block; font-size: 12px; line-height: 1.4; margin-top: 4px; white-space: normal; overflow-wrap: anywhere; opacity: .85; }\n.opportunities-extension-action-form-dialog.opportunities-extension-internal-sr-dialog { width: min(760px, 100%); }\n.opportunities-extension-session-retry {\n margin-left: 8px; padding: 2px 6px; border: 1px solid currentColor;\n border-radius: 4px; background: transparent; color: inherit; font: inherit; cursor: pointer;\n}\n.opportunities-extension-session-retry:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-session-retry:disabled { opacity: .6; cursor: wait; }\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-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.opportunities-extension-install-base-table .opportunities-extension-sort-button { min-height: 28px; padding: 4px 16px 4px 0; overflow-wrap: anywhere; }\n.opportunities-extension-install-base-table .opportunities-extension-sort-button::after { right: 0; font-size: 12px; }\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.opportunities-extension-service-requests-table .opportunities-extension-sort-button { min-height: 28px; padding: 4px 16px 4px 0; overflow-wrap: anywhere; }\n.opportunities-extension-service-requests-table .opportunities-extension-sort-button::after { right: 0; font-size: 12px; }\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-ratecard-toolbar { flex-wrap: wrap; }\n.opportunities-extension-ratecard-order-filter { position: relative; min-width: 0; }\n.opportunities-extension-ratecard-order-filter .opportunities-extension-customer-filter-trigger { min-height: 38px; padding-block: 8px; font-size: 13px; }\n.opportunities-extension-ratecard-order-filter .opportunities-extension-customer-filter-panel { width: min(320px, calc(100vw - 80px)); max-height: 45vh; overflow: auto; }\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\n.opportunities-extension-views { min-height: 0; min-width: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 8px; }\n.opportunities-extension-views > [hidden] { display: none; }\n.opportunities-extension-view-tabs { display: flex; gap: 20px; border-bottom: 1px solid #dedbd7; }\n.opportunities-extension-view-tabs button { border: 0; border-bottom: 3px solid transparent; background: transparent; color: #625d58; padding: 6px 2px 9px; font: 600 13px/20px Arial, sans-serif; cursor: pointer; }\n.opportunities-extension-view-tabs button[aria-selected=\"true\"] { border-bottom-color: #00758f; color: #00657b; }\n.opportunities-extension-view-tabs button:focus-visible { outline: 2px solid #00758f; outline-offset: -2px; }\n.opportunities-extension-view-tabs button { display: inline-flex; align-items: center; gap: 6px; }\n.opportunities-extension-view-tabs button svg { width: 16px; height: 16px; flex: 0 0 16px; }\n.opportunities-extension-calendar {\n --fc-border-color: #dedbd7; --fc-page-bg-color: #fff; --fc-neutral-bg-color: #f5f4f2;\n --fc-neutral-text-color: #625d58; --fc-today-bg-color: #eef7fa;\n --fc-button-bg-color: #fff; --fc-button-text-color: #312d2a; --fc-button-border-color: #b9b4ae;\n --fc-button-hover-bg-color: #e6f3f5; --fc-button-hover-border-color: #00758f;\n --fc-button-active-bg-color: #dceff3; --fc-button-active-border-color: #00758f;\n min-height: 0; min-width: 0; overflow: hidden; display: flex; flex-direction: column; padding: 12px; background: #fff; color: #312d2a;\n border: 1px solid var(--fc-border-color); border-radius: 4px; font: 13px/1.4 Arial, sans-serif;\n}\n.opportunities-calendar-grid { flex: 1 1 0; min-height: 0; min-width: 0; }\n.opportunities-calendar-day-cards { position: absolute; inset: 27px 0 0; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; padding-bottom: 4px; }\n.opportunities-calendar-day-card { display: block; width: calc(100% - 8px); padding: 0; text-align: left; font: inherit; }\n.opportunities-calendar-day-card:focus-visible { outline: 2px solid #00758f; outline-offset: -2px; }\n/* A single vertical sequence of months, with a separate scroll area per day. */\n.opportunities-extension-calendar .fc .fc-multimonth { overflow-y: auto; overflow-x: hidden; }\n.opportunities-extension-calendar .fc .fc-multimonth-month { width: 100%; }\n.opportunities-extension-calendar .fc .fc-multimonth-daygrid-table { height: auto !important; }\n.opportunities-extension-calendar .fc .fc-multimonth .fc-daygrid-body tbody { height: auto; }\n.opportunities-extension-calendar .fc .fc-multimonth .fc-daygrid-body tr { height: var(--calendar-week-height, 190px); }\n.opportunities-extension-calendar .fc .fc-multimonth .fc-daygrid-day-frame { position: relative; height: var(--calendar-week-height, 190px); min-height: var(--calendar-week-height, 190px); }\n.opportunities-extension-calendar .fc .fc-multimonth .fc-daygrid-day-events {\n position: absolute; inset: 27px 0 0; overflow-y: auto; overflow-x: hidden;\n margin: 0; padding-bottom: 4px; scrollbar-width: thin;\n}\n.opportunities-extension-calendar .fc .fc-multimonth .fc-daygrid-event-harness { position: relative !important; top: auto !important; left: auto !important; right: auto !important; }\n.opportunities-extension-calendar .fc .fc-multimonth .fc-daygrid-day-bg { pointer-events: none; }\n.opportunities-extension-calendar .fc { min-width: 620px; }\n.opportunities-extension-calendar .fc .fc-toolbar { gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }\n.opportunities-extension-calendar .fc .fc-toolbar-title { font-size: 18px; line-height: 26px; letter-spacing: 0; }\n.opportunities-extension-calendar .fc .fc-button { font-size: 13px; line-height: 20px; padding: 5px 10px; border-radius: 4px; box-shadow: none; }\n.opportunities-extension-calendar .fc .fc-button:focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }\n.opportunities-extension-calendar .fc .fc-button svg { width: 16px; height: 16px; margin-right: 6px; vertical-align: -3px; }\n.opportunities-extension-calendar .fc a { color: inherit; text-decoration: none; }\n.opportunities-extension-calendar .fc th { background: var(--fc-neutral-bg-color); font-size: 12px; font-weight: 600; padding: 4px 0; }\n.opportunities-extension-calendar .fc .fc-daygrid-day-number { font-size: 11px; line-height: 18px; padding: 1px 6px; }\n.opportunities-extension-calendar .fc .fc-daygrid-day-events { min-height: 0; margin-bottom: 0; }\n.opportunities-extension-calendar .fc .fc-daygrid-day-bottom { margin: 0; }\n.opportunities-extension-calendar .fc .fc-daygrid-day-frame { min-height: 0; padding-top: 0; }\n/* Event content must not contribute to the table's intrinsic row heights. */\n.opportunities-extension-calendar .fc .fc-daygrid-body tbody { height: 100%; }\n.opportunities-extension-calendar .fc .fc-daygrid-body tr { height: calc(100% / var(--opportunities-calendar-weeks, 5)); }\n.opportunities-extension-calendar .fc .fc-daygrid-day { position: relative; }\n.opportunities-extension-calendar .fc .fc-daygrid-day-frame { position: absolute; inset: 0; }\n.opportunities-extension-calendar .fc .fc-daygrid-week-number { top: 0; left: 0; padding: 3px 6px; background: var(--fc-neutral-bg-color); color: inherit; font: 600 10px/16px Arial, sans-serif; white-space: nowrap; }\n.opportunities-calendar-day-count { position: absolute; top: 2px; left: 6px; min-width: 18px; height: 16px; padding: 0 4px; border-radius: 8px; background: #dceff3; color: #005467; font: 600 10px/16px Arial, sans-serif; text-align: center; }\n.fc-daygrid-week-number ~ .opportunities-calendar-day-count { left: 82px; }\n.opportunities-calendar-day-count[hidden] { display: none; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-calendar-day-count:not([data-empty=\"true\"]) { background: #16454f; color: #b9eaf4; }\n.opportunities-extension-calendar .fc .fc-day-other { background: var(--fc-neutral-bg-color); }\n.opportunities-extension-calendar .fc .fc-daygrid-event { white-space: normal; margin: 3px 4px; border-radius: 4px; cursor: pointer; border-width: 0 0 0 3px; border-style: solid; }\n.opportunities-extension-calendar .fc .fc-event-main { color: inherit; }\n.opportunities-calendar-event-content { min-width: 0; padding: 5px 6px; overflow-wrap: anywhere; }\n.opportunities-calendar-event-content strong { display: block; font-size: 12px; line-height: 16px; font-weight: 600; }\n.opportunities-calendar-event-content span { display: block; margin-top: 3px; font-size: 11px; line-height: 15px; }\n.opportunities-calendar-event-content strong,\n.opportunities-calendar-event-content span,\n.opportunities-calendar-day-item strong,\n.opportunities-calendar-day-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.opportunities-extension-calendar .fc .fc-daygrid-more-link { color: #00657b; font-size: 11px; line-height: 16px; padding: 0 5px; }\n.opportunities-calendar-day-list { min-height: 0; overflow-y: auto; padding: 8px 20px 20px; }\n.opportunities-calendar-day-item { display: block; width: 100%; padding: 12px 0; border: 0; border-bottom: 1px solid #dedbd7; background: transparent; color: #00657b; text-align: left; cursor: pointer; overflow-wrap: anywhere; }\n.opportunities-calendar-day-item strong, .opportunities-calendar-day-item span { display: block; font: 600 13px/20px Arial, sans-serif; }\n.opportunities-calendar-day-item span { margin-top: 4px; font-weight: 400; }\n.opportunities-calendar-day-item:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-calendar-day-item { border-bottom-color: #514d48; color: #80d6e8; }\n.opportunities-extension-calendar .fc .opportunities-calendar-stage-SQL { background: #dff1f4; border-color: #00829d; color: #005467; }\n.opportunities-extension-calendar .fc .opportunities-calendar-stage-PIPELINE,\n.opportunities-extension-calendar .fc .opportunities-calendar-stage-OTHER { background: #efeeec; border-color: #706a64; color: #49443f; }\n.opportunities-extension-calendar .fc .opportunities-calendar-stage-UPSIDE { background: #fff0da; border-color: #ba7000; color: #774700; }\n.opportunities-extension-calendar .fc .opportunities-calendar-stage-FORECAST { background: #f0e8f7; border-color: #8262a3; color: #624379; }\n.opportunities-extension-calendar .fc .opportunities-calendar-stage-WON { background: #e2f1e5; border-color: #2f8050; color: #215939; }\n.opportunities-calendar-message { margin: 0 0 12px; font-size: 13px; }\n.opportunities-calendar-message[hidden], .opportunities-calendar-grid[hidden], .opportunities-calendar-undated[hidden] { display: none; }\n.opportunities-calendar-undated { flex: 0 0 auto; max-height: 90px; overflow-y: auto; margin-top: 6px; border-top: 1px solid var(--fc-border-color); padding-top: 4px; }\n.opportunities-calendar-undated summary { cursor: pointer; font-size: 12px; line-height: 18px; }\n.opportunities-calendar-undated h3 { font-size: 13px; margin: 0 0 8px; }\n.opportunities-calendar-undated button { display: block; background: none; border: 0; text-align: left; font: inherit; padding: 6px 0; color: inherit; text-decoration: underline; cursor: pointer; overflow-wrap: anywhere; }\n.opportunities-calendar-detail .opportunities-extension-account-plans-header { align-items: start; gap: 16px; }\n.opportunities-calendar-detail .opportunities-extension-action-form-dialog { display: grid; grid-template-rows: auto minmax(0, 1fr); overflow: hidden; }\n.opportunities-calendar-detail h2 { font-size: 18px; overflow-wrap: anywhere; }\n.opportunities-calendar-detail-content { padding: 20px; overflow-y: auto; }\n.opportunities-calendar-detail-content a { color: #006b82; }\n.opportunities-calendar-detail-badges { display: flex; gap: 8px; margin-top: 18px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-view-tabs { border-color: #514d48; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-view-tabs button { color: #c7c2bc; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-view-tabs button[aria-selected=\"true\"] { color: #80d6e8; border-bottom-color: #80d6e8; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-calendar {\n --fc-border-color: #514d48; --fc-page-bg-color: #292725; --fc-neutral-bg-color: #33302d;\n --fc-neutral-text-color: #c7c2bc; --fc-today-bg-color: #223b3f;\n --fc-button-bg-color: #292725; --fc-button-text-color: #eeeae6; --fc-button-border-color: #756f68;\n --fc-button-hover-bg-color: #234850; --fc-button-hover-border-color: #80d6e8;\n --fc-button-active-bg-color: #234850; --fc-button-active-border-color: #80d6e8;\n background: #292725; color: #eeeae6;\n}\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-calendar .fc .opportunities-calendar-stage-SQL { background: #16454f; color: #b9eaf4; border-color: #67cde0; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-calendar .fc .opportunities-calendar-stage-PIPELINE,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-calendar .fc .opportunities-calendar-stage-OTHER { background: #403c38; color: #e4dfd9; border-color: #b8b1a8; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-calendar .fc .opportunities-calendar-stage-UPSIDE { background: #533e1e; color: #ffe0a9; border-color: #dba44f; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-calendar .fc .opportunities-calendar-stage-FORECAST { background: #493657; color: #ead5fc; border-color: #b998d8; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-calendar .fc .opportunities-calendar-stage-WON { background: #244a32; color: #c7edce; border-color: #84c396; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-calendar .fc .fc-daygrid-more-link,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-calendar-detail-content a { color: #80d6e8; }\n@media (max-width: 700px) {\n .opportunities-extension-body { overflow-y: auto; grid-template-rows: max-content max-content minmax(480px, 1fr); }\n .opportunities-extension-views { min-height: 480px; }\n .opportunities-extension-calendar { padding: 10px; overflow-x: auto; }\n .opportunities-extension-calendar .fc { min-width: 0; }\n .opportunities-extension-calendar .fc .fc-view-harness { min-width: 560px; }\n .opportunities-extension-calendar .fc .fc-toolbar-title { font-size: 16px; }\n .opportunities-extension-calendar .fc .fc-daygrid-day-frame { padding-top: 22px; }\n .opportunities-calendar-day-count { top: 25px; }\n .fc-daygrid-week-number ~ .opportunities-calendar-day-count { left: 6px; }\n}\n.opportunities-view-bar { display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--oe-table-heading-border); flex-wrap: wrap; }\n.opportunities-view-bar .opportunities-extension-view-tabs { border-bottom: 0; }\n.opportunities-product-type-filters { display: flex; gap: 8px; margin-left: auto; padding: 4px 0; }\n.opportunities-product-type-filter { border: 1px solid #8f8a85; border-radius: 12px; padding: 3px 9px; background: transparent; color: inherit; font: 600 12px/16px Arial, sans-serif; cursor: pointer; }\n.opportunities-product-type-filter[aria-pressed=\"true\"][data-type=\"Booking\"] { color: #714016; background: #fff0df; border-color: #c58b50; }\n.opportunities-product-type-filter[aria-pressed=\"true\"][data-type=\"Workload\"] { color: #63358a; background: #f0e8f7; border-color: #bba5cf; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-product-type-filter[aria-pressed=\"true\"][data-type=\"Booking\"] { color: #ffdbb5; background: #4b321e; border-color: #c99760; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-product-type-filter[aria-pressed=\"true\"][data-type=\"Workload\"] { color: #ead5fc; background: #43334f; border-color: #9372ac; }\n.opportunities-product-type-filter:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n\n.oe-time-dialog { --time-bg: #fff; --time-input-bg: #fff; --time-muted: #faf9f8; --time-border: #dedbd7; --time-input-border: #8f8983; --time-accent: #00758f; width: min(1600px, 100%); max-height: calc(100dvh - 24px); display: flex; flex-direction: column; overflow: hidden; font-size: 13px; line-height: 20px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .oe-time-dialog { --time-bg: #292725; --time-input-bg: #211f1d; --time-muted: #35322f; --time-border: #514c47; --time-input-border: #918a82; --time-accent: #80d6e8; }\n.oe-time-modal { grid-template-columns: minmax(0, 1fr); }\n.oe-time-dialog, .oe-time-body { min-width: 0; }\n.oe-time-dialog * { box-sizing: border-box; }\n.oe-time-dialog [hidden] { display: none !important; }\n.oe-time-header { flex-shrink: 0; }\n.oe-time-body { padding: 18px; overflow: auto; min-height: 0; }\n.oe-time-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; }\n.oe-time-navigation { display: flex; align-items: center; gap: 8px; }\n.oe-time-ending { display: grid; gap: 4px; text-align: right; }\n.oe-time-summary { display: grid; grid-template-columns: repeat(4, 1fr); padding: 12px 0; gap: 12px; text-align: center; border-block: 1px solid var(--time-border); margin-bottom: 18px; }\n.oe-time-body button { min-width: 38px; min-height: 38px; display: inline-flex; gap: 8px; align-items: center; justify-content: center; color: inherit; background: transparent; border: 1px solid var(--time-input-border); border-radius: 4px; padding: 7px; cursor: pointer; font: inherit; }\n.oe-time-dialog button:hover:not(:disabled) { background: var(--time-muted); border-color: var(--time-accent); }\n.oe-time-dialog svg { width: 17px; height: 17px; flex-shrink: 0; }\n.oe-time-dialog input, .oe-time-dialog select, .oe-time-dialog textarea { font: inherit; line-height: 20px; color: inherit; background: var(--time-input-bg); border: 1px solid var(--time-input-border); border-radius: 4px; padding: 8px; min-height: 38px; }\n.oe-time-dialog select { height: 38px; }\n.oe-time-dialog :focus-visible { outline: 2px solid var(--time-accent); outline-offset: 2px; }\n.oe-time-dialog :disabled { opacity: .55; cursor: not-allowed; }\n.oe-time-grid { overflow-x: auto; }\n.oe-time-grid table { width: 100%; min-width: 1060px; border-collapse: collapse; table-layout: fixed; }\n.oe-time-grid th { background: var(--time-muted); padding: 10px 5px; font-weight: 600; text-align: center; border-bottom: 1px solid var(--time-border); }\n.oe-time-grid .opportunities-extension-sort-button { min-width: 0; min-height: 28px; padding: 4px 12px 4px 0; border: 0; border-radius: 0; gap: 0; }\n.oe-time-grid .opportunities-extension-sort-button::after { right: 0; font-size: 12px; }\n.oe-time-grid .oe-time-day-label { margin-bottom: 0; }\n.oe-time-grid th:first-child { width: 23%; }\n.oe-time-grid th:nth-child(2), .oe-time-grid th:nth-child(3) { width: 12%; }\n.oe-time-grid th:nth-last-child(-n+3) { width: 46px; }\n.oe-time-grid td { padding: 10px 5px; border-bottom: 1px solid var(--time-border); vertical-align: middle; text-align: center; }\n.oe-time-duplicate-row { outline: 2px solid #b3261e; outline-offset: -2px; animation: oe-time-duplicate-fade 5.5s linear forwards; }\n.opportunities-extension-modal[data-theme=\"dark\"] .oe-time-duplicate-row { --time-duplicate-color: #ffaaa4; }\n@keyframes oe-time-duplicate-fade { 0%, 90.909% { outline-color: var(--time-duplicate-color, #b3261e); } 100% { outline-color: transparent; } }\n@media (prefers-reduced-motion: reduce) { .oe-time-duplicate-row { animation-timing-function: step-end; } }\n.oe-time-grid select { width: 100%; }\n.oe-time-grid input[type=\"number\"] { width: min(100%, 60px); padding: 6px 3px; text-align: center; }\n.oe-time-grid .opportunities-extension-action-combobox input { width: 100%; text-align: left; text-overflow: ellipsis; }\n.oe-time-grid .opportunities-extension-action-options { text-align: left; }\n.oe-time-dialog .opportunities-extension-select-tooltip { position: fixed; z-index: 100; width: min(560px, calc(100vw - 40px)); max-height: min(440px, calc(100dvh - 48px)); overflow: auto; padding: 14px; border: 1px solid var(--time-input-border); border-radius: 4px; background: var(--time-input-bg); color: inherit; box-shadow: 0 4px 16px #0003; text-align: left; }\n.oe-time-sr-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 18px; margin: 0; }\n.oe-time-sr-details > div { min-width: 0; overflow-wrap: anywhere; }\n.oe-time-sr-details dt { font-size: 11px; opacity: .75; margin-bottom: 2px; }\n.oe-time-sr-details dd { margin: 0; white-space: pre-wrap; }\n.oe-time-sr-details .oe-time-sr-description { grid-column: 1 / -1; }\n.oe-time-day > span { display: block; }\n.oe-time-day .oe-time-day-label { display: inline-flex; align-items: center; gap: 4px; margin-bottom: 9px; }\n.oe-time-day-total { font-size: 10px; background: var(--time-accent); color: var(--time-bg); min-width: 16px; border-radius: 8px; padding: 2px 4px; }\n.oe-time-notes textarea { width: 100%; resize: vertical; }\n.oe-time-notes-box { position: relative; text-align: left; background: var(--time-input-bg); border: 1px solid var(--time-input-border); border-radius: 4px; padding: 14px 12px 42px; }\n.oe-time-notes-count { display: block; font-weight: 600; opacity: .8; margin-bottom: 8px; }\n.oe-time-notes-box textarea { border: 0; min-height: 128px; padding: 0; }\n.oe-time-notes-box .oe-time-notes-collapse { position: absolute; right: 0; bottom: 0; background: var(--time-muted); }\n.oe-time-footer { flex-wrap: wrap; justify-content: flex-start; margin-top: 18px; }\n.oe-time-footer button { padding: 7px 14px; }\n.oe-time-status { padding: 12px 0; }\n.oe-time-loading-status { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n.oe-time-skeleton { display: inline-block; vertical-align: middle; border-radius: 4px; background: var(--time-border); color: transparent; animation: oe-time-pulse 1.3s ease-in-out infinite alternate; }\n.oe-time-skeleton-select { display: block; width: 100%; height: 38px; }\n.oe-time-skeleton-control { width: min(100%, 60px); height: 38px; }\n.oe-time-skeleton-heading { width: 65%; height: 20px; margin: 17px 0; }\n.oe-time-skeleton-summary { min-width: 44px; height: 20px; }\n@keyframes oe-time-pulse { from { opacity: .4; } to { opacity: 1; } }\n@media (prefers-reduced-motion: reduce) { .oe-time-skeleton { animation: none; } }\n.oe-time-status[role=\"alert\"] { color: #a12d27; }\n.oe-time-dialog [aria-invalid=\"true\"] { border-color: #b3261e; outline: 1px solid #b3261e; }\n.opportunities-extension-modal[data-theme=\"dark\"] .oe-time-status[role=\"alert\"] { color: #ffd9d5; }\n.opportunities-extension-modal[data-theme=\"dark\"] .oe-time-dialog [aria-invalid=\"true\"] { border-color: #ffaaa4; outline-color: #ffaaa4; }\n@media (max-width: 700px) { .oe-time-modal { padding: 8px; } .oe-time-dialog { max-height: calc(100dvh - 16px); } .oe-time-body { padding: 12px; } .oe-time-summary { grid-template-columns: repeat(2, 1fr); text-align: left; } .oe-time-toolbar { flex-wrap: wrap; } .oe-time-ending { text-align: left; } }\n\n.opportunities-extension-products-table th:last-child { width: 90px; padding-left: 6px; padding-right: 6px; white-space: nowrap; text-align: center; }\n.opportunities-extension-products-table .opportunities-extension-projection-cell { text-align: center; vertical-align: middle; }\n.opportunities-extension-projection-alert { display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 0; color: #976000; width: 30px; height: 30px; flex: 0 0 30px; vertical-align: middle; }\n.opportunities-extension-projection-alert svg { display: block; flex: none; width: 19px; height: 19px; }\n.opportunities-extension-cell-actions > .opportunities-extension-opportunity-projection-alert { width: 20px; height: 20px; flex-basis: 20px; }\n.opportunities-extension-projection-alert:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-modal .opportunities-extension-projection-alert:disabled { color: #8f8a85; opacity: .55; cursor: not-allowed; background: transparent; }\n.opportunities-extension-products-ramp { cursor: pointer; font-family: inherit; }\n.opportunities-extension-products-ramp[data-booking=\"true\"] { cursor: default; color: #714016; background: #fff0df; border-color: #c58b50; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-products-ramp[data-booking=\"true\"] { color: #ffdbb5; background: #4b321e; border-color: #c99760; }\n.opportunities-extension-products-ramp:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }\n.opportunities-extension-projection-product { max-width: 100%; margin: 0 0 16px; padding: 8px; font: inherit; color: inherit; background: inherit; }\n.opportunities-extension-action-form-dialog.opportunities-extension-projection-dialog { width: min(850px, 100%); display: flex; flex-direction: column; overflow: hidden; }\n.opportunities-extension-projection-dialog > header { flex: 0 0 auto; padding: 18px 20px; }\n.opportunities-extension-projection-dialog .opportunities-extension-action-form-heading { min-width: 0; overflow-wrap: anywhere; }\n.opportunities-extension-projection-body { min-height: 0; overflow: auto; padding: 16px 20px 0; }\n.opportunities-extension-projection-context { display: grid; grid-template-columns: max-content minmax(0, 1fr) max-content minmax(0, 1fr); gap: 8px 12px; margin: 0 0 16px; font-size: 12px; }\n.opportunities-extension-projection-context dt { font-weight: 600; }\n.opportunities-extension-projection-context dd { margin: 0; overflow-wrap: anywhere; }\n.opportunities-extension-projection-summary { font-size: 13px; font-weight: 600; color: #815400; margin: 0 0 12px; }\n.opportunities-extension-projection-notice { font-size: 12px; }\n.opportunities-extension-projection-viewport { overflow-x: auto; }\n.opportunities-extension-projection-table { width: 100%; border-collapse: collapse; font-size: 12px; font-variant-numeric: tabular-nums; }\n.opportunities-extension-projection-table th, .opportunities-extension-projection-table td { padding: 9px 12px; border-bottom: 1px solid #dedbd7; text-align: right; white-space: nowrap; }\n.opportunities-extension-projection-table th:first-child { text-align: left; }\n.opportunities-extension-projection-table thead th { background: #f5f4f2; font-weight: 600; }\n.opportunities-extension-projection-table .opportunities-extension-sort-button { min-height: 28px; padding: 4px 18px 4px 0; justify-content: flex-end; }\n.opportunities-extension-projection-table th:first-child .opportunities-extension-sort-button { justify-content: flex-start; }\n.opportunities-extension-projection-table .opportunities-extension-sort-button::after { right: 0; font-size: 12px; }\n.opportunities-extension-projection-table tbody th { font-weight: 400; }\n.opportunities-extension-projection-table tr[data-divergent=\"true\"] { background: #fff7e5; }\n.opportunities-extension-projection-table tr[data-divergent=\"true\"] td:last-child { color: #815400; font-weight: 700; }\n.opportunities-extension-projection-prorated { display: inline; color: #625d58; font-size: 10px; }\n.opportunities-extension-projection-footer { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 0; padding: 14px 20px; font-size: 11px; color: #625d58; }\n.opportunities-extension-projection-update { flex: none; padding: 8px 12px; border: 1px solid #006879; border-radius: 4px; background: #006879; color: #fff; font: inherit; font-size: 12px; cursor: pointer; }\n.opportunities-extension-projection-update:hover:not(:disabled) { background: #005563; }\n.opportunities-extension-projection-update:disabled { opacity: .6; cursor: wait; }\n.opportunities-extension-projection-update:focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }\n.opportunities-extension-projection-confirm { padding: 20px; }\n.opportunities-extension-projection-confirm h2 { margin: 0 0 12px; font-size: 18px; }\n.opportunities-extension-projection-confirm p { margin: 0 0 20px; font-size: 13px; line-height: 1.5; }\n.opportunities-extension-projection-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }\n.opportunities-extension-projection-cancel { padding: 8px 12px; border: 1px solid #8b8580; border-radius: 4px; background: transparent; color: inherit; font: inherit; font-size: 12px; cursor: pointer; }\n.opportunities-extension-projection-error { color: #b3261e; font-size: 12px; overflow-wrap: anywhere; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-error { color: #ffb4ab; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-alert,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-summary,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-table tr[data-divergent=\"true\"] td:last-child { color: #ffd080; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-table thead th { background: #34312e; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-table th,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-table td { border-color: #504b46; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-table tr[data-divergent=\"true\"] { background: #3e3425; }\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-prorated,\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-footer { color: #c9c3bc; }\n@media (max-width: 600px) {\n .opportunities-extension-projection-context { grid-template-columns: max-content minmax(0, 1fr); }\n .opportunities-extension-projection-dialog > header { padding: 14px 12px; }\n .opportunities-extension-projection-body { padding: 12px 12px 0; }\n .opportunities-extension-projection-table th, .opportunities-extension-projection-table td { padding: 8px 6px; }\n .opportunities-extension-projection-footer { padding: 12px; }\n}\n\n.oe-org-modal { grid-template-columns: minmax(0, 1fr); }\n.oe-org-dialog { --org-bg: #fff; --org-canvas: #faf9f8; --org-border: #dedbd7; --org-muted: #625d58; --org-accent: #00758f; --org-selected: #e3f1f4; width: min(1500px, 100%); height: min(880px, calc(100dvh - 40px)); max-height: calc(100dvh - 40px); display: flex; flex-direction: column; overflow: hidden; font-size: 13px; color: #312d2a; background: var(--org-bg); }\n.opportunities-extension-modal[data-theme=\"dark\"] .oe-org-dialog { --org-bg: #292725; --org-canvas: #211f1d; --org-border: #625d58; --org-muted: #c9c3bd; --org-accent: #80d6e8; --org-selected: #203f46; color: #f5f4f2; }\n.oe-org-modal [hidden] { display: none !important; }\n.oe-org-dialog * { box-sizing: border-box; letter-spacing: 0; }\n.oe-org-dialog > header, .oe-org-toolbar, .oe-org-footer { flex: none; }\n.oe-org-heading { min-width: 0; overflow-wrap: anywhere; }\n.oe-org-heading p { margin: 5px 0 0; font-size: 12px; color: var(--org-muted); }\n.oe-org-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding: 10px 16px; border-bottom: 1px solid var(--org-border); }\n.oe-org-tools { display: flex; align-items: center; gap: 6px; margin-left: auto; }\n.oe-org-count { color: var(--org-muted); font-variant-numeric: tabular-nums; }\n.oe-org-zoom { width: 44px; text-align: center; font-variant-numeric: tabular-nums; }\n.oe-org-modal .oe-org-button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 34px; min-height: 34px; padding: 6px 9px; border: 1px solid var(--org-border, #8f8983); border-radius: 4px; background: transparent; color: inherit; font: inherit; cursor: pointer; }\n.oe-org-modal svg { width: 17px; height: 17px; flex: none; }\n.oe-org-dialog button:hover:not(:disabled) { background: var(--org-selected); }\n.oe-org-dialog :focus-visible { outline: 2px solid var(--org-accent); outline-offset: 2px; }\n.oe-org-modal .oe-org-primary { background: #006879; border-color: #006879; color: #fff; }\n.oe-org-dialog .oe-org-primary:hover:not(:disabled) { background: #005563; }\n.oe-org-dialog :disabled { opacity: .5; cursor: not-allowed; }\n.oe-org-workspace { min-height: 0; min-width: 0; flex: 1; display: flex; overflow: hidden; }\n.oe-org-viewport { position: relative; min-width: 0; min-height: 0; flex: 1; overflow: auto; background: var(--org-canvas); overscroll-behavior: contain; cursor: grab; }\n.oe-org-sizing { position: relative; margin-inline: auto; }\n.oe-org-canvas { position: relative; transform-origin: top left; }\n.oe-org-canvas .oe-org-lines { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }\n.oe-org-lines path { stroke: var(--org-border); stroke-width: 2; fill: none; }\n.oe-org-node { position: absolute; width: 236px; height: 146px; border: 1px solid var(--org-border); border-top: 3px solid var(--org-accent); border-radius: 4px; background: var(--org-bg); display: flex; flex-direction: column; cursor: default; }\n.oe-org-node[data-selected=\"true\"] { outline: 2px solid var(--org-accent); outline-offset: 2px; }\n.oe-org-person { flex: 1; min-height: 0; display: flex; flex-direction: column; justify-content: center; gap: 6px; text-align: left; padding: 9px 12px; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; border-radius: 0; width: 100%; }\n.oe-org-person strong, .oe-org-person > span { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; overflow-wrap: anywhere; line-height: 18px; }\n.oe-org-person > span { color: var(--org-muted); font-size: 12px; }\n.oe-org-node-actions { flex: none; display: flex; align-items: center; gap: 4px; padding: 4px 8px; height: 38px; border-top: 1px solid var(--org-border); }\n.oe-org-node-actions .oe-org-button { min-height: 28px; height: 28px; min-width: 28px; padding: 3px; border: 0; }\n.oe-org-node-actions [aria-expanded] { margin-left: auto; font-size: 11px; }\n.oe-org-note-marker { display: inline-flex; color: var(--org-muted); margin-left: auto; }\n.oe-org-editor { flex: 0 0 310px; width: 310px; padding: 16px; border-left: 1px solid var(--org-border); overflow-y: auto; background: var(--org-bg); }\n.oe-org-editor-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 16px; }\n.oe-org-editor h3 { margin: 0; font-size: 15px; }\n.oe-org-form { display: grid; gap: 16px; }\n.oe-org-field { display: grid; gap: 6px; font-size: 12px; font-weight: 600; min-width: 0; }\n.oe-org-dialog input, .oe-org-dialog textarea { min-width: 0; width: 100%; font: inherit; font-weight: 400; line-height: 20px; color: inherit; background: var(--org-bg); border: 1px solid var(--org-muted); border-radius: 3px; padding: 7px 8px; }\n.oe-org-dialog textarea { resize: vertical; min-height: 100px; max-height: 300px; }\n.oe-org-form-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }\n.oe-contact-name { position: relative; }\n.oe-org-dialog .oe-contact-name > input { padding-right: 42px; }\n.oe-org-modal .oe-contact-name > .oe-org-button { position: absolute; right: 2px; top: 2px; min-height: 30px; height: 30px; border: 0; }\n.oe-org-dialog.oe-contact-dialog { width: min(680px, 100%); height: auto; padding: 20px; gap: 20px; overflow-y: auto; }\n.oe-contact-dialog > header { margin: 0; }\n.oe-org-dialog.oe-contact-opportunities { width: min(1200px, 100%); height: auto; padding: 20px; gap: 16px; }\n.oe-contact-opportunities > header { margin: 0; }\n.oe-contact-opportunities-table { overflow: auto; min-height: 0; }\n.oe-contact-opportunities table { width: 100%; border-collapse: collapse; font-size: 12px; }\n.oe-contact-opportunities th, .oe-contact-opportunities td { padding: 12px; text-align: left; border-bottom: 1px solid var(--org-border); }\n.oe-contact-opportunities th { background: var(--org-canvas); white-space: nowrap; }\n.oe-contact-opportunities .opportunities-extension-sort-button { white-space: nowrap; min-width: max-content; min-height: 28px; padding: 4px 18px 4px 0; justify-content: flex-start; text-align: left; }\n.oe-contact-opportunities .opportunities-extension-sort-button::after { right: 0; }\n.oe-contact-opportunities th:nth-child(5), .oe-contact-opportunities td:nth-child(5) { text-align: right; font-variant-numeric: tabular-nums; }\n.oe-contact-opportunities th:nth-child(5) .opportunities-extension-sort-button { padding: 4px 0 4px 18px; justify-content: flex-end; text-align: right; }\n.oe-contact-opportunities th:nth-child(5) .opportunities-extension-sort-button::after { left: 0; right: auto; }\n.oe-contact-opportunities td:nth-child(5) .oe-contact-skeleton { margin-left: auto; }\n.oe-contact-opportunities .opportunities-extension-sort-label { white-space: nowrap; overflow-wrap: normal; word-break: normal; }\n.oe-contact-opportunities table { min-width: 900px; }\n.oe-contact-opportunities a { color: var(--org-accent); }\n.oe-contact-skeleton { display: block; height: 14px; min-width: 24px; border-radius: 4px; background: var(--org-border); animation: oe-time-pulse 1.3s ease-in-out infinite alternate; }\n.oe-contact-skeleton-total { width: 110px; }\n.oe-contact-opportunities table[aria-busy=\"true\"] { table-layout: fixed; }\n.oe-contact-opportunities table[aria-busy=\"true\"] th:nth-child(3) { width: 28%; }\n@media (prefers-reduced-motion: reduce) { .oe-contact-skeleton { animation: none; } }\n.oe-contact-dialog [aria-invalid=\"true\"] { border-color: #b3261e; outline: 1px solid #b3261e; }\n.opportunities-extension-modal[data-theme=\"dark\"] .oe-contact-dialog [aria-invalid=\"true\"] { border-color: #ffaaa4; outline-color: #ffaaa4; }\n.oe-contact-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }\n.oe-contact-phone { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; border: 1px solid var(--org-border); border-radius: 4px; padding: 12px; min-width: 0; margin: 0; }\n.oe-contact-form legend { font-weight: 600; padding: 0 4px; }\n.oe-contact-primary { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 20px; border: 0; padding: 0; margin: 0; }\n.oe-contact-primary label { display: inline-flex; align-items: center; gap: 6px; margin-top: 8px; }\n.oe-contact-primary input { width: auto; accent-color: var(--org-accent); }\n@media (max-width: 520px) { .oe-contact-form, .oe-contact-phone { grid-template-columns: minmax(0, 1fr); } }\n.oe-org-dialog .oe-org-danger, .oe-org-error { color: #b3261e; }\n.opportunities-extension-modal[data-theme=\"dark\"] .oe-org-dialog .oe-org-danger, .opportunities-extension-modal[data-theme=\"dark\"] .oe-org-error { color: #ffaaa4; }\n.oe-org-footer { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-top: 1px solid var(--org-border); }\n.oe-org-status { margin: 0 auto 0 0; font-size: 12px; overflow-wrap: anywhere; }\n.oe-org-footer .oe-org-button { flex: none; }\n.oe-org-empty { display: grid; place-content: center; justify-items: center; height: 100%; min-height: 180px; gap: 12px; }\n.oe-org-empty > svg { width: 40px; height: 40px; color: var(--org-accent); }\n.oe-org-empty h3 { margin: 0; font-size: 16px; }\n.oe-org-tooltip { position: fixed; z-index: 20; max-width: min(360px, calc(100vw - 24px)); max-height: min(260px, 45dvh); overflow-y: auto; padding: 12px; background: #fff; color: #312d2a; border: 1px solid #8f8983; border-radius: 4px; box-shadow: 0 3px 12px #0002; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; line-height: 18px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .oe-org-tooltip { background: #35322f; color: #f5f4f2; }\n.oe-org-tooltip { pointer-events: none; }\n.oe-org-confirm { position: absolute; inset: 0; display: grid; place-items: center; background: #0006; z-index: 30; padding: 16px; }\n.oe-org-confirm-panel { width: min(430px, 100%); background: #fff; color: #312d2a; border: 1px solid #8f8983; border-radius: 6px; padding: 24px; overflow-wrap: anywhere; }\n.oe-org-confirm-panel h3 { margin: 0 0 12px; font-size: 18px; }\n.oe-org-confirm-panel p { font-size: 13px; line-height: 20px; margin: 0 0 24px; }\n.opportunities-extension-modal[data-theme=\"dark\"] .oe-org-confirm-panel { background: #292725; color: #f5f4f2; }\n.oe-org-skeleton { width: 236px; height: 146px; margin: 28px; border-radius: 4px; background: var(--org-border); animation: oe-time-pulse 1.3s ease-in-out infinite alternate; }\n@media (prefers-reduced-motion: reduce) { .oe-org-skeleton { animation: none; } }\n@media (max-width: 760px) {\n .oe-org-modal { padding: 8px; }\n .oe-org-dialog { height: calc(100dvh - 16px); max-height: calc(100dvh - 16px); }\n .oe-org-workspace { flex-direction: column; }\n .oe-org-editor { flex: 0 1 55%; width: 100%; border-left: 0; border-top: 1px solid var(--org-border); }\n .oe-org-viewport { min-height: 140px; }\n .oe-org-toolbar { padding: 8px; gap: 6px; }\n .oe-org-tools { gap: 2px; }\n .oe-org-footer { padding: 8px; }\n}\n\n/* Shared data-table presentation. Calendar grids are intentionally not data tables. */\n.opportunities-extension-modal :is(\n .opportunities-extension-table-surface,\n .opportunities-extension-install-base-content,\n .opportunities-extension-service-requests-content,\n .opportunities-extension-ratecard-viewport,\n .opportunities-extension-projection-viewport,\n .oe-time-grid,\n .oe-contact-opportunities-table\n) {\n box-sizing: border-box;\n min-width: 0;\n border: 1px solid var(--oe-table-heading-border);\n border-radius: 4px;\n overflow: auto;\n}\n/* Unpadded dialogs need an inset on the scrolling frame itself. */\n.opportunities-extension-modal :is(\n .opportunities-extension-install-base-content,\n .opportunities-extension-service-requests-content,\n .opportunities-extension-ratecard-viewport\n) { margin: 16px; }\n/* Other frames already sit inside a padded body: keep a single gutter. */\n.opportunities-extension-modal :is(\n .opportunities-extension-body,\n .opportunities-extension-projection-body,\n .opportunities-extension-products-detail,\n .oe-time-body,\n .oe-org-dialog.oe-contact-opportunities\n) { padding: 16px; }\n.opportunities-extension-modal table.opportunities-extension-products-table {\n border: 1px solid var(--oe-table-heading-border);\n}\n.opportunities-extension-modal {\n --oe-table-heading: #faf9f8;\n --oe-table-text: #312d2a;\n --oe-table-border: #ebe8e5;\n --oe-table-heading-border: #b8b2ad;\n --oe-table-hover: #f5f4f2;\n}\n.opportunities-extension-modal[data-theme=\"dark\"] {\n --oe-table-heading: #333130;\n --oe-table-text: #f6f4f2;\n --oe-table-border: #3f3c39;\n --oe-table-heading-border: #4e4a46;\n --oe-table-hover: #35322f;\n}\n.opportunities-extension-modal table[data-table-layout=\"standard\"]:not(.fc-scrollgrid) {\n width: 100%; border-collapse: collapse; border-spacing: 0; table-layout: auto;\n color: var(--oe-table-text); font-size: 12px; line-height: 1.35;\n}\n.opportunities-extension-modal table[data-table-layout=\"standard\"] > thead > tr > th {\n padding: 10px 12px; height: 44px; vertical-align: middle; text-align: left;\n white-space: nowrap; overflow-wrap: normal; word-break: normal;\n font-size: 12px; font-weight: 700; color: var(--oe-table-text);\n background: var(--oe-table-heading); border-bottom: 1px solid var(--oe-table-heading-border);\n}\n.opportunities-extension-modal table[data-table-layout=\"standard\"] > tbody > tr > td {\n padding: 10px 12px; vertical-align: middle; text-align: left;\n border-bottom: 1px solid var(--oe-table-border);\n}\n.opportunities-extension-modal table[data-table-layout=\"standard\"] > tbody > tr:hover { background: var(--oe-table-hover); }\n.opportunities-extension-modal table[data-table-layout=\"standard\"] > thead > tr > th .opportunities-extension-sort-button,\n.opportunities-extension-modal table[data-table-layout=\"standard\"] .opportunities-extension-ratecard-sort {\n display: flex; align-items: center; justify-content: flex-start; gap: 8px;\n width: 100%; min-width: max-content; min-height: 28px; padding: 4px 18px 4px 0;\n border: 0; border-radius: 0; background: transparent; color: inherit;\n text-align: left; white-space: nowrap; overflow-wrap: normal; font-weight: 700;\n}\n.opportunities-extension-modal table[data-table-layout=\"standard\"] .opportunities-extension-sort-label { white-space: nowrap; overflow-wrap: normal; word-break: normal; }\n.opportunities-extension-modal table[data-table-layout=\"standard\"] > thead > tr > th .opportunities-extension-sort-button::after { position: absolute; right: 0; left: auto; font-size: 12px; }\n.opportunities-extension-modal table[data-table-layout=\"standard\"] .opportunities-extension-ratecard-sort-icon { margin-left: auto; }\n/* Currency columns keep their heading and value on the same right edge. */\n.opportunities-extension-modal table[data-table-layout=\"standard\"] :is(th, td)[data-align=\"money\"] { text-align: right; font-variant-numeric: tabular-nums; }\n.opportunities-extension-modal table[data-table-layout=\"standard\"] > thead > tr > th[data-align=\"money\"] .opportunities-extension-sort-button,\n.opportunities-extension-modal table[data-table-layout=\"standard\"] th[data-align=\"money\"] .opportunities-extension-ratecard-sort { justify-content: flex-end; text-align: right; padding: 4px 0 4px 18px; }\n.opportunities-extension-modal table[data-table-layout=\"standard\"] > thead > tr > th[data-align=\"money\"] .opportunities-extension-sort-button::after { left: 0; right: auto; }\n.opportunities-extension-modal table[data-table-layout=\"standard\"] th[data-align=\"money\"] .opportunities-extension-ratecard-sort-icon { order: -1; margin-left: 0; margin-right: auto; }\n.opportunities-extension-modal table[data-table-layout=\"standard\"] td[data-align=\"money\"] > span[class*=\"skeleton\"] { margin-left: auto; }\n/* Preserve room for Overview badges and the compact daily entry controls. */\n.opportunities-extension-modal table.opportunities-extension-table > tbody > tr > td:is([data-column=\"name\"], [data-column=\"customer\"]) { padding-bottom: 10px; vertical-align: top; }\n.opportunities-extension-modal table.opportunities-extension-table > tbody > tr > td[data-column] > .opportunities-extension-cell-content {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-height: 0;\n}\n.opportunities-extension-modal table.opportunities-extension-table > tbody > tr > td[data-column] > .opportunities-extension-cell-content > .opportunities-extension-cell-actions {\n position: static;\n height: 24px;\n flex-shrink: 0;\n}\n.opportunities-extension-modal .oe-time-grid table[data-table-layout=\"standard\"] > :is(thead, tbody) > tr > :is(th, td) { padding-left: 5px; padding-right: 5px; }\n.opportunities-extension-modal .oe-time-grid table[data-table-layout=\"standard\"] { table-layout: fixed; }\n.opportunities-extension-modal table[data-table-layout=\"standard\"] > tbody > tr > th[scope=\"row\"] { padding: 10px 12px; text-align: left; background: var(--oe-table-heading); border-bottom: 1px solid var(--oe-table-border); }\n/* Rate Card must fit its drawer; headings may wrap at spaces, never mid-word. */\n/* Overview reserves nearly half its width for the two primary descriptions. */\n.opportunities-extension-modal table.opportunities-extension-table[data-table-layout=\"standard\"] > :is(thead, tbody) > tr > :is(th, td)[data-column],\n.opportunities-extension-modal table.opportunities-extension-table[data-table-layout=\"standard\"] > :is(thead, tbody) > tr > :is(th, td)[data-column] :is(a, span, button) {\n font-family: \"Oracle Sans\", Arial, Helvetica, sans-serif;\n font-size: 12px;\n}\n.opportunities-extension-modal table.opportunities-extension-table[data-table-layout=\"standard\"] { table-layout: fixed; }\n.opportunities-extension-modal .opportunities-extension-table > thead > tr > th[data-column=\"name\"] { width: 24%; }\n.opportunities-extension-modal .opportunities-extension-table > thead > tr > th[data-column=\"customer\"] { width: 24%; }\n.opportunities-extension-modal .opportunities-extension-table > thead > tr > th[data-column=\"optyNumber\"] { width: 7%; }\n.opportunities-extension-modal .opportunities-extension-table > thead > tr > th[data-column=\"owner\"] { width: 7%; }\n.opportunities-extension-modal .opportunities-extension-table > thead > tr > th[data-column=\"revenue\"] { width: 8%; }\n.opportunities-extension-modal .opportunities-extension-table > thead > tr > th[data-column=\"closeDate\"] { width: 7%; }\n.opportunities-extension-modal .opportunities-extension-table > thead > tr > th[data-column=\"status\"] { width: 5%; }\n.opportunities-extension-modal .opportunities-extension-table > thead > tr > th[data-column=\"winProbability\"] { width: 4%; }\n.opportunities-extension-modal .opportunities-extension-table > thead > tr > th[data-column=\"stage\"] { width: 6%; }\n.opportunities-extension-modal .opportunities-extension-table > thead > tr > th[data-column=\"lastUpdateDate\"] { width: 8%; }\n.opportunities-extension-modal table.opportunities-extension-table[data-table-layout=\"standard\"] > :is(thead, tbody) > tr > :is(th, td) { padding-inline: 6px; }\n.opportunities-extension-modal table.opportunities-extension-table[data-table-layout=\"standard\"] > thead > tr > th .opportunities-extension-sort-button { min-width: 0; gap: 4px; }\n.opportunities-extension-modal table.opportunities-extension-table[data-table-layout=\"standard\"] .opportunities-extension-sort-label { overflow: hidden; text-overflow: ellipsis; }\n\n.opportunities-extension-modal table.opportunities-extension-ratecard-table[data-table-layout=\"standard\"] {\n min-width: 0;\n table-layout: fixed;\n}\n.opportunities-extension-modal table.opportunities-extension-ratecard-table[data-table-layout=\"standard\"] > :is(thead, tbody) > tr > :is(th, td) {\n padding-inline: 6px;\n}\n.opportunities-extension-modal table.opportunities-extension-ratecard-table[data-table-layout=\"standard\"] > thead > tr > th,\n.opportunities-extension-modal table.opportunities-extension-ratecard-table[data-table-layout=\"standard\"] > thead > tr > th .opportunities-extension-ratecard-sort {\n white-space: normal;\n overflow-wrap: normal;\n word-break: normal;\n hyphens: none;\n min-width: 0;\n}\n.opportunities-extension-modal table.opportunities-extension-ratecard-table[data-table-layout=\"standard\"] > thead > tr > th .opportunities-extension-ratecard-sort {\n padding: 4px 0;\n gap: 4px;\n}\n.opportunities-extension-modal .opportunities-extension-ratecard-sort > span:first-child { min-width: 0; }\n.opportunities-extension-modal .opportunities-extension-ratecard-sort-icon { flex: 0 0 12px; }\n\n/* Modal titles follow the customer Service Requests heading. Keep section and\n calendar headings independent from dialog typography. */\n.opportunities-extension-modal :is(.opportunities-extension-action-form-dialog,\n .opportunities-extension-preferences-dialog, .opportunities-extension-account-plans-dialog,\n .oe-org-confirm-panel) :is(header, h2, h3) {\n cursor: grab;\n touch-action: none;\n user-select: none;\n}\n.opportunities-extension-modal [data-dialog-dragging=\"true\"] :is(header, h2, h3) { cursor: grabbing; }\n.opportunities-extension-modal #opportunities-extension-bookmark-hint {\n background: #9a4600;\n color: #ffffff;\n border: 1px solid #b95b00;\n}\n.opportunities-extension-modal[data-theme=\"dark\"] #opportunities-extension-bookmark-hint {\n background: #ffca70;\n color: #3b2400;\n border-color: #ffd99a;\n}\n.opportunities-extension-modal :is(\n .opportunities-extension-header,\n .opportunities-extension-account-plans-header,\n .opportunities-extension-preferences-header\n) {\n padding: 16px;\n gap: 16px;\n align-items: flex-start;\n}\n/* The main heading, status badges and toolbar share one vertical center. */\n.opportunities-extension-modal .opportunities-extension-header {\n align-items: center;\n}\n.opportunities-extension-modal .opportunities-extension-header .opportunities-extension-title-row,\n.opportunities-extension-modal .opportunities-extension-header .opportunities-extension-subtitle-row,\n.opportunities-extension-modal .opportunities-extension-header .opportunities-extension-header-actions {\n align-items: center;\n}\n.opportunities-extension-modal .opportunities-extension-header .opportunities-extension-subtitle-row {\n margin-block: 0;\n}\n/* These headers live in panels that already supply the outer inset. */\n.opportunities-extension-modal :is(.oe-org-dialog.oe-contact-dialog,\n .oe-org-confirm-panel, .opportunities-extension-projection-confirm,\n .opportunities-extension-debug-panel) { padding: 16px; }\n.opportunities-extension-modal :is(.oe-org-editor-header,\n .opportunities-extension-debug-panel-header) {\n padding: 0;\n gap: 16px;\n align-items: flex-start;\n}\n.opportunities-extension-modal :is(\n .opportunities-extension-account-plans-header,\n .opportunities-extension-preferences-header,\n .opportunities-extension-debug-panel-header\n) p {\n margin: 5px 0 0;\n font-family: \"Oracle Sans\", Arial, Helvetica, sans-serif;\n font-size: 13px;\n font-weight: 400;\n line-height: 1.35;\n letter-spacing: normal;\n overflow-wrap: anywhere;\n}\n/* Search controls share the 16px inset of their table frames. */\n.opportunities-extension-modal :is(.opportunities-extension-customer-table-toolbar,\n .opportunities-extension-ratecard-toolbar,\n .opportunities-extension-ratecard-plan-details) { padding-inline: 16px; }\n.opportunities-extension-modal .opportunities-extension-customer-table-toolbar {\n padding-block: 12px;\n}\n.opportunities-extension-modal .opportunities-extension-customer-table-search {\n width: min(420px, 100%);\n height: 38px;\n padding: 8px 12px;\n font-size: 13px;\n line-height: 20px;\n}\n.opportunities-extension-modal :is(\n .opportunities-extension-header,\n .opportunities-extension-account-plans-header,\n .opportunities-extension-preferences-header,\n .opportunities-extension-debug-panel-header,\n .oe-org-editor-header\n) :is(h1, h2, h3),\n.opportunities-extension-modal .opportunities-extension-projection-confirm h2,\n.opportunities-extension-modal .oe-org-confirm-panel h3 {\n margin: 0;\n font-family: \"Oracle Sans\", Arial, Helvetica, sans-serif;\n font-size: 20px;\n font-weight: 700;\n line-height: 24px;\n letter-spacing: normal;\n color: #312d2a;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.opportunities-extension-modal[data-theme=\"dark\"] :is(\n .opportunities-extension-header,\n .opportunities-extension-account-plans-header,\n .opportunities-extension-preferences-header,\n .opportunities-extension-debug-panel-header,\n .oe-org-editor-header\n) :is(h1, h2, h3),\n.opportunities-extension-modal[data-theme=\"dark\"] .opportunities-extension-projection-confirm h2,\n.opportunities-extension-modal[data-theme=\"dark\"] .oe-org-confirm-panel h3 {\n color: #f6f4f2;\n}\n"; function loadOpportunityCalendarLibrary() { /* MIT License Copyright (c) 2021 Adam Shaw Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*! FullCalendar Standard Bundle v6.1.19 Docs & License: https://fullcalendar.io/docs/initialize-globals (c) 2024 Adam Shaw */ var FullCalendar=function(e){"use strict";var t,n,r,i,s,o,a,l,c,d={},u=[],h=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function f(e,t){for(var n in t)e[n]=t[n];return e}function g(e){var t=e.parentNode;t&&t.removeChild(e)}function p(e,n,r){var i,s,o,a={};for(o in n)"key"==o?i=n[o]:"ref"==o?s=n[o]:a[o]=n[o];if(arguments.length>2&&(a.children=arguments.length>3?t.call(arguments,2):r),"function"==typeof e&&null!=e.defaultProps)for(o in e.defaultProps)void 0===a[o]&&(a[o]=e.defaultProps[o]);return m(e,a,i,s,null)}function m(e,t,i,s,o){var a={type:e,props:t,key:i,ref:s,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==o?++r:o};return null==o&&null!=n.vnode&&n.vnode(a),a}function v(){return{current:null}}function y(e){return e.children}function b(e,t,n){"-"===t[0]?e.setProperty(t,null==n?"":n):e[t]=null==n?"":"number"!=typeof n||h.test(t)?n:n+"px"}function E(e,t,n,r,i){var s;e:if("style"===t)if("string"==typeof n)e.style.cssText=n;else{if("string"==typeof r&&(e.style.cssText=r=""),r)for(t in r)n&&t in n||b(e.style,t,"");if(n)for(t in n)r&&n[t]===r[t]||b(e.style,t,n[t])}else if("o"===t[0]&&"n"===t[1])s=t!==(t=t.replace(/Capture$/,"")),t=t.toLowerCase()in e?t.toLowerCase().slice(2):t.slice(2),e.l||(e.l={}),e.l[t+s]=n,n?r||e.addEventListener(t,s?A:S,s):e.removeEventListener(t,s?A:S,s);else if("dangerouslySetInnerHTML"!==t){if(i)t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!==t&&"height"!==t&&"href"!==t&&"list"!==t&&"form"!==t&&"tabIndex"!==t&&"download"!==t&&t in e)try{e[t]=null==n?"":n;break e}catch(e){}"function"==typeof n||(null==n||!1===n&&-1==t.indexOf("-")?e.removeAttribute(t):e.setAttribute(t,n))}}function S(e){s=!0;try{return this.l[e.type+!1](n.event?n.event(e):e)}finally{s=!1}}function A(e){s=!0;try{return this.l[e.type+!0](n.event?n.event(e):e)}finally{s=!1}}function D(e,t){this.props=e,this.context=t}function w(e,t){if(null==t)return e.__?w(e.__,e.__.__k.indexOf(e)+1):null;for(var n;tt&&o.sort((function(e,t){return e.__v.__b-t.__v.__b})));_.__r=0}function T(e,t,n,r,i,s,o,a,l,c){var h,f,g,p,v,b,E,S=r&&r.__k||u,A=S.length;for(n.__k=[],h=0;h0?m(p.type,p.props,p.key,p.ref?p.ref:null,p.__v):p)){if(p.__=n,p.__b=n.__b+1,null===(g=S[h])||g&&p.key==g.key&&p.type===g.type)S[h]=void 0;else for(f=0;f=0;t--)if((n=e.__k[t])&&(r=O(n)))return r;return null}function N(e,t,r,i,s,o,a,l,c){var d,u,h,g,p,m,v,b,E,S,A,w,C,R,x,_=t.type;if(void 0!==t.constructor)return null;null!=r.__h&&(c=r.__h,l=t.__e=r.__e,t.__h=null,o=[l]),(d=n.__b)&&d(t);try{e:if("function"==typeof _){if(b=t.props,E=(d=_.contextType)&&i[d.__c],S=d?E?E.props.value:d.__:i,r.__c?v=(u=t.__c=r.__c).__=u.__E:("prototype"in _&&_.prototype.render?t.__c=u=new _(b,S):(t.__c=u=new D(b,S),u.constructor=_,u.render=z),E&&E.sub(u),u.props=b,u.state||(u.state={}),u.context=S,u.__n=i,h=u.__d=!0,u.__h=[],u._sb=[]),null==u.__s&&(u.__s=u.state),null!=_.getDerivedStateFromProps&&(u.__s==u.state&&(u.__s=f({},u.__s)),f(u.__s,_.getDerivedStateFromProps(b,u.__s))),g=u.props,p=u.state,u.__v=t,h)null==_.getDerivedStateFromProps&&null!=u.componentWillMount&&u.componentWillMount(),null!=u.componentDidMount&&u.__h.push(u.componentDidMount);else{if(null==_.getDerivedStateFromProps&&b!==g&&null!=u.componentWillReceiveProps&&u.componentWillReceiveProps(b,S),!u.__e&&null!=u.shouldComponentUpdate&&!1===u.shouldComponentUpdate(b,u.__s,S)||t.__v===r.__v){for(t.__v!==r.__v&&(u.props=b,u.state=u.__s,u.__d=!1),t.__e=r.__e,t.__k=r.__k,t.__k.forEach((function(e){e&&(e.__=t)})),A=0;A3;)n.pop()();if(n[1]>>1,1),t.i.removeChild(e)}}),U(p(ue,{context:t.context},e.__v),t.l)):t.l&&t.componentWillUnmount()}function fe(e,t){var n=p(he,{__v:e,i:t});return n.containerInfo=t,n}(ce.prototype=new D).__a=function(e){var t=this,n=le(t.__v),r=t.o.get(e);return r[0]++,function(i){var s=function(){t.props.revealOrder?(r.push(i),de(t,e,r)):i()};n?n(s):s()}},ce.prototype.render=function(e){this.u=null,this.o=new Map;var t=M(e.children);e.revealOrder&&"b"===e.revealOrder[0]&&t.reverse();for(var n=t.length;n--;)this.o.set(t[n],this.u=[1,0,this.u]);return e.children},ce.prototype.componentDidUpdate=ce.prototype.componentDidMount=function(){var e=this;this.o.forEach((function(t,n){de(e,n,t)}))};var ge="undefined"!=typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103,pe=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,me="undefined"!=typeof document,ve=function(e){return("undefined"!=typeof Symbol&&"symbol"==typeof Symbol()?/fil|che|rad/i:/fil|che|ra/i).test(e)};D.prototype.isReactComponent={},["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach((function(e){Object.defineProperty(D.prototype,e,{configurable:!0,get:function(){return this["UNSAFE_"+e]},set:function(t){Object.defineProperty(this,e,{configurable:!0,writable:!0,value:t})}})}));var ye=n.event;function be(){}function Ee(){return this.cancelBubble}function Se(){return this.defaultPrevented}n.event=function(e){return ye&&(e=ye(e)),e.persist=be,e.isPropagationStopped=Ee,e.isDefaultPrevented=Se,e.nativeEvent=e};var Ae={configurable:!0,get:function(){return this.class}},De=n.vnode;n.vnode=function(e){var t=e.type,n=e.props,r=n;if("string"==typeof t){var i=-1===t.indexOf("-");for(var s in r={},n){var o=n[s];me&&"children"===s&&"noscript"===t||"value"===s&&"defaultValue"in n&&null==o||("defaultValue"===s&&"value"in n&&null==n.value?s="value":"download"===s&&!0===o?o="":/ondoubleclick/i.test(s)?s="ondblclick":/^onchange(textarea|input)/i.test(s+t)&&!ve(n.type)?s="oninput":/^onfocus$/i.test(s)?s="onfocusin":/^onblur$/i.test(s)?s="onfocusout":/^on(Ani|Tra|Tou|BeforeInp|Compo)/.test(s)?s=s.toLowerCase():i&&pe.test(s)?s=s.replace(/[A-Z0-9]/g,"-$&").toLowerCase():null===o&&(o=void 0),/^oninput$/i.test(s)&&(s=s.toLowerCase(),r[s]&&(s="oninputCapture")),r[s]=o)}"select"==t&&r.multiple&&Array.isArray(r.value)&&(r.value=M(n.children).forEach((function(e){e.props.selected=-1!=r.value.indexOf(e.props.value)}))),"select"==t&&null!=r.defaultValue&&(r.value=M(n.children).forEach((function(e){e.props.selected=r.multiple?-1!=r.defaultValue.indexOf(e.props.value):r.defaultValue==e.props.value}))),e.props=r,n.class!=n.className&&(Ae.enumerable="className"in n,null!=n.className&&(r.class=n.className),Object.defineProperty(r,"className",Ae))}e.$$typeof=ge,De&&De(e)};var we=n.__r;n.__r=function(e){we&&we(e),e.__c};const Ce=[],Re=new Map;function xe(e){Ce.push(e),Re.forEach(t=>{Te(t,e)})}function _e(e){let t=Re.get(e);if(!t||!t.isConnected){if(t=e.querySelector("style[data-fullcalendar]"),!t){t=document.createElement("style"),t.setAttribute("data-fullcalendar","");const n=function(){void 0===ke&&(ke=function(){const e=document.querySelector('meta[name="csp-nonce"]');if(e&&e.hasAttribute("content"))return e.getAttribute("content");const t=document.querySelector("script[nonce]");if(t)return t.nonce||"";return""}());return ke}();n&&(t.nonce=n);const r=e===document?document.head:e,i=e===document?r.querySelector("script,link[rel=stylesheet],link[as=style],style"):r.firstChild;r.insertBefore(t,i)}Re.set(e,t),function(e){for(const t of Ce)Te(e,t)}(t)}}function Te(e,t){const{sheet:n}=e,r=n.cssRules.length;t.split("}").forEach((e,t)=>{(e=e.trim())&&n.insertRule(e+"}",r+t)})}let ke;"undefined"!=typeof document&&_e(document);xe(':root{--fc-small-font-size:.85em;--fc-page-bg-color:#fff;--fc-neutral-bg-color:hsla(0,0%,82%,.3);--fc-neutral-text-color:grey;--fc-border-color:#ddd;--fc-button-text-color:#fff;--fc-button-bg-color:#2c3e50;--fc-button-border-color:#2c3e50;--fc-button-hover-bg-color:#1e2b37;--fc-button-hover-border-color:#1a252f;--fc-button-active-bg-color:#1a252f;--fc-button-active-border-color:#151e27;--fc-event-bg-color:#3788d8;--fc-event-border-color:#3788d8;--fc-event-text-color:#fff;--fc-event-selected-overlay-color:rgba(0,0,0,.25);--fc-more-link-bg-color:#d0d0d0;--fc-more-link-text-color:inherit;--fc-event-resizer-thickness:8px;--fc-event-resizer-dot-total-width:8px;--fc-event-resizer-dot-border-width:1px;--fc-non-business-color:hsla(0,0%,84%,.3);--fc-bg-event-color:#8fdf82;--fc-bg-event-opacity:0.3;--fc-highlight-color:rgba(188,232,241,.3);--fc-today-bg-color:rgba(255,220,40,.15);--fc-now-indicator-color:red}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc{display:flex;flex-direction:column;font-size:1em}.fc,.fc *,.fc :after,.fc :before{box-sizing:border-box}.fc table{border-collapse:collapse;border-spacing:0;font-size:1em}.fc th{text-align:center}.fc td,.fc th{padding:0;vertical-align:top}.fc a[data-navlink]{cursor:pointer}.fc a[data-navlink]:hover{text-decoration:underline}.fc-direction-ltr{direction:ltr;text-align:left}.fc-direction-rtl{direction:rtl;text-align:right}.fc-theme-standard td,.fc-theme-standard th{border:1px solid var(--fc-border-color)}.fc-liquid-hack td,.fc-liquid-hack th{position:relative}@font-face{font-family:fcicons;font-style:normal;font-weight:400;src:url("data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBfAAAAC8AAAAYGNtYXAXVtKNAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5ZgYydxIAAAF4AAAFNGhlYWQUJ7cIAAAGrAAAADZoaGVhB20DzAAABuQAAAAkaG10eCIABhQAAAcIAAAALGxvY2ED4AU6AAAHNAAAABhtYXhwAA8AjAAAB0wAAAAgbmFtZXsr690AAAdsAAABhnBvc3QAAwAAAAAI9AAAACAAAwPAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpBgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6Qb//f//AAAAAAAg6QD//f//AAH/4xcEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAWIAjQKeAskAEwAAJSc3NjQnJiIHAQYUFwEWMjc2NCcCnuLiDQ0MJAz/AA0NAQAMJAwNDcni4gwjDQwM/wANIwz/AA0NDCMNAAAAAQFiAI0CngLJABMAACUBNjQnASYiBwYUHwEHBhQXFjI3AZ4BAA0N/wAMJAwNDeLiDQ0MJAyNAQAMIw0BAAwMDSMM4uINIwwNDQAAAAIA4gC3Ax4CngATACcAACUnNzY0JyYiDwEGFB8BFjI3NjQnISc3NjQnJiIPAQYUHwEWMjc2NCcB87e3DQ0MIw3VDQ3VDSMMDQ0BK7e3DQ0MJAzVDQ3VDCQMDQ3zuLcMJAwNDdUNIwzWDAwNIwy4twwkDA0N1Q0jDNYMDA0jDAAAAgDiALcDHgKeABMAJwAAJTc2NC8BJiIHBhQfAQcGFBcWMjchNzY0LwEmIgcGFB8BBwYUFxYyNwJJ1Q0N1Q0jDA0Nt7cNDQwjDf7V1Q0N1QwkDA0Nt7cNDQwkDLfWDCMN1Q0NDCQMt7gMIw0MDNYMIw3VDQ0MJAy3uAwjDQwMAAADAFUAAAOrA1UAMwBoAHcAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMhMjY1NCYjISIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAAVYRGRkR/qoRGRkRA1UFBAUOCQkVDAsZDf2rDRkLDBUJCA4FBQUFBQUOCQgVDAsZDQJVDRkLDBUJCQ4FBAVVAgECBQMCBwQECAX9qwQJAwQHAwMFAQICAgIBBQMDBwQDCQQCVQUIBAQHAgMFAgEC/oAZEhEZGRESGQAAAAADAFUAAAOrA1UAMwBoAIkAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMzFRQWMzI2PQEzMjY1NCYrATU0JiMiBh0BIyIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAgBkSEhmAERkZEYAZEhIZgBEZGREDVQUEBQ4JCRUMCxkN/asNGQsMFQkIDgUFBQUFBQ4JCBUMCxkNAlUNGQsMFQkJDgUEBVUCAQIFAwIHBAQIBf2rBAkDBAcDAwUBAgICAgEFAwMHBAMJBAJVBQgEBAcCAwUCAQL+gIASGRkSgBkSERmAEhkZEoAZERIZAAABAOIAjQMeAskAIAAAExcHBhQXFjI/ARcWMjc2NC8BNzY0JyYiDwEnJiIHBhQX4uLiDQ0MJAzi4gwkDA0N4uINDQwkDOLiDCQMDQ0CjeLiDSMMDQ3h4Q0NDCMN4uIMIw0MDOLiDAwNIwwAAAABAAAAAQAAa5n0y18PPPUACwQAAAAAANivOVsAAAAA2K85WwAAAAADqwNVAAAACAACAAAAAAAAAAEAAAPA/8AAAAQAAAAAAAOrAAEAAAAAAAAAAAAAAAAAAAALBAAAAAAAAAAAAAAAAgAAAAQAAWIEAAFiBAAA4gQAAOIEAABVBAAAVQQAAOIAAAAAAAoAFAAeAEQAagCqAOoBngJkApoAAQAAAAsAigADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGZjaWNvbnMAZgBjAGkAYwBvAG4Ac1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGZjaWNvbnMAZgBjAGkAYwBvAG4Ac2ZjaWNvbnMAZgBjAGkAYwBvAG4Ac1JlZ3VsYXIAUgBlAGcAdQBsAGEAcmZjaWNvbnMAZgBjAGkAYwBvAG4Ac0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") format("truetype")}.fc-icon{speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;display:inline-block;font-family:fcicons!important;font-style:normal;font-variant:normal;font-weight:400;height:1em;line-height:1;text-align:center;text-transform:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:1em}.fc-icon-chevron-left:before{content:"\\e900"}.fc-icon-chevron-right:before{content:"\\e901"}.fc-icon-chevrons-left:before{content:"\\e902"}.fc-icon-chevrons-right:before{content:"\\e903"}.fc-icon-minus-square:before{content:"\\e904"}.fc-icon-plus-square:before{content:"\\e905"}.fc-icon-x:before{content:"\\e906"}.fc .fc-button{border-radius:0;font-family:inherit;font-size:inherit;line-height:inherit;margin:0;overflow:visible;text-transform:none}.fc .fc-button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.fc .fc-button{-webkit-appearance:button}.fc .fc-button:not(:disabled){cursor:pointer}.fc .fc-button{background-color:transparent;border:1px solid transparent;border-radius:.25em;display:inline-block;font-size:1em;font-weight:400;line-height:1.5;padding:.4em .65em;text-align:center;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle}.fc .fc-button:hover{text-decoration:none}.fc .fc-button:focus{box-shadow:0 0 0 .2rem rgba(44,62,80,.25);outline:0}.fc .fc-button:disabled{opacity:.65}.fc .fc-button-primary{background-color:var(--fc-button-bg-color);border-color:var(--fc-button-border-color);color:var(--fc-button-text-color)}.fc .fc-button-primary:hover{background-color:var(--fc-button-hover-bg-color);border-color:var(--fc-button-hover-border-color);color:var(--fc-button-text-color)}.fc .fc-button-primary:disabled{background-color:var(--fc-button-bg-color);border-color:var(--fc-button-border-color);color:var(--fc-button-text-color)}.fc .fc-button-primary:focus{box-shadow:0 0 0 .2rem rgba(76,91,106,.5)}.fc .fc-button-primary:not(:disabled).fc-button-active,.fc .fc-button-primary:not(:disabled):active{background-color:var(--fc-button-active-bg-color);border-color:var(--fc-button-active-border-color);color:var(--fc-button-text-color)}.fc .fc-button-primary:not(:disabled).fc-button-active:focus,.fc .fc-button-primary:not(:disabled):active:focus{box-shadow:0 0 0 .2rem rgba(76,91,106,.5)}.fc .fc-button .fc-icon{font-size:1.5em;vertical-align:middle}.fc .fc-button-group{display:inline-flex;position:relative;vertical-align:middle}.fc .fc-button-group>.fc-button{flex:1 1 auto;position:relative}.fc .fc-button-group>.fc-button.fc-button-active,.fc .fc-button-group>.fc-button:active,.fc .fc-button-group>.fc-button:focus,.fc .fc-button-group>.fc-button:hover{z-index:1}.fc-direction-ltr .fc-button-group>.fc-button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0;margin-left:-1px}.fc-direction-ltr .fc-button-group>.fc-button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.fc-direction-rtl .fc-button-group>.fc-button:not(:first-child){border-bottom-right-radius:0;border-top-right-radius:0;margin-right:-1px}.fc-direction-rtl .fc-button-group>.fc-button:not(:last-child){border-bottom-left-radius:0;border-top-left-radius:0}.fc .fc-toolbar{align-items:center;display:flex;justify-content:space-between}.fc .fc-toolbar.fc-header-toolbar{margin-bottom:1.5em}.fc .fc-toolbar.fc-footer-toolbar{margin-top:1.5em}.fc .fc-toolbar-title{font-size:1.75em;margin:0}.fc-direction-ltr .fc-toolbar>*>:not(:first-child){margin-left:.75em}.fc-direction-rtl .fc-toolbar>*>:not(:first-child){margin-right:.75em}.fc-direction-rtl .fc-toolbar-ltr{flex-direction:row-reverse}.fc .fc-scroller{-webkit-overflow-scrolling:touch;position:relative}.fc .fc-scroller-liquid{height:100%}.fc .fc-scroller-liquid-absolute{bottom:0;left:0;position:absolute;right:0;top:0}.fc .fc-scroller-harness{direction:ltr;overflow:hidden;position:relative}.fc .fc-scroller-harness-liquid{height:100%}.fc-direction-rtl .fc-scroller-harness>.fc-scroller{direction:rtl}.fc-theme-standard .fc-scrollgrid{border:1px solid var(--fc-border-color)}.fc .fc-scrollgrid,.fc .fc-scrollgrid table{table-layout:fixed;width:100%}.fc .fc-scrollgrid table{border-left-style:hidden;border-right-style:hidden;border-top-style:hidden}.fc .fc-scrollgrid{border-bottom-width:0;border-collapse:separate;border-right-width:0}.fc .fc-scrollgrid-liquid{height:100%}.fc .fc-scrollgrid-section,.fc .fc-scrollgrid-section table,.fc .fc-scrollgrid-section>td{height:1px}.fc .fc-scrollgrid-section-liquid>td{height:100%}.fc .fc-scrollgrid-section>*{border-left-width:0;border-top-width:0}.fc .fc-scrollgrid-section-footer>*,.fc .fc-scrollgrid-section-header>*{border-bottom-width:0}.fc .fc-scrollgrid-section-body table,.fc .fc-scrollgrid-section-footer table{border-bottom-style:hidden}.fc .fc-scrollgrid-section-sticky>*{background:var(--fc-page-bg-color);position:sticky;z-index:3}.fc .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky>*{top:0}.fc .fc-scrollgrid-section-footer.fc-scrollgrid-section-sticky>*{bottom:0}.fc .fc-scrollgrid-sticky-shim{height:1px;margin-bottom:-1px}.fc-sticky{position:sticky}.fc .fc-view-harness{flex-grow:1;position:relative}.fc .fc-view-harness-active>.fc-view{bottom:0;left:0;position:absolute;right:0;top:0}.fc .fc-col-header-cell-cushion{display:inline-block;padding:2px 4px}.fc .fc-bg-event,.fc .fc-highlight,.fc .fc-non-business{bottom:0;left:0;position:absolute;right:0;top:0}.fc .fc-non-business{background:var(--fc-non-business-color)}.fc .fc-bg-event{background:var(--fc-bg-event-color);opacity:var(--fc-bg-event-opacity)}.fc .fc-bg-event .fc-event-title{font-size:var(--fc-small-font-size);font-style:italic;margin:.5em}.fc .fc-highlight{background:var(--fc-highlight-color)}.fc .fc-cell-shaded,.fc .fc-day-disabled{background:var(--fc-neutral-bg-color)}a.fc-event,a.fc-event:hover{text-decoration:none}.fc-event.fc-event-draggable,.fc-event[href]{cursor:pointer}.fc-event .fc-event-main{position:relative;z-index:2}.fc-event-dragging:not(.fc-event-selected){opacity:.75}.fc-event-dragging.fc-event-selected{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-event .fc-event-resizer{display:none;position:absolute;z-index:4}.fc-event-selected .fc-event-resizer,.fc-event:hover .fc-event-resizer{display:block}.fc-event-selected .fc-event-resizer{background:var(--fc-page-bg-color);border-color:inherit;border-radius:calc(var(--fc-event-resizer-dot-total-width)/2);border-style:solid;border-width:var(--fc-event-resizer-dot-border-width);height:var(--fc-event-resizer-dot-total-width);width:var(--fc-event-resizer-dot-total-width)}.fc-event-selected .fc-event-resizer:before{bottom:-20px;content:"";left:-20px;position:absolute;right:-20px;top:-20px}.fc-event-selected,.fc-event:focus{box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event-selected:before,.fc-event:focus:before{bottom:0;content:"";left:0;position:absolute;right:0;top:0;z-index:3}.fc-event-selected:after,.fc-event:focus:after{background:var(--fc-event-selected-overlay-color);bottom:-1px;content:"";left:-1px;position:absolute;right:-1px;top:-1px;z-index:1}.fc-h-event{background-color:var(--fc-event-bg-color);border:1px solid var(--fc-event-border-color);display:block}.fc-h-event .fc-event-main{color:var(--fc-event-text-color)}.fc-h-event .fc-event-main-frame{display:flex}.fc-h-event .fc-event-time{max-width:100%;overflow:hidden}.fc-h-event .fc-event-title-container{flex-grow:1;flex-shrink:1;min-width:0}.fc-h-event .fc-event-title{display:inline-block;left:0;max-width:100%;overflow:hidden;right:0;vertical-align:top}.fc-h-event.fc-event-selected:before{bottom:-10px;top:-10px}.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-start),.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-end){border-bottom-left-radius:0;border-left-width:0;border-top-left-radius:0}.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-end),.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-start){border-bottom-right-radius:0;border-right-width:0;border-top-right-radius:0}.fc-h-event:not(.fc-event-selected) .fc-event-resizer{bottom:0;top:0;width:var(--fc-event-resizer-thickness)}.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start,.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end{cursor:w-resize;left:calc(var(--fc-event-resizer-thickness)*-.5)}.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end,.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start{cursor:e-resize;right:calc(var(--fc-event-resizer-thickness)*-.5)}.fc-h-event.fc-event-selected .fc-event-resizer{margin-top:calc(var(--fc-event-resizer-dot-total-width)*-.5);top:50%}.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-start,.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end{left:calc(var(--fc-event-resizer-dot-total-width)*-.5)}.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-end,.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start{right:calc(var(--fc-event-resizer-dot-total-width)*-.5)}.fc .fc-popover{box-shadow:0 2px 6px rgba(0,0,0,.15);position:absolute;z-index:9999}.fc .fc-popover-header{align-items:center;display:flex;flex-direction:row;justify-content:space-between;padding:3px 4px}.fc .fc-popover-title{margin:0 2px}.fc .fc-popover-close{cursor:pointer;font-size:1.1em;opacity:.65}.fc-theme-standard .fc-popover{background:var(--fc-page-bg-color);border:1px solid var(--fc-border-color)}.fc-theme-standard .fc-popover-header{background:var(--fc-neutral-bg-color)}');class Me{constructor(e){this.drainedOption=e,this.isRunning=!1,this.isDirty=!1,this.pauseDepths={},this.timeoutId=0}request(e){this.isDirty=!0,this.isPaused()||(this.clearTimeout(),null==e?this.tryDrain():this.timeoutId=setTimeout(this.tryDrain.bind(this),e))}pause(e=""){let{pauseDepths:t}=this;t[e]=(t[e]||0)+1,this.clearTimeout()}resume(e="",t){let{pauseDepths:n}=this;if(e in n){if(t)delete n[e];else{n[e]-=1,n[e]<=0&&delete n[e]}this.tryDrain()}}isPaused(){return Object.keys(this.pauseDepths).length}tryDrain(){if(!this.isRunning&&!this.isPaused()){for(this.isRunning=!0;this.isDirty;)this.isDirty=!1,this.drained();this.isRunning=!1}}clear(){this.clearTimeout(),this.isDirty=!1,this.pauseDepths={}}clearTimeout(){this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=0)}drained(){this.drainedOption&&this.drainedOption()}}function Ie(e){e.parentNode&&e.parentNode.removeChild(e)}function Oe(e,t){if(e.closest)return e.closest(t);if(!document.documentElement.contains(e))return null;do{if(Ne(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}function Ne(e,t){return(e.matches||e.matchesSelector||e.msMatchesSelector).call(e,t)}function Pe(e,t){let n=e instanceof HTMLElement?[e]:e,r=[];for(let e=0;e{let r=Oe(n.target,e);r&&t.call(r,n,r)}}(n,r);return e.addEventListener(t,i),()=>{e.removeEventListener(t,i)}}const Ve=["webkitTransitionEnd","otransitionend","oTransitionEnd","msTransitionEnd","transitionend"];function Ge(e,t){let n=r=>{t(r),Ve.forEach(t=>{e.removeEventListener(t,n)})};Ve.forEach(t=>{e.addEventListener(t,n)})}function Qe(e){return Object.assign({onClick:e},qe(e))}function qe(e){return{tabIndex:0,onKeyDown(t){"Enter"!==t.key&&" "!==t.key||(e(t),t.preventDefault())}}}let Ye=0;function Ze(){return Ye+=1,String(Ye)}function Xe(){document.body.classList.add("fc-not-allowed")}function $e(){document.body.classList.remove("fc-not-allowed")}function Je(e){e.style.userSelect="none",e.style.webkitUserSelect="none",e.addEventListener("selectstart",We)}function Ke(e){e.style.userSelect="",e.style.webkitUserSelect="",e.removeEventListener("selectstart",We)}function et(e){e.addEventListener("contextmenu",We)}function tt(e){e.removeEventListener("contextmenu",We)}function nt(e){let t,n,r=[],i=[];for("string"==typeof e?i=e.split(/\s*,\s*/):"function"==typeof e?i=[e]:Array.isArray(e)&&(i=e),t=0;te.replace("$"+n,t||""),e):n}function lt(e,t){return e-t}function ct(e){return e%1==0}function dt(e){let t=e.querySelector(".fc-scrollgrid-shrink-frame"),n=e.querySelector(".fc-scrollgrid-shrink-cushion");if(!t)throw new Error("needs fc-scrollgrid-shrink-frame className");if(!n)throw new Error("needs fc-scrollgrid-shrink-cushion className");return e.getBoundingClientRect().width-t.getBoundingClientRect().width+n.getBoundingClientRect().width}const ut=["years","months","days","milliseconds"],ht=/^(-?)(?:(\d+)\.)?(\d+):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/;function ft(e,t){return"string"==typeof e?function(e){let t=ht.exec(e);if(t){let e=t[1]?-1:1;return{years:0,months:0,days:e*(t[2]?parseInt(t[2],10):0),milliseconds:e*(60*(t[3]?parseInt(t[3],10):0)*60*1e3+60*(t[4]?parseInt(t[4],10):0)*1e3+1e3*(t[5]?parseInt(t[5],10):0)+(t[6]?parseInt(t[6],10):0))}}return null}(e):"object"==typeof e&&e?gt(e):"number"==typeof e?gt({[t||"milliseconds"]:e}):null}function gt(e){let t={years:e.years||e.year||0,months:e.months||e.month||0,days:e.days||e.day||0,milliseconds:60*(e.hours||e.hour||0)*60*1e3+60*(e.minutes||e.minute||0)*1e3+1e3*(e.seconds||e.second||0)+(e.milliseconds||e.millisecond||e.ms||0)},n=e.weeks||e.week;return n&&(t.days+=7*n,t.specifiedWeeks=!0),t}function pt(e,t){return{years:e.years+t.years,months:e.months+t.months,days:e.days+t.days,milliseconds:e.milliseconds+t.milliseconds}}function mt(e,t){return{years:e.years*t,months:e.months*t,days:e.days*t,milliseconds:e.milliseconds*t}}function vt(e){return yt(e)/864e5}function yt(e){return 31536e6*e.years+2592e6*e.months+864e5*e.days+e.milliseconds}function bt(e,t){let n=null;for(let r=0;r10&&(null==t?r=r.replace("Z",""):0!==t&&(r=r.replace("Z",Vt(t,!0)))),r}function Lt(e){return e.toISOString().replace(/T.*$/,"")}function Wt(e){return e.toISOString().match(/^\d{4}-\d{2}/)[0]}function Ft(e){return ot(e.getUTCHours(),2)+":"+ot(e.getUTCMinutes(),2)+":"+ot(e.getUTCSeconds(),2)}function Vt(e,t=!1){let n=e<0?"-":"+",r=Math.abs(e),i=Math.floor(r/60),s=Math.round(r%60);return t?`${n+ot(i,2)}:${ot(s,2)}`:`GMT${n}${i}${s?":"+ot(s,2):""}`}function Gt(e,t,n){let r,i;return function(...s){if(r){if(!St(r,s)){n&&n(i);let r=e.apply(this,s);t&&t(r,i)||(i=r)}}else i=e.apply(this,s);return r=s,i}}function Qt(e,t,n){let r,i;return s=>{if(r){if(!Cn(r,s)){n&&n(i);let r=e.call(this,s);t&&t(r,i)||(i=r)}}else i=e.call(this,s);return r=s,i}}const qt={week:3,separator:9,omitZeroMinute:9,meridiem:9,omitCommas:9},Yt={timeZoneName:7,era:6,year:5,month:4,day:2,weekday:2,hour:1,minute:1,second:1},Zt=/\s*([ap])\.?m\.?/i,Xt=/,/g,$t=/\s+/g,Jt=/\u200e/g,Kt=/UTC|GMT/;class en{constructor(e){let t={},n={},r=9;for(let i in e)if(i in qt){n[i]=e[i];qt[i]<9&&(r=Math.min(qt[i],r))}else t[i]=e[i],i in Yt&&(r=Math.min(Yt[i],r));this.standardDateProps=t,this.extendedSettings=n,this.smallestUnitNum=r,this.buildFormattingFunc=Gt(tn)}format(e,t){return this.buildFormattingFunc(this.standardDateProps,this.extendedSettings,t)(e)}formatRange(e,t,n,r){let{standardDateProps:i,extendedSettings:s}=this,o=function(e,t,n){if(n.getMarkerYear(e)!==n.getMarkerYear(t))return 5;if(n.getMarkerMonth(e)!==n.getMarkerMonth(t))return 4;if(n.getMarkerDay(e)!==n.getMarkerDay(t))return 2;if(zt(e)!==zt(t))return 1;return 0}(e.marker,t.marker,n.calendarSystem);if(!o)return this.format(e,n);let a=o;!(a>1)||"numeric"!==i.year&&"2-digit"!==i.year||"numeric"!==i.month&&"2-digit"!==i.month||"numeric"!==i.day&&"2-digit"!==i.day||(a=1);let l=this.format(e,n),c=this.format(t,n);if(l===c)return l;let d=tn(function(e,t){let n={};for(let r in e)(!(r in Yt)||Yt[r]<=t)&&(n[r]=e[r]);return n}(i,a),s,n),u=d(e),h=d(t),f=function(e,t,n,r){let i=0;for(;iVt(e.timeZoneOffset):0===r&&t.week?e=>function(e,t,n,r,i){let s=[];"long"===i?s.push(n):"short"!==i&&"narrow"!==i||s.push(t);"long"!==i&&"short"!==i||s.push(" ");s.push(r.simpleNumberFormat.format(e)),"rtl"===r.options.direction&&s.reverse();return s.join("")}(n.computeWeekNumber(e.marker),n.weekText,n.weekTextLong,n.locale,t.week):function(e,t,n){e=Object.assign({},e),t=Object.assign({},t),function(e,t){e.timeZoneName&&(e.hour||(e.hour="2-digit"),e.minute||(e.minute="2-digit"));"long"===e.timeZoneName&&(e.timeZoneName="short");t.omitZeroMinute&&(e.second||e.millisecond)&&delete t.omitZeroMinute}(e,t),e.timeZone="UTC";let r,i=new Intl.DateTimeFormat(n.locale.codes,e);if(t.omitZeroMinute){let t=Object.assign({},e);delete t.minute,r=new Intl.DateTimeFormat(n.locale.codes,t)}return s=>{let o,{marker:a}=s;return o=r&&!a.getUTCMinutes()?r:i,function(e,t,n,r,i){e=e.replace(Jt,""),"short"===n.timeZoneName&&(e=function(e,t){let n=!1;e=e.replace(Kt,()=>(n=!0,t)),n||(e+=" "+t);return e}(e,"UTC"===i.timeZone||null==t.timeZoneOffset?"UTC":Vt(t.timeZoneOffset)));r.omitCommas&&(e=e.replace(Xt,"").trim());r.omitZeroMinute&&(e=e.replace(":00",""));!1===r.meridiem?e=e.replace(Zt,"").trim():"narrow"===r.meridiem?e=e.replace(Zt,(e,t)=>t.toLocaleLowerCase()):"short"===r.meridiem?e=e.replace(Zt,(e,t)=>t.toLocaleLowerCase()+"m"):"lowercase"===r.meridiem&&(e=e.replace(Zt,e=>e.toLocaleLowerCase()));return e=(e=e.replace($t," ")).trim()}(o.format(a),s,e,t,n)}}(e,t,n)}function nn(e,t){let n=t.markerToArray(e.marker);return{marker:e.marker,timeZoneOffset:e.timeZoneOffset,array:n,year:n[0],month:n[1],day:n[2],hour:n[3],minute:n[4],second:n[5],millisecond:n[6]}}function rn(e,t,n,r){let i=nn(e,n.calendarSystem);return{date:i,start:i,end:t?nn(t,n.calendarSystem):null,timeZone:n.timeZone,localeCodes:n.locale.codes,defaultSeparator:r||n.defaultSeparator}}class sn{constructor(e){this.cmdStr=e}format(e,t,n){return t.cmdFormatter(this.cmdStr,rn(e,null,t,n))}formatRange(e,t,n,r){return n.cmdFormatter(this.cmdStr,rn(e,t,n,r))}}class on{constructor(e){this.func=e}format(e,t,n){return this.func(rn(e,null,t,n))}formatRange(e,t,n,r){return this.func(rn(e,t,n,r))}}function an(e){return"object"==typeof e&&e?new en(e):"string"==typeof e?new sn(e):"function"==typeof e?new on(e):null}const ln={navLinkDayClick:yn,navLinkWeekClick:yn,duration:ft,bootstrapFontAwesome:yn,buttonIcons:yn,customButtons:yn,defaultAllDayEventDuration:ft,defaultTimedEventDuration:ft,nextDayThreshold:ft,scrollTime:ft,scrollTimeReset:Boolean,slotMinTime:ft,slotMaxTime:ft,dayPopoverFormat:an,slotDuration:ft,snapDuration:ft,headerToolbar:yn,footerToolbar:yn,defaultRangeSeparator:String,titleRangeSeparator:String,forceEventDuration:Boolean,dayHeaders:Boolean,dayHeaderFormat:an,dayHeaderClassNames:yn,dayHeaderContent:yn,dayHeaderDidMount:yn,dayHeaderWillUnmount:yn,dayCellClassNames:yn,dayCellContent:yn,dayCellDidMount:yn,dayCellWillUnmount:yn,initialView:String,aspectRatio:Number,weekends:Boolean,weekNumberCalculation:yn,weekNumbers:Boolean,weekNumberClassNames:yn,weekNumberContent:yn,weekNumberDidMount:yn,weekNumberWillUnmount:yn,editable:Boolean,viewClassNames:yn,viewDidMount:yn,viewWillUnmount:yn,nowIndicator:Boolean,nowIndicatorSnap:yn,nowIndicatorClassNames:yn,nowIndicatorContent:yn,nowIndicatorDidMount:yn,nowIndicatorWillUnmount:yn,showNonCurrentDates:Boolean,lazyFetching:Boolean,startParam:String,endParam:String,timeZoneParam:String,timeZone:String,locales:yn,locale:yn,themeSystem:String,dragRevertDuration:Number,dragScroll:Boolean,allDayMaintainDuration:Boolean,unselectAuto:Boolean,dropAccept:yn,eventOrder:nt,eventOrderStrict:Boolean,handleWindowResize:Boolean,windowResizeDelay:Number,longPressDelay:Number,eventDragMinDistance:Number,expandRows:Boolean,height:yn,contentHeight:yn,direction:String,weekNumberFormat:an,eventResizableFromStart:Boolean,displayEventTime:Boolean,displayEventEnd:Boolean,weekText:String,weekTextLong:String,progressiveEventRendering:Boolean,businessHours:yn,initialDate:yn,now:yn,eventDataTransform:yn,stickyHeaderDates:yn,stickyFooterScrollbar:yn,viewHeight:yn,defaultAllDay:Boolean,eventSourceFailure:yn,eventSourceSuccess:yn,eventDisplay:String,eventStartEditable:Boolean,eventDurationEditable:Boolean,eventOverlap:yn,eventConstraint:yn,eventAllow:yn,eventBackgroundColor:String,eventBorderColor:String,eventTextColor:String,eventColor:String,eventClassNames:yn,eventContent:yn,eventDidMount:yn,eventWillUnmount:yn,selectConstraint:yn,selectOverlap:yn,selectAllow:yn,droppable:Boolean,unselectCancel:String,slotLabelFormat:yn,slotLaneClassNames:yn,slotLaneContent:yn,slotLaneDidMount:yn,slotLaneWillUnmount:yn,slotLabelClassNames:yn,slotLabelContent:yn,slotLabelDidMount:yn,slotLabelWillUnmount:yn,dayMaxEvents:yn,dayMaxEventRows:yn,dayMinWidth:Number,slotLabelInterval:ft,allDayText:String,allDayClassNames:yn,allDayContent:yn,allDayDidMount:yn,allDayWillUnmount:yn,slotMinWidth:Number,navLinks:Boolean,eventTimeFormat:an,rerenderDelay:Number,moreLinkText:yn,moreLinkHint:yn,selectMinDistance:Number,selectable:Boolean,selectLongPressDelay:Number,eventLongPressDelay:Number,selectMirror:Boolean,eventMaxStack:Number,eventMinHeight:Number,eventMinWidth:Number,eventShortHeight:Number,slotEventOverlap:Boolean,plugins:yn,firstDay:Number,dayCount:Number,dateAlignment:String,dateIncrement:ft,hiddenDays:yn,fixedWeekCount:Boolean,validRange:yn,visibleRange:yn,titleFormat:yn,eventInteractive:Boolean,noEventsText:String,viewHint:yn,navLinkHint:yn,closeHint:String,timeHint:String,eventHint:String,moreLinkClick:yn,moreLinkClassNames:yn,moreLinkContent:yn,moreLinkDidMount:yn,moreLinkWillUnmount:yn,monthStartFormat:an,handleCustomRendering:yn,customRenderingMetaMap:yn,customRenderingReplaces:Boolean},cn={eventDisplay:"auto",defaultRangeSeparator:" - ",titleRangeSeparator:" – ",defaultTimedEventDuration:"01:00:00",defaultAllDayEventDuration:{day:1},forceEventDuration:!1,nextDayThreshold:"00:00:00",dayHeaders:!0,initialView:"",aspectRatio:1.35,headerToolbar:{start:"title",center:"",end:"today prev,next"},weekends:!0,weekNumbers:!1,weekNumberCalculation:"local",editable:!1,nowIndicator:!1,scrollTime:"06:00:00",scrollTimeReset:!0,slotMinTime:"00:00:00",slotMaxTime:"24:00:00",showNonCurrentDates:!0,lazyFetching:!0,startParam:"start",endParam:"end",timeZoneParam:"timeZone",timeZone:"local",locales:[],locale:"",themeSystem:"standard",dragRevertDuration:500,dragScroll:!0,allDayMaintainDuration:!1,unselectAuto:!0,dropAccept:"*",eventOrder:"start,-duration,allDay,title",dayPopoverFormat:{month:"long",day:"numeric",year:"numeric"},handleWindowResize:!0,windowResizeDelay:100,longPressDelay:1e3,eventDragMinDistance:5,expandRows:!1,navLinks:!1,selectable:!1,eventMinHeight:15,eventMinWidth:30,eventShortHeight:30,monthStartFormat:{month:"long",day:"numeric"},nowIndicatorSnap:"auto"},dn={datesSet:yn,eventsSet:yn,eventAdd:yn,eventChange:yn,eventRemove:yn,windowResize:yn,eventClick:yn,eventMouseEnter:yn,eventMouseLeave:yn,select:yn,unselect:yn,loading:yn,_unmount:yn,_beforeprint:yn,_afterprint:yn,_noEventDrop:yn,_noEventResize:yn,_resize:yn,_scrollRequest:yn},un={buttonText:yn,buttonHints:yn,views:yn,plugins:yn,initialEvents:yn,events:yn,eventSources:yn},hn={headerToolbar:fn,footerToolbar:fn,buttonText:fn,buttonHints:fn,buttonIcons:fn,dateIncrement:fn,plugins:gn,events:gn,eventSources:gn,resources:gn};function fn(e,t){return"object"==typeof e&&"object"==typeof t&&e&&t?Cn(e,t):e===t}function gn(e,t){return Array.isArray(e)&&Array.isArray(t)?St(e,t):e===t}const pn={type:String,component:yn,buttonText:String,buttonTextKey:String,dateProfileGeneratorClass:yn,usesMinMaxTime:Boolean,classNames:yn,content:yn,didMount:yn,willUnmount:yn};function mn(e){return En(e,hn)}function vn(e,t){let n={},r={};for(let r in t)r in e&&(n[r]=t[r](e[r]));for(let n in e)n in t||(r[n]=e[n]);return{refined:n,extra:r}}function yn(e){return e}const{hasOwnProperty:bn}=Object.prototype;function En(e,t){let n={};if(t)for(let r in t)if(t[r]===fn){let t=[];for(let i=e.length-1;i>=0;i-=1){let s=e[i][r];if("object"==typeof s&&s)t.unshift(s);else if(void 0!==s){n[r]=s;break}}t.length&&(n[r]=En(t))}for(let t=e.length-1;t>=0;t-=1){let r=e[t];for(let e in r)e in n||(n[e]=r[e])}return n}function Sn(e,t){let n={};for(let r in e)t(e[r],r)&&(n[r]=e[r]);return n}function An(e,t){let n={};for(let r in e)n[r]=t(e[r],r);return n}function Dn(e){let t={};for(let n of e)t[n]=!0;return t}function wn(e){let t=[];for(let n in e)t.push(e[n]);return t}function Cn(e,t){if(e===t)return!0;for(let n in e)if(bn.call(e,n)&&!(n in t))return!1;for(let n in t)if(bn.call(t,n)&&e[n]!==t[n])return!1;return!0}const Rn=/^on[A-Z]/;function xn(e,t,n={}){if(e===t)return!0;for(let r in t)if(!(r in e)||!_n(e[r],t[r],n[r]))return!1;for(let n in e)if(!(n in t))return!1;return!0}function _n(e,t,n){return e===t||!0===n||!!n&&n(e,t)}function Tn(e,t=0,n,r=1){let i=[];null==n&&(n=Object.keys(e).length);for(let s=t;s=1?Math.min(i,s):i}(e,this.weekDow,this.weekDoy)}format(e,t,n={}){return t.format({marker:e,timeZoneOffset:null!=n.forcedTzo?n.forcedTzo:this.offsetForMarker(e)},this)}formatRange(e,t,n,r={}){return r.isEndExclusive&&(t=Ct(t,-1)),n.formatRange({marker:e,timeZoneOffset:null!=r.forcedStartTzo?r.forcedStartTzo:this.offsetForMarker(e)},{marker:t,timeZoneOffset:null!=r.forcedEndTzo?r.forcedEndTzo:this.offsetForMarker(t)},this,r.defaultSeparator)}formatIso(e,t={}){let n=null;return t.omitTimeZoneOffset||(n=null!=t.forcedTzo?t.forcedTzo:this.offsetForMarker(e)),Ut(e,n,t.omitTime)}timestampToMarker(e){return"local"===this.timeZone?Bt(Nt(new Date(e))):"UTC"!==this.timeZone&&this.namedTimeZoneImpl?Bt(this.namedTimeZoneImpl.timestampToArray(e)):new Date(e)}offsetForMarker(e){return"local"===this.timeZone?-Pt(Ht(e)).getTimezoneOffset():"UTC"===this.timeZone?0:this.namedTimeZoneImpl?this.namedTimeZoneImpl.offsetForArray(Ht(e)):null}toDate(e,t){return"local"===this.timeZone?Pt(Ht(e)):"UTC"===this.timeZone?new Date(e.valueOf()):this.namedTimeZoneImpl?new Date(e.valueOf()-1e3*this.namedTimeZoneImpl.offsetForArray(Ht(e))*60):new Date(e.valueOf()-(t||0))}}class Hn{constructor(e){this.iconOverrideOption&&this.setIconOverride(e[this.iconOverrideOption])}setIconOverride(e){let t,n;if("object"==typeof e&&e){for(n in t=Object.assign({},this.iconClasses),e)t[n]=this.applyIconOverridePrefix(e[n]);this.iconClasses=t}else!1===e&&(this.iconClasses={})}applyIconOverridePrefix(e){let t=this.iconOverridePrefix;return t&&0!==e.indexOf(t)&&(e=t+e),e}getClass(e){return this.classes[e]||""}getIconClass(e,t){let n;return n=t&&this.rtlIconClasses&&this.rtlIconClasses[e]||this.iconClasses[e],n?`${this.baseIconClass} ${n}`:""}getCustomButtonIconClass(e){let t;return this.iconOverrideCustomButtonOption&&(t=e[this.iconOverrideCustomButtonOption],t)?`${this.baseIconClass} ${this.applyIconOverridePrefix(t)}`:""}}function Bn(e){e();let t=n.debounceRendering,r=[];for(n.debounceRendering=function(e){r.push(e)},U(p(jn,{}),document.createElement("div"));r.length;)r.shift()();n.debounceRendering=t}Hn.prototype.classes={},Hn.prototype.iconClasses={},Hn.prototype.baseIconClass="",Hn.prototype.iconOverridePrefix="";class jn extends D{render(){return p("div",{})}componentDidMount(){this.setState({})}}function zn(e){let t=function(e,t){var n={__c:t="__cC"+c++,__:e,Consumer:function(e,t){return e.children(t)},Provider:function(e){var n,r;return this.getChildContext||(n=[],(r={})[t]=this,this.getChildContext=function(){return r},this.shouldComponentUpdate=function(e){this.props.value!==e.value&&n.some((function(e){e.__e=!0,x(e)}))},this.sub=function(e){n.push(e);var t=e.componentWillUnmount;e.componentWillUnmount=function(){n.splice(n.indexOf(e),1),t&&t.call(e)}}),e.children}};return n.Provider.__=n.Consumer.contextType=n}(e),n=t.Provider;return t.Provider=function(){let e=!this.getChildContext,t=n.apply(this,arguments);if(e){let e=[];this.shouldComponentUpdate=t=>{this.props.value!==t.value&&e.forEach(e=>{e.context=t.value,e.forceUpdate()})},this.sub=t=>{e.push(t);let n=t.componentWillUnmount;t.componentWillUnmount=()=>{e.splice(e.indexOf(t),1),n&&n.call(t)}}}return t},t}class Un{constructor(e,t,n,r){this.execFunc=e,this.emitter=t,this.scrollTime=n,this.scrollTimeReset=r,this.handleScrollRequest=e=>{this.queuedRequest=Object.assign({},this.queuedRequest||{},e),this.drain()},t.on("_scrollRequest",this.handleScrollRequest),this.fireInitialScroll()}detach(){this.emitter.off("_scrollRequest",this.handleScrollRequest)}update(e){e&&this.scrollTimeReset?this.fireInitialScroll():this.drain()}fireInitialScroll(){this.handleScrollRequest({time:this.scrollTime})}drain(){this.queuedRequest&&this.execFunc(this.queuedRequest)&&(this.queuedRequest=null)}}const Ln=zn({});function Wn(e,t,n,r,i,s,o,a,l,c,d,u,h,f){return{dateEnv:i,nowManager:s,options:n,pluginHooks:a,emitter:d,dispatch:l,getCurrentData:c,calendarApi:u,viewSpec:e,viewApi:t,dateProfileGenerator:r,theme:o,isRtl:"rtl"===n.direction,addResizeHandler(e){d.on("_resize",e)},removeResizeHandler(e){d.off("_resize",e)},createScrollResponder:e=>new Un(e,d,ft(n.scrollTime),n.scrollTimeReset),registerInteractiveComponent:h,unregisterInteractiveComponent:f}}class Fn extends D{shouldComponentUpdate(e,t){return!xn(this.props,e,this.propEquality)||!xn(this.state,t,this.stateEquality)}safeSetState(e){xn(this.state,Object.assign(Object.assign({},this.state),e),this.stateEquality)||this.setState(e)}}Fn.addPropsEquality=function(e){let t=Object.create(this.prototype.propEquality);Object.assign(t,e),this.prototype.propEquality=t},Fn.addStateEquality=function(e){let t=Object.create(this.prototype.stateEquality);Object.assign(t,e),this.prototype.stateEquality=t},Fn.contextType=Ln,Fn.prototype.propEquality={},Fn.prototype.stateEquality={};class Vn extends Fn{}function Gn(e,t){"function"==typeof e?e(t):e&&(e.current=t)}Vn.contextType=Ln;class Qn extends Vn{constructor(){super(...arguments),this.id=Ze(),this.queuedDomNodes=[],this.currentDomNodes=[],this.handleEl=e=>{const{options:t}=this.context,{generatorName:n}=this.props;t.customRenderingReplaces&&qn(n,t)||this.updateElRef(e)},this.updateElRef=e=>{this.props.elRef&&Gn(this.props.elRef,e)}}render(){const{props:e,context:t}=this,{options:n}=t,{customGenerator:r,defaultGenerator:s,renderProps:o}=e,a=Yn(e,[],this.handleEl);let l,c,d=!1,u=[];if(null!=r){const e="function"==typeof r?r(o,p):r;if(!0===e)d=!0;else{const t=e&&"object"==typeof e;t&&"html"in e?a.dangerouslySetInnerHTML={__html:e.html}:t&&"domNodes"in e?u=Array.prototype.slice.call(e.domNodes):(t?i(e):"function"!=typeof e)?l=e:c=e}}else d=!qn(e.generatorName,n);return d&&s&&(l=s(o)),this.queuedDomNodes=u,this.currentGeneratorMeta=c,p(e.elTag,a,l)}componentDidMount(){this.applyQueueudDomNodes(),this.triggerCustomRendering(!0)}componentDidUpdate(){this.applyQueueudDomNodes(),this.triggerCustomRendering(!0)}componentWillUnmount(){this.triggerCustomRendering(!1)}triggerCustomRendering(e){var t;const{props:n,context:r}=this,{handleCustomRendering:i,customRenderingMetaMap:s}=r.options;if(i){const r=null!==(t=this.currentGeneratorMeta)&&void 0!==t?t:null==s?void 0:s[n.generatorName];r&&i(Object.assign(Object.assign({id:this.id,isActive:e,containerEl:this.base,reportNewContainerEl:this.updateElRef,generatorMeta:r},n),{elClasses:(n.elClasses||[]).filter(Zn)}))}}applyQueueudDomNodes(){const{queuedDomNodes:e,currentDomNodes:t}=this,n=this.base;if(!St(e,t)){t.forEach(Ie);for(let t of e)n.appendChild(t);this.currentDomNodes=e}}}function qn(e,t){var n;return Boolean(t.handleCustomRendering&&e&&(null===(n=t.customRenderingMetaMap)||void 0===n?void 0:n[e]))}function Yn(e,t,n){const r=Object.assign(Object.assign({},e.elAttrs),{ref:n});return(e.elClasses||t)&&(r.className=(e.elClasses||[]).concat(t||[]).concat(r.className||[]).filter(Boolean).join(" ")),e.elStyle&&(r.style=e.elStyle),r}function Zn(e){return Boolean(e)}Qn.addPropsEquality({elClasses:St,elStyle:Cn,elAttrs:function(e,t){const n=function(e,t){let n=[];for(let r in e)bn.call(e,r)&&(r in t||n.push(r));for(let r in t)bn.call(t,r)&&e[r]!==t[r]&&n.push(r);return n}(e,t);for(let e of n)if(!Rn.test(e))return!1;return!0},renderProps:Cn});const Xn=zn(0);class $n extends D{constructor(){super(...arguments),this.InnerContent=Jn.bind(void 0,this),this.handleEl=e=>{this.el=e,this.props.elRef&&(Gn(this.props.elRef,e),e&&this.didMountMisfire&&this.componentDidMount())}}render(){const{props:e}=this,t=function(e,t){const n="function"==typeof e?e(t):e||[];return"string"==typeof n?[n]:n}(e.classNameGenerator,e.renderProps);if(e.children){const n=Yn(e,t,this.handleEl),r=e.children(this.InnerContent,e.renderProps,n);return e.elTag?p(e.elTag,n,r):r}return p(Qn,Object.assign(Object.assign({},e),{elRef:this.handleEl,elTag:e.elTag||"div",elClasses:(e.elClasses||[]).concat(t),renderId:this.context}))}componentDidMount(){var e,t;this.el?null===(t=(e=this.props).didMount)||void 0===t||t.call(e,Object.assign(Object.assign({},this.props.renderProps),{el:this.el})):this.didMountMisfire=!0}componentWillUnmount(){var e,t;null===(t=(e=this.props).willUnmount)||void 0===t||t.call(e,Object.assign(Object.assign({},this.props.renderProps),{el:this.el}))}}function Jn(e,t){const n=e.props;return p(Qn,Object.assign({renderProps:n.renderProps,generatorName:n.generatorName,customGenerator:n.customGenerator,defaultGenerator:n.defaultGenerator,renderId:e.context},t))}$n.contextType=Xn;class Kn extends Vn{render(){let{props:e,context:t}=this,{options:n}=t,r={view:t.viewApi};return p($n,{elRef:e.elRef,elTag:e.elTag||"div",elAttrs:e.elAttrs,elClasses:[...er(e.viewSpec),...e.elClasses||[]],elStyle:e.elStyle,renderProps:r,classNameGenerator:n.viewClassNames,generatorName:void 0,didMount:n.viewDidMount,willUnmount:n.viewWillUnmount},()=>e.children)}}function er(e){return[`fc-${e.type}-view`,"fc-view"]}function tr(e,t){let n,r,i=[],{start:s}=t;for(e.sort(nr),n=0;ns&&i.push({start:s,end:r.start}),r.end>s&&(s=r.end);return st.start)&&(null===e.start||null===t.end||e.start=e.start)&&(null===e.end||null!==t.end&&t.end<=e.end)}function ar(e,t){return(null===e.start||t>=e.start)&&(null===e.end||t=yt(t)&&(r=wt(r,1))}return e.start&&(n=Mt(e.start),r&&r<=n&&(r=wt(n,1))),{start:n,end:r}}function dr(e){let t=cr(e);return xt(t.start,t.end)>1}function ur(e,t,n,r){return"year"===r?ft(n.diffWholeYears(e,t),"year"):"month"===r?ft(n.diffWholeMonths(e,t),"month"):_t(e,t)}class hr{constructor(e){this.props=e,this.initHiddenDays()}buildPrev(e,t,n){let{dateEnv:r}=this.props,i=r.subtract(r.startOf(t,e.currentRangeUnit),e.dateIncrement);return this.build(i,-1,n)}buildNext(e,t,n){let{dateEnv:r}=this.props,i=r.add(r.startOf(t,e.currentRangeUnit),e.dateIncrement);return this.build(i,1,n)}build(e,t,n=!0){let r,i,s,o,a,l,{props:c}=this;var d,u;return r=this.buildValidRange(),r=this.trimHiddenDays(r),n&&(d=e,e=null!=(u=r).start&&d=u.end?new Date(u.end.valueOf()-1):d),i=this.buildCurrentRangeInfo(e,t),s=/^(year|month|week|day)$/.test(i.unit),o=this.buildRenderRange(this.trimHiddenDays(i.range),i.unit,s),o=this.trimHiddenDays(o),a=o,c.showNonCurrentDates||(a=rr(a,i.range)),a=this.adjustActiveRange(a),a=rr(a,r),l=sr(i.range,r),ar(o,e)||(e=o.start),{currentDate:e,validRange:r,currentRange:i.range,currentRangeUnit:i.unit,isRangeAllDay:s,activeRange:a,renderRange:o,slotMinTime:c.slotMinTime,slotMaxTime:c.slotMaxTime,isValid:l,dateIncrement:this.buildDateIncrement(i.duration)}}buildValidRange(){let e=this.props.validRangeInput,t="function"==typeof e?e.call(this.props.calendarApi,this.props.dateEnv.toDate(this.props.nowManager.getDateMarker())):e;return this.refineRange(t)||{start:null,end:null}}buildCurrentRangeInfo(e,t){let n,{props:r}=this,i=null,s=null,o=null;return r.duration?(i=r.duration,s=r.durationUnit,o=this.buildRangeFromDuration(e,t,i,s)):(n=this.props.dayCount)?(s="day",o=this.buildRangeFromDayCount(e,t,n)):(o=this.buildCustomVisibleRange(e))?s=r.dateEnv.greatestWholeUnit(o.start,o.end).unit:(i=this.getFallbackDuration(),s=Et(i).unit,o=this.buildRangeFromDuration(e,t,i,s)),{duration:i,unit:s,range:o}}getFallbackDuration(){return ft({day:1})}adjustActiveRange(e){let{dateEnv:t,usesMinMaxTime:n,slotMinTime:r,slotMaxTime:i}=this.props,{start:s,end:o}=e;return n&&(vt(r)<0&&(s=Mt(s),s=t.add(s,r)),vt(i)>1&&(o=Mt(o),o=wt(o,-1),o=t.add(o,i))),{start:s,end:o}}buildRangeFromDuration(e,t,n,r){let i,s,o,{dateEnv:a,dateAlignment:l}=this.props;if(!l){let{dateIncrement:e}=this.props;l=e&&yt(e)!o[e.defId].recurringDef);for(let e in o){let n=o[e];if(n.recurringDef){let{duration:o}=n.recurringDef;o||(o=n.allDay?s.defaultAllDayEventDuration:s.defaultTimedEventDuration);let l=pr(n,o,t,r,i.recurringTypes);for(let t of l){let n=fr(e,{start:t,end:r.add(t,o)});a[n.instanceId]=n}}}return{defs:o,instances:a}}function pr(e,t,n,r,i){let s=i[e.recurringDef.typeId].expand(e.recurringDef.typeData,{start:r.subtract(n.start,t),end:n.end},r);return e.allDay&&(s=s.map(Mt)),s}const mr={id:String,groupId:String,title:String,url:String,interactive:Boolean},vr={start:yn,end:yn,date:yn,allDay:Boolean},yr=Object.assign(Object.assign(Object.assign({},mr),vr),{extendedProps:yn});function br(e,t,n,r,i=Sr(n),s,o){let{refined:a,extra:l}=Er(e,n,i),c=function(e,t){let n=null;e&&(n=e.defaultAllDay);null==n&&(n=t.options.defaultAllDay);return n}(t,n),d=function(e,t,n,r){for(let i=0;i{return n=t,r=e,Boolean(n.groupId&&n.groupId===r.groupId);var n,r});return r.defs[t.defId]=t,r.instances[n.instanceId]=n,r}return{defs:{},instances:{}}}function Rr(){return{defs:{},instances:{}}}function xr(e,t){return{defs:Object.assign(Object.assign({},e.defs),t.defs),instances:Object.assign(Object.assign({},e.instances),t.instances)}}function _r(e,t){let n=Sn(e.defs,t),r=Sn(e.instances,e=>n[e.defId]);return{defs:n,instances:r}}function Tr(e){return Array.isArray(e)?e:"string"==typeof e?e.split(/\s+/):[]}const kr={display:String,editable:Boolean,startEditable:Boolean,durationEditable:Boolean,constraint:yn,overlap:yn,allow:yn,className:Tr,classNames:Tr,color:String,backgroundColor:String,borderColor:String,textColor:String},Mr={display:null,startEditable:null,durationEditable:null,constraints:[],overlap:null,allows:[],backgroundColor:"",borderColor:"",textColor:"",classNames:[]};function Ir(e,t){let n=function(e,t){return Array.isArray(e)?Dr(e,null,t,!0):"object"==typeof e&&e?Dr([e],null,t,!0):null!=e?String(e):null}(e.constraint,t);return{display:e.display||null,startEditable:null!=e.startEditable?e.startEditable:e.editable,durationEditable:null!=e.durationEditable?e.durationEditable:e.editable,constraints:null!=n?[n]:[],overlap:null!=e.overlap?e.overlap:null,allows:null!=e.allow?[e.allow]:[],backgroundColor:e.backgroundColor||e.color||"",borderColor:e.borderColor||e.color||"",textColor:e.textColor||"",classNames:(e.className||[]).concat(e.classNames||[])}}function Or(e){return e.reduce(Nr,Mr)}function Nr(e,t){return{display:null!=t.display?t.display:e.display,startEditable:null!=t.startEditable?t.startEditable:e.startEditable,durationEditable:null!=t.durationEditable?t.durationEditable:e.durationEditable,constraints:e.constraints.concat(t.constraints),overlap:"boolean"==typeof t.overlap?t.overlap:e.overlap,allows:e.allows.concat(t.allows),backgroundColor:t.backgroundColor||e.backgroundColor,borderColor:t.borderColor||e.borderColor,textColor:t.textColor||e.textColor,classNames:e.classNames.concat(t.classNames)}}const Pr={id:String,defaultAllDay:Boolean,url:String,format:String,events:yn,eventDataTransform:yn,success:yn,failure:yn};function Hr(e,t,n=Br(t)){let r;if("string"==typeof e?r={url:e}:"function"==typeof e||Array.isArray(e)?r={events:e}:"object"==typeof e&&e&&(r=e),r){let{refined:i,extra:s}=vn(r,n),o=function(e,t){let n=t.pluginHooks.eventSourceDefs;for(let t=n.length-1;t>=0;t-=1){let r=n[t].parseMeta(e);if(r)return{sourceDefId:t,meta:r}}return null}(i,t);if(o)return{_raw:e,isFetching:!1,latestFetchId:"",fetchRange:null,defaultAllDay:i.defaultAllDay,eventDataTransform:i.eventDataTransform,success:i.success,failure:i.failure,publicId:i.id||"",sourceId:Ze(),sourceDefId:o.sourceDefId,meta:o.meta,ui:Ir(i,t),extendedProps:s}}return null}function Br(e){return Object.assign(Object.assign(Object.assign({},kr),Pr),e.pluginHooks.eventSourceRefiners)}function jr(e,t,n,r,i){switch(t.type){case"RECEIVE_EVENTS":return function(e,t,n,r,i,s){if(t&&n===t.latestFetchId){let n=Dr(zr(i,t,s),t,s);return r&&(n=gr(n,r,s)),xr(Wr(e,t.sourceId),n)}return e}(e,n[t.sourceId],t.fetchId,t.fetchRange,t.rawEvents,i);case"RESET_RAW_EVENTS":return function(e,t,n,r,i){const{defIdMap:s,instanceIdMap:o}=function(e){const{defs:t,instances:n}=e,r={},i={};for(let e in t){const n=t[e],{publicId:i}=n;i&&(r[i]=e)}for(let e in n){const r=n[e],s=t[r.defId],{publicId:o}=s;o&&(i[o]=e)}return{defIdMap:r,instanceIdMap:i}}(e);return gr(Dr(zr(n,t,i),t,i,!1,s,o),r,i)}(e,n[t.sourceId],t.rawEvents,r.activeRange,i);case"ADD_EVENTS":return function(e,t,n,r){n&&(t=gr(t,n,r));return xr(e,t)}(e,t.eventStore,r?r.activeRange:null,i);case"RESET_EVENTS":return t.eventStore;case"MERGE_EVENTS":return xr(e,t.eventStore);case"PREV":case"NEXT":case"CHANGE_DATE":case"CHANGE_VIEW_TYPE":return r?gr(e,r.activeRange,i):e;case"REMOVE_EVENTS":return function(e,t){let{defs:n,instances:r}=e,i={},s={};for(let e in n)t.defs[e]||(i[e]=n[e]);for(let e in r)!t.instances[e]&&i[r[e].defId]&&(s[e]=r[e]);return{defs:i,instances:s}}(e,t.eventStore);case"REMOVE_EVENT_SOURCE":return Wr(e,t.sourceId);case"REMOVE_ALL_EVENT_SOURCES":return _r(e,e=>!e.sourceId);case"REMOVE_ALL_EVENTS":return{defs:{},instances:{}};default:return e}}function zr(e,t,n){let r=n.options.eventDataTransform,i=t?t.eventDataTransform:null;return i&&(e=Ur(e,i)),r&&(e=Ur(e,r)),e}function Ur(e,t){let n;if(t){n=[];for(let r of e){let e=t(r);e?n.push(e):null==e&&n.push(r)}}else n=e;return n}function Lr(e,t,n){let{defs:r}=e,i=An(e.instances,e=>r[e.defId].allDay?e:Object.assign(Object.assign({},e),{range:{start:n.createMarker(t.toDate(e.range.start,e.forcedStartTzo)),end:n.createMarker(t.toDate(e.range.end,e.forcedEndTzo))},forcedStartTzo:n.canComputeOffset?null:e.forcedStartTzo,forcedEndTzo:n.canComputeOffset?null:e.forcedEndTzo}));return{defs:r,instances:i}}function Wr(e,t){return _r(e,e=>e.sourceId!==t)}class Fr{constructor(){this.handlers={},this.thisContext=null}setThisContext(e){this.thisContext=e}setOptions(e){this.options=e}on(e,t){!function(e,t,n){(e[t]||(e[t]=[])).push(n)}(this.handlers,e,t)}off(e,t){!function(e,t,n){n?e[t]&&(e[t]=e[t].filter(e=>e!==n)):delete e[t]}(this.handlers,e,t)}trigger(e,...t){let n=this.handlers[e]||[],r=this.options&&this.options[e],i=[].concat(r||[],n);for(let e of i)e.apply(this.thisContext,t)}hasHandlers(e){return Boolean(this.handlers[e]&&this.handlers[e].length||this.options&&this.options[e])}}const Vr={startTime:"09:00",endTime:"17:00",daysOfWeek:[1,2,3,4,5],display:"inverse-background",classNames:"fc-non-business",groupId:"_businessHours"};function Gr(e,t){return Dr(function(e){let t;t=!0===e?[{}]:Array.isArray(e)?e.filter(e=>e.daysOfWeek):"object"==typeof e&&e?[e]:[];return t=t.map(e=>Object.assign(Object.assign({},Vr),e)),t}(e),null,t)}function Qr(e,t,n){n.emitter.trigger("select",Object.assign(Object.assign({},qr(e,n)),{jsEvent:t?t.origEvent:null,view:n.viewApi||n.calendarApi.view}))}function qr(e,t){let n={};for(let r of t.pluginHooks.dateSpanTransforms)Object.assign(n,r(e,t));var r,i;return Object.assign(n,(r=e,i=t.dateEnv,Object.assign(Object.assign({},Ai(r.range,i,r.allDay)),{allDay:r.allDay}))),n}function Yr(e,t,n){let{dateEnv:r,options:i}=n,s=t;return e?(s=Mt(s),s=r.add(s,i.defaultAllDayEventDuration)):s=r.add(s,i.defaultTimedEventDuration),s}function Zr(e,t,n,r){let i=oi(e.defs,t),s={defs:{},instances:{}};for(let t in e.defs){let o=e.defs[t];s.defs[t]=Xr(o,i[t],n,r)}for(let t in e.instances){let o=e.instances[t],a=s.defs[o.defId];s.instances[t]=$r(o,a,i[o.defId],n,r)}return s}function Xr(e,t,n,r){let i=n.standardProps||{};null==i.hasEnd&&t.durationEditable&&(n.startDelta||n.endDelta)&&(i.hasEnd=!0);let s=Object.assign(Object.assign(Object.assign({},e),i),{ui:Object.assign(Object.assign({},e.ui),i.ui)});n.extendedProps&&(s.extendedProps=Object.assign(Object.assign({},s.extendedProps),n.extendedProps));for(let e of r.pluginHooks.eventDefMutationAppliers)e(s,n,r);return!s.hasEnd&&r.options.forceEventDuration&&(s.hasEnd=!0),s}function $r(e,t,n,r,i){let{dateEnv:s}=i,o=r.standardProps&&!0===r.standardProps.allDay,a=r.standardProps&&!1===r.standardProps.hasEnd,l=Object.assign({},e);return o&&(l.range=lr(l.range)),r.datesDelta&&n.startEditable&&(l.range={start:s.add(l.range.start,r.datesDelta),end:s.add(l.range.end,r.datesDelta)}),r.startDelta&&n.durationEditable&&(l.range={start:s.add(l.range.start,r.startDelta),end:l.range.end}),r.endDelta&&n.durationEditable&&(l.range={start:l.range.start,end:s.add(l.range.end,r.endDelta)}),a&&(l.range={start:l.range.start,end:Yr(t.allDay,l.range.start,i)}),t.allDay&&(l.range={start:Mt(l.range.start),end:Mt(l.range.end)}),l.range.endai(e,t))}function ai(e,t){let n=[];return t[""]&&n.push(t[""]),t[e.defId]&&n.push(t[e.defId]),n.push(e.ui),Or(n)}function li(e,t){let n=e.map(ci);return n.sort((e,n)=>rt(e,n,t)),n.map(e=>e._seg)}function ci(e){let{eventRange:t}=e,n=t.def,r=t.instance?t.instance.range:t.range,i=r.start?r.start.valueOf():0,s=r.end?r.end.valueOf():0;return Object.assign(Object.assign(Object.assign({},n.extendedProps),n),{id:n.publicId,start:i,end:s,duration:s-i,allDay:Number(n.allDay),_seg:e})}function di(e,t){let{pluginHooks:n}=t,r=n.isDraggableTransformers,{def:i,ui:s}=e.eventRange,o=s.startEditable;for(let e of r)o=e(o,i,s,t);return o}function ui(e,t){return e.isStart&&e.eventRange.ui.durationEditable&&t.options.eventResizableFromStart}function hi(e,t){return e.isEnd&&e.eventRange.ui.durationEditable}function fi(e,t,n,r,i,s,o){let{dateEnv:a,options:l}=n,{displayEventTime:c,displayEventEnd:d}=l,u=e.eventRange.def,h=e.eventRange.instance;null==c&&(c=!1!==r),null==d&&(d=!1!==i);let f=h.range.start,g=h.range.end,p=s||e.start||e.eventRange.range.start,m=o||e.end||e.eventRange.range.end,v=Mt(f).valueOf()===Mt(p).valueOf(),y=Mt(Ct(g,-1)).valueOf()===Mt(Ct(m,-1)).valueOf();return c&&!u.allDay&&(v||y)?(p=v?f:p,m=y?g:m,d&&u.hasEnd?a.formatRange(p,m,t,{forcedStartTzo:s?null:h.forcedStartTzo,forcedEndTzo:o?null:h.forcedEndTzo}):a.format(p,t,{forcedTzo:s?null:h.forcedStartTzo})):""}function gi(e,t,n){let r=e.eventRange.range;return{isPast:r.end<=(n||t.start),isFuture:r.start>=(n||t.end),isToday:t&&ar(t,r.start)}}function pi(e){let t=["fc-event"];return e.isMirror&&t.push("fc-event-mirror"),e.isDraggable&&t.push("fc-event-draggable"),(e.isStartResizable||e.isEndResizable)&&t.push("fc-event-resizable"),e.isDragging&&t.push("fc-event-dragging"),e.isResizing&&t.push("fc-event-resizing"),e.isSelected&&t.push("fc-event-selected"),e.isStart&&t.push("fc-event-start"),e.isEnd&&t.push("fc-event-end"),e.isPast&&t.push("fc-event-past"),e.isToday&&t.push("fc-event-today"),e.isFuture&&t.push("fc-event-future"),t}function mi(e){return e.instance?e.instance.instanceId:`${e.def.defId}:${e.range.start.toISOString()}`}function vi(e,t){let{def:n,instance:r}=e.eventRange,{url:i}=n;if(i)return{href:i};let{emitter:s,options:o}=t,{eventInteractive:a}=o;return null==a&&(a=n.interactive,null==a&&(a=Boolean(s.hasHandlers("eventClick")))),a?qe(e=>{s.trigger("eventClick",{el:e.target,event:new Kr(t,n,r),jsEvent:e,view:t.viewApi})}):{}}const yi={start:yn,end:yn,allDay:Boolean};function bi(e,t,n){let r=function(e,t){let{refined:n,extra:r}=vn(e,yi),i=n.start?t.createMarkerMeta(n.start):null,s=n.end?t.createMarkerMeta(n.end):null,{allDay:o}=n;null==o&&(o=i&&i.isTimeUnspecified&&(!s||s.isTimeUnspecified));return Object.assign({range:{start:i?i.marker:null,end:s?s.marker:null},allDay:o},r)}(e,t),{range:i}=r;if(!i.start)return null;if(!i.end){if(null==n)return null;i.end=t.add(i.start,n)}return r}function Ei(e,t){return ir(e.range,t.range)&&e.allDay===t.allDay&&function(e,t){for(let n in t)if("range"!==n&&"allDay"!==n&&e[n]!==t[n])return!1;for(let n in e)if(!(n in t))return!1;return!0}(e,t)}function Si(e,t,n){return Object.assign(Object.assign({},Ai(e,t,n)),{timeZone:t.timeZone})}function Ai(e,t,n){return{start:t.toDate(e.start),end:t.toDate(e.end),startStr:t.formatIso(e.start,{omitTime:n}),endStr:t.formatIso(e.end,{omitTime:n})}}function Di(e,t,n){let r=!1,i=function(e){r||(r=!0,t(e))},s=function(e){r||(r=!0,n(e))},o=e(i,s);o&&"function"==typeof o.then&&o.then(i,s)}class wi extends Error{constructor(e,t){super(e),this.response=t}}function Ci(e,t,n){const r={method:e=e.toUpperCase()};return"GET"===e?t+=(-1===t.indexOf("?")?"?":"&")+new URLSearchParams(n):(r.body=new URLSearchParams(n),r.headers={"Content-Type":"application/x-www-form-urlencoded"}),fetch(t,r).then(e=>{if(e.ok)return e.json().then(t=>[t,e],()=>{throw new wi("Failure parsing JSON",e)});throw new wi("Request failed",e)})}let Ri;function xi(){return null==Ri&&(Ri=function(){if("undefined"==typeof document)return!0;let e=document.createElement("div");e.style.position="absolute",e.style.top="0px",e.style.left="0px",e.innerHTML="
",e.querySelector("table").style.height="100px",e.querySelector("div").style.height="100%",document.body.appendChild(e);let t=e.querySelector("div").offsetHeight>0;return document.body.removeChild(e),t}()),Ri}class _i extends Vn{constructor(){super(...arguments),this.state={forPrint:!1},this.handleBeforePrint=()=>{Bn(()=>{this.setState({forPrint:!0})})},this.handleAfterPrint=()=>{Bn(()=>{this.setState({forPrint:!1})})}}render(){let{props:e}=this,{options:t}=e,{forPrint:n}=this.state,r=n||"auto"===t.height||"auto"===t.contentHeight,i=r||null==t.height?"":t.height,s=["fc",n?"fc-media-print":"fc-media-screen","fc-direction-"+t.direction,e.theme.getClass("root")];return xi()||s.push("fc-liquid-hack"),e.children(s,i,r,n)}componentDidMount(){let{emitter:e}=this.props;e.on("_beforeprint",this.handleBeforePrint),e.on("_afterprint",this.handleAfterPrint)}componentWillUnmount(){let{emitter:e}=this.props;e.off("_beforeprint",this.handleBeforePrint),e.off("_afterprint",this.handleAfterPrint)}}class Ti{constructor(e){this.component=e.component,this.isHitComboAllowed=e.isHitComboAllowed||null}destroy(){}}function ki(e){return{[e.component.uid]:e}}const Mi={};class Ii extends D{constructor(e,t){super(e,t),this.handleRefresh=()=>{let e=this.computeTiming();e.state.nowDate.valueOf()!==this.state.nowDate.valueOf()&&this.setState(e.state),this.clearTimeout(),this.setTimeout(e.waitMs)},this.handleVisibilityChange=()=>{document.hidden||this.handleRefresh()},this.state=this.computeTiming().state}render(){let{props:e,state:t}=this;return e.children(t.nowDate,t.todayRange)}componentDidMount(){this.setTimeout(),this.context.nowManager.addResetListener(this.handleRefresh),document.addEventListener("visibilitychange",this.handleVisibilityChange)}componentDidUpdate(e){e.unit!==this.props.unit&&(this.clearTimeout(),this.setTimeout())}componentWillUnmount(){this.clearTimeout(),this.context.nowManager.removeResetListener(this.handleRefresh),document.removeEventListener("visibilitychange",this.handleVisibilityChange)}computeTiming(){let e,t,{props:n,context:r}=this,i=r.nowManager.getDateMarker(),{nowIndicatorSnap:s}=r.options;if("auto"===s&&(s=/year|month|week|day/.test(n.unit)||1===(n.unitValue||1)),s){e=r.dateEnv.startOf(i,n.unit),t=r.dateEnv.add(e,ft(1,n.unit)).valueOf()-i.valueOf()}else e=i,t=6e4;return t=Math.min(864e5,t),{state:{nowDate:e,todayRange:Oi(e)},waitMs:t}}setTimeout(e=this.computeTiming().waitMs){this.timeoutId=setTimeout(()=>{const e=this.computeTiming();this.setState(e.state,()=>{this.setTimeout(e.waitMs)})},e)}clearTimeout(){this.timeoutId&&clearTimeout(this.timeoutId)}}function Oi(e){let t=Mt(e);return{start:t,end:wt(t,1)}}Ii.contextType=Ln;class Ni{getCurrentData(){return this.currentDataManager.getCurrentData()}dispatch(e){this.currentDataManager.dispatch(e)}get view(){return this.getCurrentData().viewApi}batchRendering(e){e()}updateSize(){this.trigger("_resize",!0)}setOption(e,t){this.dispatch({type:"SET_OPTION",optionName:e,rawOptionValue:t})}getOption(e){return this.currentDataManager.currentCalendarOptionsInput[e]}getAvailableLocaleCodes(){return Object.keys(this.getCurrentData().availableRawLocales)}on(e,t){let{currentDataManager:n}=this;n.currentCalendarOptionsRefiners[e]?n.emitter.on(e,t):console.warn(`Unknown listener name '${e}'`)}off(e,t){this.currentDataManager.emitter.off(e,t)}trigger(e,...t){this.currentDataManager.emitter.trigger(e,...t)}changeView(e,t){this.batchRendering(()=>{if(this.unselect(),t)if(t.start&&t.end)this.dispatch({type:"CHANGE_VIEW_TYPE",viewType:e}),this.dispatch({type:"SET_OPTION",optionName:"visibleRange",rawOptionValue:t});else{let{dateEnv:n}=this.getCurrentData();this.dispatch({type:"CHANGE_VIEW_TYPE",viewType:e,dateMarker:n.createMarker(t)})}else this.dispatch({type:"CHANGE_VIEW_TYPE",viewType:e})})}zoomTo(e,t){let n;t=t||"day",n=this.getCurrentData().viewSpecs[t]||this.getUnitViewSpec(t),this.unselect(),n?this.dispatch({type:"CHANGE_VIEW_TYPE",viewType:n.type,dateMarker:e}):this.dispatch({type:"CHANGE_DATE",dateMarker:e})}getUnitViewSpec(e){let t,n,{viewSpecs:r,toolbarConfig:i}=this.getCurrentData(),s=[].concat(i.header?i.header.viewsWithButtons:[],i.footer?i.footer.viewsWithButtons:[]);for(let e in r)s.push(e);for(t=0;t{this.dispatch({type:"REMOVE_EVENTS",eventStore:ei(e)})}})}getEventById(e){let t=this.getCurrentData(),{defs:n,instances:r}=t.eventStore;e=String(e);for(let i in n){let s=n[i];if(s.publicId===e){if(s.recurringDef)return new Kr(t,s,null);for(let e in r){let n=r[e];if(n.defId===s.defId)return new Kr(t,s,n)}}}return null}getEvents(){let e=this.getCurrentData();return ti(e.eventStore,e)}removeAllEvents(){this.dispatch({type:"REMOVE_ALL_EVENTS"})}getEventSources(){let e=this.getCurrentData(),t=e.eventSources,n=[];for(let r in t)n.push(new Jr(e,t[r]));return n}getEventSourceById(e){let t=this.getCurrentData(),n=t.eventSources;e=String(e);for(let r in n)if(n[r].publicId===e)return new Jr(t,n[r]);return null}addEventSource(e){let t=this.getCurrentData();if(e instanceof Jr)return t.eventSources[e.internalEventSource.sourceId]||this.dispatch({type:"ADD_EVENT_SOURCES",sources:[e.internalEventSource]}),e;let n=Hr(e,t);return n?(this.dispatch({type:"ADD_EVENT_SOURCES",sources:[n]}),new Jr(t,n)):null}removeAllEventSources(){this.dispatch({type:"REMOVE_ALL_EVENT_SOURCES"})}refetchEvents(){this.dispatch({type:"FETCH_EVENT_SOURCES",isRefetch:!0})}scrollToTime(e){let t=ft(e);t&&this.trigger("_scrollRequest",{time:t})}}function Pi(e,t){return e.left>=t.left&&e.left=t.top&&e.topthis.eventUiBuilders[t]||Gt(Wi));for(let n in t){let c=t[n],d=s[n]||Ui,u=this.eventUiBuilders[n];l[n]={businessHours:c.businessHours||e.businessHours,dateSelection:r[n]||null,eventStore:d,eventUiBases:u(e.eventUiBases[""],c.ui,i[n]),eventSelection:d.instances[e.eventSelection]?e.eventSelection:"",eventDrag:o[n]||null,eventResize:a[n]||null}}return l}_splitDateSpan(e){let t={};if(e){let n=this.getKeysForDateSpan(e);for(let r of n)t[r]=e}return t}_getKeysForEventDefs(e){return An(e.defs,e=>this.getKeysForEventDef(e))}_splitEventStore(e,t){let{defs:n,instances:r}=e,i={};for(let e in n)for(let r of t[e])i[r]||(i[r]={defs:{},instances:{}}),i[r].defs[e]=n[e];for(let e in r){let n=r[e];for(let r of t[n.defId])i[r]&&(i[r].instances[e]=n)}return i}_splitIndividualUi(e,t){let n={};for(let r in e)if(r)for(let i of t[r])n[i]||(n[i]={}),n[i][r]=e[r];return n}_splitInteraction(e){let t={};if(e){let n=this._splitEventStore(e.affectedEvents,this._getKeysForEventDefs(e.affectedEvents)),r=this._getKeysForEventDefs(e.mutatedEvents),i=this._splitEventStore(e.mutatedEvents,r),s=r=>{t[r]||(t[r]={affectedEvents:n[r]||Ui,mutatedEvents:i[r]||Ui,isEvent:e.isEvent})};for(let e in n)s(e);for(let e in i)s(e)}return t}}function Wi(e,t,n){let r=[];e&&r.push(e),t&&r.push(t);let i={"":Or(r)};return n&&Object.assign(i,n),i}function Fi(e,t,n,r){return{dow:e.getUTCDay(),isDisabled:Boolean(r&&(!r.activeRange||!ar(r.activeRange,e))),isOther:Boolean(r&&!ar(r.currentRange,e)),isToday:Boolean(t&&ar(t,e)),isPast:Boolean(n?en:!!t&&e>=t.end)}}function Vi(e,t){let n=["fc-day","fc-day-"+At[e.dow]];return e.isDisabled?n.push("fc-day-disabled"):(e.isToday&&(n.push("fc-day-today"),n.push(t.getClass("today"))),e.isPast&&n.push("fc-day-past"),e.isFuture&&n.push("fc-day-future"),e.isOther&&n.push("fc-day-other")),n}const Gi=an({year:"numeric",month:"long",day:"numeric"}),Qi=an({week:"long"});function qi(e,t,n="day",r=!0){const{dateEnv:i,options:s,calendarApi:o}=e;let a=i.format(t,"week"===n?Qi:Gi);if(s.navLinks){let e=i.toDate(t);const l=e=>{let r="day"===n?s.navLinkDayClick:"week"===n?s.navLinkWeekClick:null;"function"==typeof r?r.call(o,i.toDate(t),e):("string"==typeof r&&(n=r),o.zoomTo(t,n))};return Object.assign({title:at(s.navLinkHint,[a,e],a),"data-navlink":""},r?Qe(l):{onClick:l})}return{"aria-label":a}}let Yi,Zi=null;function Xi(){return null===Zi&&(Zi=function(){let e=document.createElement("div");Be(e,{position:"absolute",top:-1e3,left:0,border:0,padding:0,overflow:"scroll",direction:"rtl"}),e.innerHTML="
",document.body.appendChild(e);let t=e.firstChild.getBoundingClientRect().left>e.getBoundingClientRect().left;return Ie(e),t}()),Zi}function $i(){return Yi||(Yi=function(){let e=document.createElement("div");e.style.overflow="scroll",e.style.position="absolute",e.style.top="-9999px",e.style.left="-9999px",document.body.appendChild(e);let t=Ji(e);return document.body.removeChild(e),t}()),Yi}function Ji(e){return{x:e.offsetHeight-e.clientHeight,y:e.offsetWidth-e.clientWidth}}function Ki(e,t=!1){let n=window.getComputedStyle(e),r=parseInt(n.borderLeftWidth,10)||0,i=parseInt(n.borderRightWidth,10)||0,s=parseInt(n.borderTopWidth,10)||0,o=parseInt(n.borderBottomWidth,10)||0,a=Ji(e),l=a.y-r-i,c={borderLeft:r,borderRight:i,borderTop:s,borderBottom:o,scrollbarBottom:a.x-s-o,scrollbarLeft:0,scrollbarRight:0};return Xi()&&"rtl"===n.direction?c.scrollbarLeft=l:c.scrollbarRight=l,t&&(c.paddingLeft=parseInt(n.paddingLeft,10)||0,c.paddingRight=parseInt(n.paddingRight,10)||0,c.paddingTop=parseInt(n.paddingTop,10)||0,c.paddingBottom=parseInt(n.paddingBottom,10)||0),c}function es(e,t=!1,n){let r=n?e.getBoundingClientRect():ts(e),i=Ki(e,t),s={left:r.left+i.borderLeft+i.scrollbarLeft,right:r.right-i.borderRight-i.scrollbarRight,top:r.top+i.borderTop,bottom:r.bottom-i.borderBottom-i.scrollbarBottom};return t&&(s.left+=i.paddingLeft,s.right-=i.paddingRight,s.top+=i.paddingTop,s.bottom-=i.paddingBottom),s}function ts(e){let t=e.getBoundingClientRect();return{left:t.left+window.scrollX,top:t.top+window.scrollY,right:t.right+window.scrollX,bottom:t.bottom+window.scrollY}}function ns(e){let t=[];for(;e instanceof HTMLElement;){let n=window.getComputedStyle(e);if("fixed"===n.position)break;/(auto|scroll)/.test(n.overflow+n.overflowY+n.overflowX)&&t.push(e),e=e.parentNode}return t}class rs{constructor(e,t,n,r){this.els=t;let i=this.originClientRect=e.getBoundingClientRect();n&&this.buildElHorizontals(i.left),r&&this.buildElVerticals(i.top)}buildElHorizontals(e){let t=[],n=[];for(let r of this.els){let i=r.getBoundingClientRect();t.push(i.left-e),n.push(i.right-e)}this.lefts=t,this.rights=n}buildElVerticals(e){let t=[],n=[];for(let r of this.els){let i=r.getBoundingClientRect();t.push(i.top-e),n.push(i.bottom-e)}this.tops=t,this.bottoms=n}leftToIndex(e){let t,{lefts:n,rights:r}=this,i=n.length;for(t=0;t=n[t]&&e=n[t]&&e0}canScrollHorizontally(){return this.getMaxScrollLeft()>0}canScrollUp(){return this.getScrollTop()>0}canScrollDown(){return this.getScrollTop()0}canScrollRight(){return this.getScrollLeft()e.thickness||1)){this.getEntryThickness=e,this.strictOrder=!1,this.allowReslicing=!1,this.maxCoord=-1,this.maxStackCnt=-1,this.levelCoords=[],this.entriesByLevel=[],this.stackCnts={}}addSegs(e){let t=[];for(let n of e)this.insertEntry(n,t);return t}insertEntry(e,t){let n=this.findInsertion(e);this.isInsertionValid(n,e)?this.insertEntryAt(e,n):this.handleInvalidInsertion(n,e,t)}isInsertionValid(e,t){return(-1===this.maxCoord||e.levelCoord+this.getEntryThickness(t)<=this.maxCoord)&&(-1===this.maxStackCnt||e.stackCnti.end&&this.insertEntry({index:e.index,thickness:e.thickness,span:{start:i.end,end:r.end}},n)}insertEntryAt(e,t){let{entriesByLevel:n,levelCoords:r}=this;-1===t.lateral?(gs(r,t.level,t.levelCoord),gs(n,t.level,[e])):gs(n[t.level],t.lateral,e),this.stackCnts[us(e)]=t.stackCnt}findInsertion(e){let{levelCoords:t,entriesByLevel:n,strictOrder:r,stackCnts:i}=this,s=t.length,o=0,a=-1,l=-1,c=null,d=0;for(let u=0;u=o+this.getEntryThickness(e))break;let h,f=n[u],g=ps(f,e.span.start,ds),p=g[0]+g[1];for(;(h=f[p])&&h.span.starto&&(o=e,c=h,a=u,l=p),e===o&&(d=Math.max(d,i[us(h)]+1)),p+=1}}let u=0;if(c)for(u=a+1;un(e[i-1]))return[i,0];for(;ro))return[s,1];r=s+1}}return[r,0]}class ms{constructor(e,t){this.emitter=new Fr}destroy(){}setMirrorIsVisible(e){}setMirrorNeedsRevert(e){}setAutoScrollEnabled(e){}}const vs={},ys={startTime:ft,duration:ft,create:Boolean,sourceId:String};function bs(e){let{refined:t,extra:n}=vn(e,ys);return{startTime:t.startTime||null,duration:t.duration||null,create:null==t.create||t.create,sourceId:t.sourceId,leftoverProps:n}}function Es(e,t){return an(!e||t>10?{weekday:"short"}:t>1?{weekday:"short",month:"numeric",day:"numeric",omitCommas:!0}:{weekday:"long"})}const Ss="fc-col-header-cell";function As(e){return e.text}class Ds extends Vn{render(){let{dateEnv:e,options:t,theme:n,viewApi:r}=this.context,{props:i}=this,{date:s,dateProfile:o}=i,a=Fi(s,i.todayRange,null,o),l=[Ss].concat(Vi(a,n)),c=e.format(s,i.dayHeaderFormat),d=!a.isDisabled&&i.colCnt>1?qi(this.context,s):{},u=e.toDate(s);e.namedTimeZoneImpl&&(u=Ct(u,36e5));let h=Object.assign(Object.assign(Object.assign({date:u,view:r},i.extraRenderProps),{text:c}),a);return p($n,{elTag:"th",elClasses:l,elAttrs:Object.assign({role:"columnheader",colSpan:i.colSpan,"data-date":a.isDisabled?void 0:Lt(s)},i.extraDataAttrs),renderProps:h,generatorName:"dayHeaderContent",customGenerator:t.dayHeaderContent,defaultGenerator:As,classNameGenerator:t.dayHeaderClassNames,didMount:t.dayHeaderDidMount,willUnmount:t.dayHeaderWillUnmount},e=>p("div",{className:"fc-scrollgrid-sync-inner"},!a.isDisabled&&p(e,{elTag:"a",elAttrs:d,elClasses:["fc-col-header-cell-cushion",i.isSticky&&"fc-sticky"]})))}}const ws=an({weekday:"long"});class Cs extends Vn{render(){let{props:e}=this,{dateEnv:t,theme:n,viewApi:r,options:i}=this.context,s=wt(new Date(2592e5),e.dow),o={dow:e.dow,isDisabled:!1,isFuture:!1,isPast:!1,isToday:!1,isOther:!1},a=t.format(s,e.dayHeaderFormat),l=Object.assign(Object.assign(Object.assign(Object.assign({date:s},o),{view:r}),e.extraRenderProps),{text:a});return p($n,{elTag:"th",elClasses:[Ss,...Vi(o,n),...e.extraClassNames||[]],elAttrs:Object.assign({role:"columnheader",colSpan:e.colSpan},e.extraDataAttrs),renderProps:l,generatorName:"dayHeaderContent",customGenerator:i.dayHeaderContent,defaultGenerator:As,classNameGenerator:i.dayHeaderClassNames,didMount:i.dayHeaderDidMount,willUnmount:i.dayHeaderWillUnmount},n=>p("div",{className:"fc-scrollgrid-sync-inner"},p(n,{elTag:"a",elClasses:["fc-col-header-cell-cushion",e.isSticky&&"fc-sticky"],elAttrs:{"aria-label":t.format(s,ws)}})))}}class Rs extends Vn{constructor(){super(...arguments),this.createDayHeaderFormatter=Gt(xs)}render(){let{context:e}=this,{dates:t,dateProfile:n,datesRepDistinctDays:r,renderIntro:i}=this.props,s=this.createDayHeaderFormatter(e.options.dayHeaderFormat,r,t.length);return p(Ii,{unit:"day"},(e,o)=>p("tr",{role:"row"},i&&i("day"),t.map(e=>r?p(Ds,{key:e.toISOString(),date:e,dateProfile:n,todayRange:o,colCnt:t.length,dayHeaderFormat:s}):p(Cs,{key:e.getUTCDay(),dow:e.getUTCDay(),dayHeaderFormat:s}))))}}function xs(e,t,n){return e||Es(t,n)}class _s{constructor(e,t){let n=e.start,{end:r}=e,i=[],s=[],o=-1;for(;n=t.length?t[t.length-1]+1:t[n]}}class Ts{constructor(e,t){let n,r,i,{dates:s}=e;if(t){for(r=s[0].getUTCDay(),n=1;n!p[e.instanceId])}),u=d.defs,h=d.instances,f=oi(u,e.eventUiBases);var g,p;for(let r in l){let o=l[r],g=o.range,p=c[o.defId],m=a[o.defId];if(!Hs(p.constraints,g,d,e.businessHours,t))return!1;let{eventOverlap:v}=t.options,y="function"==typeof v?v:null;for(let e in h){let n=h[e];if(sr(g,n.range)){if(!1===f[n.defId].overlap&&s.isEvent)return!1;if(!1===p.overlap)return!1;if(y&&!y(new Kr(t,u[n.defId],n),new Kr(t,m,o)))return!1}}let b=i.eventStore;for(let e of p.allows){let i,s=Object.assign(Object.assign({},n),{range:o.range,allDay:m.allDay}),a=b.defs[m.defId],l=b.instances[r];if(i=a?new Kr(t,a,l):new Kr(t,m),!e(qr(s,t),i))return!1}}return!0}(e,t,n,r))&&!(e.dateSelection&&!function(e,t,n,r){let i=e.eventStore,s=i.defs,o=i.instances,a=e.dateSelection,l=a.range,{selectionConfig:c}=t.getCurrentData();r&&(c=r(c));if(!Hs(c.constraints,l,i,e.businessHours,t))return!1;let{selectOverlap:d}=t.options,u="function"==typeof d?d:null;for(let e in o){let n=o[e];if(sr(l,n.range)){if(!1===c.overlap)return!1;if(u&&!u(new Kr(t,s[n.defId],n),null))return!1}}for(let e of c.allows){let r=Object.assign(Object.assign({},n),a);if(!e(qr(r,t),null))return!1}return!0}(e,t,n,r))}function Hs(e,t,n,r,i){for(let s of e)if(!zs(Bs(s,t,n,r,i),t))return!1;return!0}function Bs(e,t,n,r,i){return"businessHours"===e?js(gr(r,t,i)):"string"==typeof e?js(_r(n,t=>t.groupId===e)):"object"==typeof e&&e?js(gr(e,t,i)):[]}function js(e){let{instances:t}=e,n=[];for(let e in t)n.push(t[e].range);return n}function zs(e,t){for(let n of e)if(or(n,t))return!0;return!1}const Us=/^(visible|hidden)$/;class Ls extends Vn{constructor(){super(...arguments),this.handleEl=e=>{this.el=e,Gn(this.props.elRef,e)}}render(){let{props:e}=this,{liquid:t,liquidIsAbsolute:n}=e,r=t&&n,i=["fc-scroller"];return t&&(n?i.push("fc-scroller-liquid-absolute"):i.push("fc-scroller-liquid")),p("div",{ref:this.handleEl,className:i.join(" "),style:{overflowX:e.overflowX,overflowY:e.overflowY,left:r&&-(e.overcomeLeft||0)||"",right:r&&-(e.overcomeRight||0)||"",bottom:r&&-(e.overcomeBottom||0)||"",marginLeft:!r&&-(e.overcomeLeft||0)||"",marginRight:!r&&-(e.overcomeRight||0)||"",marginBottom:!r&&-(e.overcomeBottom||0)||"",maxHeight:e.maxHeight||""}},e.children)}needsXScrolling(){if(Us.test(this.props.overflowX))return!1;let{el:e}=this,t=this.el.getBoundingClientRect().width-this.getYScrollbarWidth(),{children:n}=e;for(let e=0;et)return!0}return!1}needsYScrolling(){if(Us.test(this.props.overflowY))return!1;let{el:e}=this,t=this.el.getBoundingClientRect().height-this.getXScrollbarWidth(),{children:n}=e;for(let e=0;et)return!0}return!1}getXScrollbarWidth(){return Us.test(this.props.overflowX)?0:this.el.offsetHeight-this.el.clientHeight}getYScrollbarWidth(){return Us.test(this.props.overflowY)?0:this.el.offsetWidth-this.el.clientWidth}}class Ws{constructor(e){this.masterCallback=e,this.currentMap={},this.depths={},this.callbackMap={},this.handleValue=(e,t)=>{let{depths:n,currentMap:r}=this,i=!1,s=!1;null!==e?(i=t in r,r[t]=e,n[t]=(n[t]||0)+1,s=!0):(n[t]-=1,n[t]||(delete r[t],delete this.callbackMap[t],i=!0)),this.masterCallback&&(i&&this.masterCallback(null,String(t)),s&&this.masterCallback(e,String(t)))}}createRef(e){let t=this.callbackMap[e];return t||(t=this.callbackMap[e]=t=>{this.handleValue(t,String(e))}),t}collect(e,t,n){return Tn(this.currentMap,e,t,n)}getAll(){return wn(this.currentMap)}}function Fs(e){let t=Pe(e,".fc-scrollgrid-shrink"),n=0;for(let e of t)n=Math.max(n,dt(e));return Math.ceil(n)}function Vs(e,t){return e.liquid&&t.liquid}function Gs(e,t){return null!=t.maxHeight||Vs(e,t)}function Qs(e,t,n,r){let{expandRows:i}=n;return"function"==typeof t.content?t.content(n):p("table",{role:"presentation",className:[t.tableClassName,e.syncRowHeights?"fc-scrollgrid-sync-table":""].join(" "),style:{minWidth:n.tableMinWidth,width:n.clientWidth,height:i?n.clientHeight:""}},n.tableColGroupNode,p(r?"thead":"tbody",{role:"presentation"},"function"==typeof t.rowContent?t.rowContent(n):t.rowContent))}function qs(e,t){return St(e,t,Cn)}function Ys(e,t){let n=[];for(let r of e){let e=r.span||1;for(let i=0;ie,qs),this.renderMicroColGroup=Gt(Ys),this.scrollerRefs=new Ws,this.scrollerElRefs=new Ws(this._handleScrollerEl.bind(this)),this.state={shrinkWidth:null,forceYScrollbars:!1,scrollerClientWidths:{},scrollerClientHeights:{}},this.handleSizing=()=>{this.safeSetState(Object.assign({shrinkWidth:this.computeShrinkWidth()},this.computeScrollerDims()))}}render(){let{props:e,state:t,context:n}=this,r=e.sections||[],i=this.processCols(e.cols),s=this.renderMicroColGroup(i,t.shrinkWidth),o=$s(e.liquid,n);e.collapsibleWidth&&o.push("fc-scrollgrid-collapsible");let a,l=r.length,c=0,d=[],u=[],h=[];for(;c{}},r);return p(r?"th":"td",{ref:n.elRef,role:"presentation"},p("div",{className:"fc-scroller-harness"+(c?" fc-scroller-harness-liquid":"")},p(Ls,{ref:this.scrollerRefs.createRef(u),elRef:this.scrollerElRefs.createRef(u),overflowY:d,overflowX:i.liquid?"hidden":"visible",maxHeight:e.maxHeight,liquid:c,liquidIsAbsolute:!0},h)))}_handleScrollerEl(e,t){let n=function(e,t){for(let n of e)if(n.key===t)return n;return null}(this.props.sections,t);n&&Gn(n.chunk.scrollerElRef,e)}componentDidMount(){this.handleSizing(),this.context.addResizeHandler(this.handleSizing)}componentDidUpdate(){this.handleSizing()}componentWillUnmount(){this.context.removeResizeHandler(this.handleSizing)}computeShrinkWidth(){return Xs(this.props.cols)?Fs(this.scrollerElRefs.getAll()):0}computeScrollerDims(){let e=$i(),{scrollerRefs:t,scrollerElRefs:n}=this,r=!1,i={},s={};for(let e in t.currentMap){let n=t.currentMap[e];if(n&&n.needsYScrolling()){r=!0;break}}for(let t of this.props.sections){let o=t.key,a=n.currentMap[o];if(a){let t=a.parentNode;i[o]=Math.floor(t.getBoundingClientRect().width-(r?e.y:0)),s[o]=Math.floor(t.getBoundingClientRect().height)}}return{forceYScrollbars:r,scrollerClientWidths:i,scrollerClientHeights:s}}}no.addStateEquality({scrollerClientWidths:Cn,scrollerClientHeights:Cn});class ro extends Vn{constructor(){super(...arguments),this.buildPublicEvent=Gt((e,t,n)=>new Kr(e,t,n)),this.handleEl=e=>{this.el=e,Gn(this.props.elRef,e),e&&ii(e,this.props.seg)}}render(){const{props:e,context:t}=this,{options:n}=t,{seg:r}=e,{eventRange:i}=r,{ui:s}=i,o={event:this.buildPublicEvent(t,i.def,i.instance),view:t.viewApi,timeText:e.timeText,textColor:s.textColor,backgroundColor:s.backgroundColor,borderColor:s.borderColor,isDraggable:!e.disableDragging&&di(r,t),isStartResizable:!e.disableResizing&&ui(r,t),isEndResizable:!e.disableResizing&&hi(r),isMirror:Boolean(e.isDragging||e.isResizing||e.isDateSelecting),isStart:Boolean(r.isStart),isEnd:Boolean(r.isEnd),isPast:Boolean(e.isPast),isFuture:Boolean(e.isFuture),isToday:Boolean(e.isToday),isSelected:Boolean(e.isSelected),isDragging:Boolean(e.isDragging),isResizing:Boolean(e.isResizing)};return p($n,{elRef:this.handleEl,elTag:e.elTag,elAttrs:e.elAttrs,elClasses:[...pi(o),...r.eventRange.ui.classNames,...e.elClasses||[]],elStyle:e.elStyle,renderProps:o,generatorName:"eventContent",customGenerator:n.eventContent,defaultGenerator:e.defaultGenerator,classNameGenerator:n.eventClassNames,didMount:n.eventDidMount,willUnmount:n.eventWillUnmount},e.children)}componentDidUpdate(e){this.el&&this.props.seg!==e.seg&&ii(this.el,this.props.seg)}}class io extends Vn{render(){let{props:e,context:t}=this,{options:n}=t,{seg:r}=e,{ui:i}=r.eventRange,s=fi(r,n.eventTimeFormat||e.defaultTimeFormat,t,e.defaultDisplayEventTime,e.defaultDisplayEventEnd);return p(ro,Object.assign({},e,{elTag:"a",elStyle:{borderColor:i.borderColor,backgroundColor:i.backgroundColor},elAttrs:vi(r,t),defaultGenerator:so,timeText:s}),(e,t)=>p(y,null,p(e,{elTag:"div",elClasses:["fc-event-main"],elStyle:{color:t.textColor}}),Boolean(t.isStartResizable)&&p("div",{className:"fc-event-resizer fc-event-resizer-start"}),Boolean(t.isEndResizable)&&p("div",{className:"fc-event-resizer fc-event-resizer-end"})))}}function so(e){return p("div",{className:"fc-event-main-frame"},e.timeText&&p("div",{className:"fc-event-time"},e.timeText),p("div",{className:"fc-event-title-container"},p("div",{className:"fc-event-title fc-sticky"},e.event.title||p(y,null," "))))}io.addPropsEquality({seg:Cn});const oo=e=>p(Ln.Consumer,null,t=>{let{options:n}=t,r={isAxis:e.isAxis,date:t.dateEnv.toDate(e.date),view:t.viewApi};return p($n,{elRef:e.elRef,elTag:e.elTag||"div",elAttrs:e.elAttrs,elClasses:e.elClasses,elStyle:e.elStyle,renderProps:r,generatorName:"nowIndicatorContent",customGenerator:n.nowIndicatorContent,classNameGenerator:n.nowIndicatorClassNames,didMount:n.nowIndicatorDidMount,willUnmount:n.nowIndicatorWillUnmount},e.children)}),ao=an({day:"numeric"});class lo extends Vn{constructor(){super(...arguments),this.refineRenderProps=Qt(uo)}render(){let{props:e,context:t}=this,{options:n}=t,r=this.refineRenderProps({date:e.date,dateProfile:e.dateProfile,todayRange:e.todayRange,isMonthStart:e.isMonthStart||!1,showDayNumber:e.showDayNumber,extraRenderProps:e.extraRenderProps,viewApi:t.viewApi,dateEnv:t.dateEnv,monthStartFormat:n.monthStartFormat});return p($n,{elRef:e.elRef,elTag:e.elTag,elAttrs:Object.assign(Object.assign({},e.elAttrs),r.isDisabled?{}:{"data-date":Lt(e.date)}),elClasses:[...Vi(r,t.theme),...e.elClasses||[]],elStyle:e.elStyle,renderProps:r,generatorName:"dayCellContent",customGenerator:n.dayCellContent,defaultGenerator:e.defaultGenerator,classNameGenerator:r.isDisabled?void 0:n.dayCellClassNames,didMount:n.dayCellDidMount,willUnmount:n.dayCellWillUnmount},e.children)}}function co(e){return Boolean(e.dayCellContent||qn("dayCellContent",e))}function uo(e){let{date:t,dateEnv:n,dateProfile:r,isMonthStart:i}=e,s=Fi(t,e.todayRange,null,r),o=e.showDayNumber?n.format(t,i?e.monthStartFormat:ao):"";return Object.assign(Object.assign(Object.assign({date:n.toDate(t),view:e.viewApi},s),{isMonthStart:i,dayNumberText:o}),e.extraRenderProps)}class ho extends Vn{render(){let{props:e}=this,{seg:t}=e;return p(ro,{elTag:"div",elClasses:["fc-bg-event"],elStyle:{backgroundColor:t.eventRange.ui.backgroundColor},defaultGenerator:fo,seg:t,timeText:"",isDragging:!1,isResizing:!1,isDateSelecting:!1,isSelected:!1,isPast:e.isPast,isFuture:e.isFuture,isToday:e.isToday,disableDragging:!0,disableResizing:!0})}}function fo(e){let{title:t}=e.event;return t&&p("div",{className:"fc-event-title"},e.event.title)}function go(e){return p("div",{className:"fc-"+e})}const po=e=>p(Ln.Consumer,null,t=>{let{dateEnv:n,options:r}=t,{date:i}=e,s=r.weekNumberFormat||e.defaultFormat,o={num:n.computeWeekNumber(i),text:n.format(i,s),date:i};return p($n,{elRef:e.elRef,elTag:e.elTag,elAttrs:e.elAttrs,elClasses:e.elClasses,elStyle:e.elStyle,renderProps:o,generatorName:"weekNumberContent",customGenerator:r.weekNumberContent,defaultGenerator:mo,classNameGenerator:r.weekNumberClassNames,didMount:r.weekNumberDidMount,willUnmount:r.weekNumberWillUnmount},e.children)});function mo(e){return e.text}class vo extends Vn{constructor(){super(...arguments),this.state={titleId:Le()},this.handleRootEl=e=>{this.rootEl=e,this.props.elRef&&Gn(this.props.elRef,e)},this.handleDocumentMouseDown=e=>{const t=ze(e);this.rootEl.contains(t)||this.handleCloseClick()},this.handleDocumentKeyDown=e=>{"Escape"===e.key&&this.handleCloseClick()},this.handleCloseClick=()=>{let{onClose:e}=this.props;e&&e()}}render(){let{theme:e,options:t}=this.context,{props:n,state:r}=this,i=["fc-popover",e.getClass("popover")].concat(n.extraClassNames||[]);return fe(p("div",Object.assign({},n.extraAttrs,{id:n.id,className:i.join(" "),"aria-labelledby":r.titleId,ref:this.handleRootEl}),p("div",{className:"fc-popover-header "+e.getClass("popoverHeader")},p("span",{className:"fc-popover-title",id:r.titleId},n.title),p("span",{className:"fc-popover-close "+e.getIconClass("close"),title:t.closeHint,onClick:this.handleCloseClick})),p("div",{className:"fc-popover-body "+e.getClass("popoverContent")},n.children)),n.parentEl)}componentDidMount(){document.addEventListener("mousedown",this.handleDocumentMouseDown),document.addEventListener("keydown",this.handleDocumentKeyDown),this.updateSize()}componentWillUnmount(){document.removeEventListener("mousedown",this.handleDocumentMouseDown),document.removeEventListener("keydown",this.handleDocumentKeyDown)}updateSize(){let{isRtl:e}=this.context,{alignmentEl:t,alignGridTop:n}=this.props,{rootEl:r}=this,i=function(e){let t=ns(e),n=e.getBoundingClientRect();for(let e of t){let t=Hi(n,e.getBoundingClientRect());if(!t)return null;n=t}return n}(t);if(i){let s=r.getBoundingClientRect(),o=n?Oe(t,".fc-scrollgrid").getBoundingClientRect().top:i.top,a=e?i.right-s.width:i.left;o=Math.max(o,10),a=Math.min(a,document.documentElement.clientWidth-10-s.width),a=Math.max(a,10);let l=r.offsetParent.getBoundingClientRect();Be(r,{top:o-l.top,left:a-l.left})}}}class yo extends ls{constructor(){super(...arguments),this.handleRootEl=e=>{this.rootEl=e,e?this.context.registerInteractiveComponent(this,{el:e,useEventCenter:!1}):this.context.unregisterInteractiveComponent(this)}}render(){let{options:e,dateEnv:t}=this.context,{props:n}=this,{startDate:r,todayRange:i,dateProfile:s}=n,o=t.format(r,e.dayPopoverFormat);return p(lo,{elRef:this.handleRootEl,date:r,dateProfile:s,todayRange:i},(t,r,i)=>p(vo,{elRef:i.ref,id:n.id,title:o,extraClassNames:["fc-more-popover"].concat(i.className||[]),extraAttrs:i,parentEl:n.parentEl,alignmentEl:n.alignmentEl,alignGridTop:n.alignGridTop,onClose:n.onClose},co(e)&&p(t,{elTag:"div",elClasses:["fc-more-popover-misc"]}),n.children))}queryHit(e,t,n,r){let{rootEl:i,props:s}=this;return e>=0&&e=0&&t{this.linkEl=e,this.props.elRef&&Gn(this.props.elRef,e)},this.handleClick=e=>{let{props:t,context:n}=this,{moreLinkClick:r}=n.options,i=So(t).start;function s(e){let{def:t,instance:r,range:i}=e.eventRange;return{event:new Kr(n,t,r),start:n.dateEnv.toDate(i.start),end:n.dateEnv.toDate(i.end),isStart:e.isStart,isEnd:e.isEnd}}"function"==typeof r&&(r=r({date:i,allDay:Boolean(t.allDayDate),allSegs:t.allSegs.map(s),hiddenSegs:t.hiddenSegs.map(s),jsEvent:e,view:n.viewApi})),r&&"popover"!==r?"string"==typeof r&&n.calendarApi.zoomTo(i,r):this.setState({isPopoverOpen:!0})},this.handlePopoverClose=()=>{this.setState({isPopoverOpen:!1})}}render(){let{props:e,state:t}=this;return p(Ln.Consumer,null,n=>{let{viewApi:r,options:i,calendarApi:s}=n,{moreLinkText:o}=i,{moreCnt:a}=e,l=So(e),c="function"==typeof o?o.call(s,a):`+${a} ${o}`,d=at(i.moreLinkHint,[a],c),u={num:a,shortText:"+"+a,text:c,view:r};return p(y,null,Boolean(e.moreCnt)&&p($n,{elTag:e.elTag||"a",elRef:this.handleLinkEl,elClasses:[...e.elClasses||[],"fc-more-link"],elStyle:e.elStyle,elAttrs:Object.assign(Object.assign(Object.assign({},e.elAttrs),Qe(this.handleClick)),{title:d,"aria-expanded":t.isPopoverOpen,"aria-controls":t.isPopoverOpen?t.popoverId:""}),renderProps:u,generatorName:"moreLinkContent",customGenerator:i.moreLinkContent,defaultGenerator:e.defaultGenerator||Eo,classNameGenerator:i.moreLinkClassNames,didMount:i.moreLinkDidMount,willUnmount:i.moreLinkWillUnmount},e.children),t.isPopoverOpen&&p(yo,{id:t.popoverId,startDate:l.start,endDate:l.end,dateProfile:e.dateProfile,todayRange:e.todayRange,extraDateSpan:e.extraDateSpan,parentEl:this.parentEl,alignmentEl:e.alignmentElRef?e.alignmentElRef.current:this.linkEl,alignGridTop:e.alignGridTop,forceTimed:e.forceTimed,onClose:this.handlePopoverClose},e.popoverContent()))})}componentDidMount(){this.updateParentEl()}componentDidUpdate(){this.updateParentEl()}updateParentEl(){this.linkEl&&(this.parentEl=Oe(this.linkEl,".fc-view-harness"))}}function Eo(e){return e.text}function So(e){if(e.allDayDate)return{start:e.allDayDate,end:wt(e.allDayDate,1)};let{hiddenSegs:t}=e;return{start:Ao(t),end:(n=t,n.reduce(wo).eventRange.range.end)};var n}function Ao(e){return e.reduce(Do).eventRange.range.start}function Do(e,t){return e.eventRange.range.startt.eventRange.range.end?e:t}var Co={__proto__:null,BASE_OPTION_DEFAULTS:cn,BaseComponent:Vn,BgEvent:ho,CalendarImpl:Ni,CalendarRoot:_i,ContentContainer:$n,CustomRenderingStore:class extends class{constructor(){this.handlers=[]}set(e){this.currentValue=e;for(let t of this.handlers)t(e)}subscribe(e){this.handlers.push(e),void 0!==this.currentValue&&e(this.currentValue)}}{constructor(){super(...arguments),this.map=new Map}handle(e){const{map:t}=this;let n=!1;e.isActive?(t.set(e.id,e),n=!0):t.has(e.id)&&(t.delete(e.id),n=!0),n&&this.set(t)}},DateComponent:ls,DateEnv:Pn,DateProfileGenerator:hr,DayCellContainer:lo,DayHeader:Rs,DaySeriesModel:_s,DayTableModel:Ts,DelayedRunner:Me,ElementDragging:ms,ElementScrollController:os,Emitter:Fr,EventContainer:ro,EventImpl:Kr,Interaction:Ti,MoreLinkContainer:bo,NamedTimeZoneImpl:class{constructor(e){this.timeZoneName=e}},NowIndicatorContainer:oo,NowTimer:Ii,PositionCache:rs,RefMap:Ws,ScrollController:ss,ScrollResponder:Un,Scroller:Ls,SegHierarchy:cs,SimpleScrollGrid:no,Slicer:ks,Splitter:Li,StandardEvent:io,TableDateCell:Ds,TableDowCell:Cs,Theme:Hn,ViewContainer:Kn,ViewContextType:Ln,WeekNumberContainer:po,WindowScrollController:as,addDays:wt,addDurations:pt,addMs:Ct,addWeeks:Dt,allowContextMenu:tt,allowSelection:Ke,applyMutationToEventStore:Zr,applyStyle:Be,asCleanDays:function(e){return e.years||e.months||e.milliseconds?0:e.days},asRoughMinutes:function(e){return yt(e)/6e4},asRoughMs:yt,asRoughSeconds:function(e){return yt(e)/1e3},binarySearch:ps,buildElAttrs:Yn,buildEntryKey:us,buildEventApis:ti,buildEventRangeKey:mi,buildIsoString:Ut,buildNavLinkAttrs:qi,buildSegTimeText:fi,collectFromHash:Tn,combineEventUis:Or,compareByFieldSpecs:rt,compareNumbers:lt,compareObjs:xn,computeEarliestSegStart:Ao,computeEdges:Ki,computeFallbackHeaderFormat:Es,computeInnerRect:es,computeRect:ts,computeShrinkWidth:Fs,computeVisibleDayRange:cr,config:vs,constrainPoint:Bi,createDuration:ft,createEmptyEventStore:Rr,createEventInstance:fr,createEventUi:Ir,createFormatter:an,diffDates:ur,diffDayAndTime:_t,diffDays:xt,diffPoints:zi,diffWeeks:Rt,diffWholeDays:kt,diffWholeWeeks:Tt,disableCursor:Xe,elementClosest:Oe,elementMatches:Ne,enableCursor:$e,eventTupleToStore:wr,filterHash:Sn,findDirectChildren:function(e,t){let n=e instanceof HTMLElement?[e]:e,r=[];for(let e=0;e{let o=r.length,a=s.length,l=0;for(;l{let o={};for(let a in s)if(i[a])if(St(r[a],s[a]))o[a]=i[a];else{n&&n(i[a]);let r=e.apply(this,s[a]);o[a]=t&&t(r,i[a])?i[a]:r}else o[a]=e.apply(this,s[a]);return r=s,i=o,o}},memoizeObjArg:Qt,mergeEventStores:xr,multiplyDuration:mt,padStart:ot,parseBusinessHours:Gr,parseClassNames:Tr,parseDragMeta:bs,parseEventDef:Ar,parseFieldSpecs:nt,parseMarker:Nn,pointInsideRect:Pi,preventContextMenu:et,preventDefault:We,preventSelection:Je,rangeContainsMarker:ar,rangeContainsRange:or,rangesEqual:ir,rangesIntersect:sr,refineEventDef:Er,refineProps:vn,removeElement:Ie,removeExact:function(e,t){let n=0,r=0;for(;r2&&(a.children=arguments.length>3?t.call(arguments,2):r),m(e.type,a,i||e.key,s||e.ref,null)},createElement:p,createRef:v,h:p,hydrate:function e(t,n){U(t,n,e)},get isValidElement(){return i},get options(){return n},render:U,toChildArray:M};const xo=[],_o={code:"en",week:{dow:0,doy:4},direction:"ltr",buttonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",year:"year",today:"today",month:"month",week:"week",day:"day",list:"list"},weekText:"W",weekTextLong:"Week",closeHint:"Close",timeHint:"Time",eventHint:"Event",allDayText:"all-day",moreLinkText:"more",noEventsText:"No events to display"},To=Object.assign(Object.assign({},_o),{buttonHints:{prev:"Previous $0",next:"Next $0",today:(e,t)=>"day"===t?"Today":"This "+e},viewHint:"$0 view",navLinkHint:"Go to $0",moreLinkHint:e=>`Show ${e} more event${1===e?"":"s"}`});function ko(e){let t=e.length>0?e[0].code:"en",n=xo.concat(e),r={en:To};for(let e of n)r[e.code]=e;return{map:r,defaultCode:t}}function Mo(e,t){return"object"!=typeof e||Array.isArray(e)?function(e,t){let n=[].concat(e||[]),r=function(e,t){for(let n=0;n0;e-=1){let n=r.slice(0,e).join("-");if(t[n])return t[n]}}return null}(n,t)||To;return Io(e,n,r)}(e,t):Io(e.code,[e.code],e)}function Io(e,t,n){let r=En([_o,n],["buttonText"]);delete r.code;let{week:i}=r;return delete r.week,{codeArg:e,codes:t,week:i,simpleNumberFormat:new Intl.NumberFormat(e),options:r}}function Oo(e){return{id:Ze(),name:e.name,premiumReleaseDate:e.premiumReleaseDate?new Date(e.premiumReleaseDate):void 0,deps:e.deps||[],reducers:e.reducers||[],isLoadingFuncs:e.isLoadingFuncs||[],contextInit:[].concat(e.contextInit||[]),eventRefiners:e.eventRefiners||{},eventDefMemberAdders:e.eventDefMemberAdders||[],eventSourceRefiners:e.eventSourceRefiners||{},isDraggableTransformers:e.isDraggableTransformers||[],eventDragMutationMassagers:e.eventDragMutationMassagers||[],eventDefMutationAppliers:e.eventDefMutationAppliers||[],dateSelectionTransformers:e.dateSelectionTransformers||[],datePointTransforms:e.datePointTransforms||[],dateSpanTransforms:e.dateSpanTransforms||[],views:e.views||{},viewPropsTransformers:e.viewPropsTransformers||[],isPropsValid:e.isPropsValid||null,externalDefTransforms:e.externalDefTransforms||[],viewContainerAppends:e.viewContainerAppends||[],eventDropTransformers:e.eventDropTransformers||[],componentInteractions:e.componentInteractions||[],calendarInteractions:e.calendarInteractions||[],themeClasses:e.themeClasses||{},eventSourceDefs:e.eventSourceDefs||[],cmdFormatter:e.cmdFormatter,recurringTypes:e.recurringTypes||[],namedTimeZonedImpl:e.namedTimeZonedImpl,initialView:e.initialView||"",elementDraggingImpl:e.elementDraggingImpl,optionChangeHandlers:e.optionChangeHandlers||{},scrollGridImpl:e.scrollGridImpl||null,listenerRefiners:e.listenerRefiners||{},optionRefiners:e.optionRefiners||{},propSetHandlers:e.propSetHandlers||{}}}function No(){let e,t=[],n=[];return(r,i)=>(e&&St(r,t)&&St(i,n)||(e=function(e,t){let n={},r={premiumReleaseDate:void 0,reducers:[],isLoadingFuncs:[],contextInit:[],eventRefiners:{},eventDefMemberAdders:[],eventSourceRefiners:{},isDraggableTransformers:[],eventDragMutationMassagers:[],eventDefMutationAppliers:[],dateSelectionTransformers:[],datePointTransforms:[],dateSpanTransforms:[],views:{},viewPropsTransformers:[],isPropsValid:null,externalDefTransforms:[],viewContainerAppends:[],eventDropTransformers:[],componentInteractions:[],calendarInteractions:[],themeClasses:{},eventSourceDefs:[],cmdFormatter:null,recurringTypes:[],namedTimeZonedImpl:null,initialView:"",elementDraggingImpl:null,optionChangeHandlers:{},scrollGridImpl:null,listenerRefiners:{},optionRefiners:{},propSetHandlers:{}};function i(e){for(let o of e){const e=o.name,a=n[e];void 0===a?(n[e]=o.id,i(o.deps),s=o,r={premiumReleaseDate:Po((t=r).premiumReleaseDate,s.premiumReleaseDate),reducers:t.reducers.concat(s.reducers),isLoadingFuncs:t.isLoadingFuncs.concat(s.isLoadingFuncs),contextInit:t.contextInit.concat(s.contextInit),eventRefiners:Object.assign(Object.assign({},t.eventRefiners),s.eventRefiners),eventDefMemberAdders:t.eventDefMemberAdders.concat(s.eventDefMemberAdders),eventSourceRefiners:Object.assign(Object.assign({},t.eventSourceRefiners),s.eventSourceRefiners),isDraggableTransformers:t.isDraggableTransformers.concat(s.isDraggableTransformers),eventDragMutationMassagers:t.eventDragMutationMassagers.concat(s.eventDragMutationMassagers),eventDefMutationAppliers:t.eventDefMutationAppliers.concat(s.eventDefMutationAppliers),dateSelectionTransformers:t.dateSelectionTransformers.concat(s.dateSelectionTransformers),datePointTransforms:t.datePointTransforms.concat(s.datePointTransforms),dateSpanTransforms:t.dateSpanTransforms.concat(s.dateSpanTransforms),views:Object.assign(Object.assign({},t.views),s.views),viewPropsTransformers:t.viewPropsTransformers.concat(s.viewPropsTransformers),isPropsValid:s.isPropsValid||t.isPropsValid,externalDefTransforms:t.externalDefTransforms.concat(s.externalDefTransforms),viewContainerAppends:t.viewContainerAppends.concat(s.viewContainerAppends),eventDropTransformers:t.eventDropTransformers.concat(s.eventDropTransformers),calendarInteractions:t.calendarInteractions.concat(s.calendarInteractions),componentInteractions:t.componentInteractions.concat(s.componentInteractions),themeClasses:Object.assign(Object.assign({},t.themeClasses),s.themeClasses),eventSourceDefs:t.eventSourceDefs.concat(s.eventSourceDefs),cmdFormatter:s.cmdFormatter||t.cmdFormatter,recurringTypes:t.recurringTypes.concat(s.recurringTypes),namedTimeZonedImpl:s.namedTimeZonedImpl||t.namedTimeZonedImpl,initialView:t.initialView||s.initialView,elementDraggingImpl:t.elementDraggingImpl||s.elementDraggingImpl,optionChangeHandlers:Object.assign(Object.assign({},t.optionChangeHandlers),s.optionChangeHandlers),scrollGridImpl:s.scrollGridImpl||t.scrollGridImpl,listenerRefiners:Object.assign(Object.assign({},t.listenerRefiners),s.listenerRefiners),optionRefiners:Object.assign(Object.assign({},t.optionRefiners),s.optionRefiners),propSetHandlers:Object.assign(Object.assign({},t.propSetHandlers),s.propSetHandlers)}):a!==o.id&&console.warn(`Duplicate plugin '${e}'`)}var t,s}return e&&i(e),i(t),r}(r,i)),t=r,n=i,e)}function Po(e,t){return void 0===e?t:void 0===t?e:new Date(Math.max(e.valueOf(),t.valueOf()))}class Ho extends Hn{}function Bo(e,t,n,r){if(t[e])return t[e];let i=function(e,t,n,r){let i=n[e],s=r[e],o=e=>i&&null!==i[e]?i[e]:s&&null!==s[e]?s[e]:null,a=o("component"),l=o("superType"),c=null;if(l){if(l===e)throw new Error("Can't have a custom view type that references itself");c=Bo(l,t,n,r)}!a&&c&&(a=c.component);if(!a)return null;return{type:e,component:a,defaults:Object.assign(Object.assign({},c?c.defaults:{}),i?i.rawOptions:{}),overrides:Object.assign(Object.assign({},c?c.overrides:{}),s?s.rawOptions:{})}}(e,t,n,r);return i&&(t[e]=i),i}function jo(e){return An(e,zo)}function zo(e){let t="function"==typeof e?{component:e}:e,{component:n}=t;return t.content?n=Uo(t):!n||n.prototype instanceof Vn||(n=Uo(Object.assign(Object.assign({},t),{content:n}))),{superType:t.type,component:n,rawOptions:t}}function Uo(e){return t=>p(Ln.Consumer,null,n=>p($n,{elTag:"div",elClasses:er(n.viewSpec),renderProps:Object.assign(Object.assign({},t),{nextDayThreshold:n.options.nextDayThreshold}),generatorName:void 0,customGenerator:e.content,classNameGenerator:e.classNames,didMount:e.didMount,willUnmount:e.willUnmount}))}function Lo(e,t,n,r){let i=jo(e),s=jo(t.views);return An(function(e,t){let n,r={};for(n in e)Bo(n,r,e,t);for(n in t)Bo(n,r,e,t);return r}(i,s),e=>function(e,t,n,r,i){let s=e.overrides.duration||e.defaults.duration||r.duration||n.duration,o=null,a="",l="",c={};if(s&&(o=function(e){let t=JSON.stringify(e),n=Wo[t];void 0===n&&(n=ft(e),Wo[t]=n);return n}(s),o)){let e=Et(o);a=e.unit,1===e.value&&(l=a,c=t[a]?t[a].rawOptions:{})}let d=t=>{let n=t.buttonText||{},r=e.defaults.buttonTextKey;return null!=r&&null!=n[r]?n[r]:null!=n[e.type]?n[e.type]:null!=n[l]?n[l]:null},u=t=>{let n=t.buttonHints||{},r=e.defaults.buttonTextKey;return null!=r&&null!=n[r]?n[r]:null!=n[e.type]?n[e.type]:null!=n[l]?n[l]:null};return{type:e.type,component:e.component,duration:o,durationUnit:a,singleUnit:l,optionDefaults:e.defaults,optionOverrides:Object.assign(Object.assign({},c),e.overrides),buttonTextOverride:d(r)||d(n)||e.overrides.buttonText,buttonTextDefault:d(i)||e.defaults.buttonText||d(cn)||e.type,buttonTitleOverride:u(r)||u(n)||e.overrides.buttonHint,buttonTitleDefault:u(i)||e.defaults.buttonHint||u(cn)}}(e,s,t,n,r))}Ho.prototype.classes={root:"fc-theme-standard",tableCellShaded:"fc-cell-shaded",buttonGroup:"fc-button-group",button:"fc-button fc-button-primary",buttonActive:"fc-button-active"},Ho.prototype.baseIconClass="fc-icon",Ho.prototype.iconClasses={close:"fc-icon-x",prev:"fc-icon-chevron-left",next:"fc-icon-chevron-right",prevYear:"fc-icon-chevrons-left",nextYear:"fc-icon-chevrons-right"},Ho.prototype.rtlIconClasses={prev:"fc-icon-chevron-right",next:"fc-icon-chevron-left",prevYear:"fc-icon-chevrons-right",nextYear:"fc-icon-chevrons-left"},Ho.prototype.iconOverrideOption="buttonIcons",Ho.prototype.iconOverrideCustomButtonOption="icon",Ho.prototype.iconOverridePrefix="fc-icon-";let Wo={};function Fo(e,t,n){let r=t?t.activeRange:null;return Qo({},function(e,t){let n=Br(t),r=[].concat(e.eventSources||[]),i=[];e.initialEvents&&r.unshift(e.initialEvents);e.events&&r.unshift(e.events);for(let e of r){let r=Hr(e,t,n);r&&i.push(r)}return i}(e,n),r,n)}function Vo(e,t,n,r){let i=n?n.activeRange:null;switch(t.type){case"ADD_EVENT_SOURCES":return Qo(e,t.sources,i,r);case"REMOVE_EVENT_SOURCE":return s=e,o=t.sourceId,Sn(s,e=>e.sourceId!==o);case"PREV":case"NEXT":case"CHANGE_DATE":case"CHANGE_VIEW_TYPE":return n?qo(e,i,r):e;case"FETCH_EVENT_SOURCES":return Yo(e,t.sourceIds?Dn(t.sourceIds):Xo(e,r),i,t.isRefetch||!1,r);case"RECEIVE_EVENTS":case"RECEIVE_EVENT_ERROR":return function(e,t,n,r){let i=e[t];if(i&&n===i.latestFetchId)return Object.assign(Object.assign({},e),{[t]:Object.assign(Object.assign({},i),{isFetching:!1,fetchRange:r})});return e}(e,t.sourceId,t.fetchId,t.fetchRange);case"REMOVE_ALL_EVENT_SOURCES":return{};default:return e}var s,o}function Go(e){for(let t in e)if(e[t].isFetching)return!0;return!1}function Qo(e,t,n,r){let i={};for(let e of t)i[e.sourceId]=e;return n&&(i=qo(i,n,r)),Object.assign(Object.assign({},e),i)}function qo(e,t,n){return Yo(e,Sn(e,e=>function(e,t,n){if(!$o(e,n))return!e.latestFetchId;return!n.options.lazyFetching||!e.fetchRange||e.isFetching||t.starte.fetchRange.end}(e,t,n)),t,!1,n)}function Yo(e,t,n,r,i){let s={};for(let o in e){let a=e[o];t[o]?s[o]=Zo(a,n,r,i):s[o]=a}return s}function Zo(e,t,n,r){let{options:i,calendarApi:s}=r,o=r.pluginHooks.eventSourceDefs[e.sourceDefId],a=Ze();return o.fetch({eventSource:e,range:t,isRefetch:n,context:r},n=>{let{rawEvents:o}=n;i.eventSourceSuccess&&(o=i.eventSourceSuccess.call(s,o,n.response)||o),e.success&&(o=e.success.call(s,o,n.response)||o),r.dispatch({type:"RECEIVE_EVENTS",sourceId:e.sourceId,fetchId:a,fetchRange:t,rawEvents:o})},n=>{let o=!1;i.eventSourceFailure&&(i.eventSourceFailure.call(s,n),o=!0),e.failure&&(e.failure(n),o=!0),o||console.warn(n.message,n),r.dispatch({type:"RECEIVE_EVENT_ERROR",sourceId:e.sourceId,fetchId:a,fetchRange:t,error:n})}),Object.assign(Object.assign({},e),{isFetching:!0,latestFetchId:a})}function Xo(e,t){return Sn(e,e=>$o(e,t))}function $o(e,t){return!t.pluginHooks.eventSourceDefs[e.sourceDefId].ignoreRange}function Jo(e,t){switch(t.type){case"UNSELECT_DATES":return null;case"SELECT_DATES":return t.selection;default:return e}}function Ko(e,t){switch(t.type){case"UNSELECT_EVENT":return"";case"SELECT_EVENT":return t.eventInstanceId;default:return e}}function ea(e,t){let n;switch(t.type){case"UNSET_EVENT_DRAG":return null;case"SET_EVENT_DRAG":return n=t.state,{affectedEvents:n.affectedEvents,mutatedEvents:n.mutatedEvents,isEvent:n.isEvent};default:return e}}function ta(e,t){let n;switch(t.type){case"UNSET_EVENT_RESIZE":return null;case"SET_EVENT_RESIZE":return n=t.state,{affectedEvents:n.affectedEvents,mutatedEvents:n.mutatedEvents,isEvent:n.isEvent};default:return e}}function na(e,t,n,r,i){return{header:e.headerToolbar?ra(e.headerToolbar,e,t,n,r,i):null,footer:e.footerToolbar?ra(e.footerToolbar,e,t,n,r,i):null}}function ra(e,t,n,r,i,s){let o={},a=[],l=!1;for(let c in e){let d=ia(e[c],t,n,r,i,s);o[c]=d.widgets,a.push(...d.viewsWithButtons),l=l||d.hasTitle}return{sectionWidgets:o,viewsWithButtons:a,hasTitle:l}}function ia(e,t,n,r,i,s){let o="rtl"===t.direction,a=t.customButtons||{},l=n.buttonText||{},c=t.buttonText||{},d=n.buttonHints||{},u=t.buttonHints||{},h=e?e.split(" "):[],f=[],g=!1;return{widgets:h.map(e=>e.split(",").map(e=>{if("title"===e)return g=!0,{buttonName:e};let n,h,p,m,v,y;if(n=a[e])p=e=>{n.click&&n.click.call(e.target,e,e.target)},(m=r.getCustomButtonIconClass(n))||(m=r.getIconClass(e,o))||(v=n.text),y=n.hint||n.text;else if(h=i[e]){f.push(e),p=()=>{s.changeView(e)},(v=h.buttonTextOverride)||(m=r.getIconClass(e,o))||(v=h.buttonTextDefault);let n=h.buttonTextOverride||h.buttonTextDefault;y=at(h.buttonTitleOverride||h.buttonTitleDefault||t.viewHint,[n,e],n)}else if(s[e])if(p=()=>{s[e]()},(v=l[e])||(m=r.getIconClass(e,o))||(v=c[e]),"prevYear"===e||"nextYear"===e){let t="prevYear"===e?"prev":"next";y=at(d[t]||u[t],[c.year||"year","year"],c[e])}else y=t=>at(d[e]||u[e],[c[t]||t,t],c[e]);return{buttonName:e,buttonClick:p,buttonIcon:m,buttonText:v,buttonHint:y}})),viewsWithButtons:f,hasTitle:g}}class sa{constructor(e,t,n){this.type=e,this.getCurrentData=t,this.dateEnv=n}get calendar(){return this.getCurrentData().calendarApi}get title(){return this.getCurrentData().viewTitle}get activeStart(){return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.start)}get activeEnd(){return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.end)}get currentStart(){return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.start)}get currentEnd(){return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.end)}getOption(e){return this.getCurrentData().options[e]}}function oa(e,t){let n=wn(t.getCurrentData().eventSources);if(1===n.length&&1===e.length&&Array.isArray(n[0]._raw)&&Array.isArray(e[0]))return void t.dispatch({type:"RESET_RAW_EVENTS",sourceId:n[0].sourceId,rawEvents:e[0]});let r=[];for(let t of e){let e=!1;for(let r=0;rArray.isArray(e.events)?e.events:null,fetch(e,t){t({rawEvents:e.eventSource.meta})}}]}),Oo({name:"func-event-source",eventSourceDefs:[{parseMeta:e=>"function"==typeof e.events?e.events:null,fetch(e,t,n){const{dateEnv:r}=e.context;Di(e.eventSource.meta.bind(null,Si(e.range,r)),e=>t({rawEvents:e}),n)}}]}),Oo({name:"json-event-source",eventSourceRefiners:{method:String,extraParams:yn,startParam:String,endParam:String,timeZoneParam:String},eventSourceDefs:[{parseMeta:e=>!e.url||"json"!==e.format&&e.format?null:{url:e.url,format:"json",method:(e.method||"GET").toUpperCase(),extraParams:e.extraParams,startParam:e.startParam,endParam:e.endParam,timeZoneParam:e.timeZoneParam},fetch(e,t,n){const{meta:r}=e.eventSource,i=function(e,t,n){let r,i,s,o,{dateEnv:a,options:l}=n,c={};r=e.startParam,null==r&&(r=l.startParam);i=e.endParam,null==i&&(i=l.endParam);s=e.timeZoneParam,null==s&&(s=l.timeZoneParam);o="function"==typeof e.extraParams?e.extraParams():e.extraParams||{};Object.assign(c,o),c[r]=a.formatIso(t.start),c[i]=a.formatIso(t.end),"local"!==a.timeZone&&(c[s]=a.timeZone);return c}(r,e.range,e.context);Ci(r.method,r.url,i).then(([e,n])=>{t({rawEvents:e,response:n})},n)}}]}),Oo({name:"simple-recurring-event",recurringTypes:[{parse(e,t){if(e.daysOfWeek||e.startTime||e.endTime||e.startRecur||e.endRecur){let i,s={daysOfWeek:e.daysOfWeek||null,startTime:e.startTime||null,endTime:e.endTime||null,startRecur:e.startRecur?t.createMarker(e.startRecur):null,endRecur:e.endRecur?t.createMarker(e.endRecur):null,dateEnv:t};return e.duration&&(i=e.duration),!i&&e.startTime&&e.endTime&&(n=e.endTime,r=e.startTime,i={years:n.years-r.years,months:n.months-r.months,days:n.days-r.days,milliseconds:n.milliseconds-r.milliseconds}),{allDayGuess:Boolean(!e.startTime&&!e.endTime),duration:i,typeData:s}}var n,r;return null},expand(e,t,n){let r=rr(t,{start:e.startRecur,end:e.endRecur});return r?function(e,t,n,r,i){let s=e?Dn(e):null,o=Mt(i.start),a=i.end,l=[];t&&(t.milliseconds<0?a=wt(a,1):t.milliseconds>=864e5&&(o=wt(o,-1)));for(;oGo(e.eventSources)],propSetHandlers:{dateProfile:function(e,t){t.emitter.trigger("datesSet",Object.assign(Object.assign({},Si(e.activeRange,t.dateEnv)),{view:t.viewApi}))},eventStore:function(e,t){let{emitter:n}=t;n.hasHandlers("eventsSet")&&n.trigger("eventsSet",ti(e,t))}}})];class la{constructor(e,t){this.runTaskOption=e,this.drainedOption=t,this.queue=[],this.delayedRunner=new Me(this.drain.bind(this))}request(e,t){this.queue.push(e),this.delayedRunner.request(t)}pause(e){this.delayedRunner.pause(e)}resume(e,t){this.delayedRunner.resume(e,t)}drain(){let{queue:e}=this;for(;e.length;){let t,n=[];for(;t=e.shift();)this.runTask(t),n.push(t);this.drained(n)}}runTask(e){this.runTaskOption&&this.runTaskOption(e)}drained(e){this.drainedOption&&this.drainedOption(e)}}function ca(e,t,n){let r;return r=/^(year|month)$/.test(e.currentRangeUnit)?e.currentRange:e.activeRange,n.formatRange(r.start,r.end,an(t.titleFormat||function(e){let{currentRangeUnit:t}=e;if("year"===t)return{year:"numeric"};if("month"===t)return{year:"numeric",month:"long"};let n=kt(e.currentRange.start,e.currentRange.end);if(null!==n&&n>1)return{year:"numeric",month:"short",day:"numeric"};return{year:"numeric",month:"long",day:"numeric"}}(e)),{isEndExclusive:e.isRangeAllDay,defaultSeparator:t.titleRangeSeparator})}class da{constructor(){this.resetListeners=new Set}handleInput(e,t){const n=this.dateEnv;if(e!==n&&("function"==typeof t?this.nowFn=t:n||(this.nowAnchorDate=e.toDate(t?e.createMarker(t):e.createNowMarker()),this.nowAnchorQueried=Date.now()),this.dateEnv=e,n))for(const e of this.resetListeners.values())e()}getDateMarker(){return this.nowAnchorDate?this.dateEnv.timestampToMarker(this.nowAnchorDate.valueOf()+(Date.now()-this.nowAnchorQueried)):this.dateEnv.createMarker(this.nowFn())}addResetListener(e){this.resetListeners.add(e)}removeResetListener(e){this.resetListeners.delete(e)}}class ua{constructor(e){this.computeCurrentViewData=Gt(this._computeCurrentViewData),this.organizeRawLocales=Gt(ko),this.buildLocale=Gt(Mo),this.buildPluginHooks=No(),this.buildDateEnv=Gt(ha),this.buildTheme=Gt(fa),this.parseToolbars=Gt(na),this.buildViewSpecs=Gt(Lo),this.buildDateProfileGenerator=Qt(ga),this.buildViewApi=Gt(pa),this.buildViewUiProps=Qt(ya),this.buildEventUiBySource=Gt(ma,Cn),this.buildEventUiBases=Gt(va),this.parseContextBusinessHours=Qt(Ea),this.buildTitle=Gt(ca),this.nowManager=new da,this.emitter=new Fr,this.actionRunner=new la(this._handleAction.bind(this),this.updateData.bind(this)),this.currentCalendarOptionsInput={},this.currentCalendarOptionsRefined={},this.currentViewOptionsInput={},this.currentViewOptionsRefined={},this.currentCalendarOptionsRefiners={},this.optionsForRefining=[],this.optionsForHandling=[],this.getCurrentData=()=>this.data,this.dispatch=e=>{this.actionRunner.request(e)},this.props=e,this.actionRunner.pause(),this.nowManager=new da;let t={},n=this.computeOptionsData(e.optionOverrides,t,e.calendarApi),r=n.calendarOptions.initialView||n.pluginHooks.initialView,i=this.computeCurrentViewData(r,n,e.optionOverrides,t);e.calendarApi.currentDataManager=this,this.emitter.setThisContext(e.calendarApi),this.emitter.setOptions(i.options);let s={nowManager:this.nowManager,dateEnv:n.dateEnv,options:n.calendarOptions,pluginHooks:n.pluginHooks,calendarApi:e.calendarApi,dispatch:this.dispatch,emitter:this.emitter,getCurrentData:this.getCurrentData},o=function(e,t,n){let r=e.initialDate;return null!=r?t.createMarker(r):n.getDateMarker()}(n.calendarOptions,n.dateEnv,this.nowManager),a=i.dateProfileGenerator.build(o);ar(a.activeRange,o)||(o=a.currentRange.start);for(let e of n.pluginHooks.contextInit)e(s);let l=Fo(n.calendarOptions,a,s),c={dynamicOptionOverrides:t,currentViewType:r,currentDate:o,dateProfile:a,businessHours:this.parseContextBusinessHours(s),eventSources:l,eventUiBases:{},eventStore:{defs:{},instances:{}},renderableEventStore:{defs:{},instances:{}},dateSelection:null,eventSelection:"",eventDrag:null,eventResize:null,selectionConfig:this.buildViewUiProps(s).selectionConfig},d=Object.assign(Object.assign({},s),c);for(let e of n.pluginHooks.reducers)Object.assign(c,e(null,null,d));ba(c,s)&&this.emitter.trigger("loading",!0),this.state=c,this.updateData(),this.actionRunner.resume()}resetOptions(e,t){let{props:n}=this;void 0===t?n.optionOverrides=e:(n.optionOverrides=Object.assign(Object.assign({},n.optionOverrides||{}),e),this.optionsForRefining.push(...t)),(void 0===t||t.length)&&this.actionRunner.request({type:"NOTHING"})}_handleAction(e){let{props:t,state:n,emitter:r}=this,i=function(e,t){switch(t.type){case"SET_OPTION":return Object.assign(Object.assign({},e),{[t.optionName]:t.rawOptionValue});default:return e}}(n.dynamicOptionOverrides,e),s=this.computeOptionsData(t.optionOverrides,i,t.calendarApi),o=function(e,t){switch(t.type){case"CHANGE_VIEW_TYPE":e=t.viewType}return e}(n.currentViewType,e),a=this.computeCurrentViewData(o,s,t.optionOverrides,i);t.calendarApi.currentDataManager=this,r.setThisContext(t.calendarApi),r.setOptions(a.options);let l={nowManager:this.nowManager,dateEnv:s.dateEnv,options:s.calendarOptions,pluginHooks:s.pluginHooks,calendarApi:t.calendarApi,dispatch:this.dispatch,emitter:r,getCurrentData:this.getCurrentData},{currentDate:c,dateProfile:d}=n;this.data&&this.data.dateProfileGenerator!==a.dateProfileGenerator&&(d=a.dateProfileGenerator.build(c)),c=function(e,t){switch(t.type){case"CHANGE_DATE":return t.dateMarker;default:return e}}(c,e),d=function(e,t,n,r){let i;switch(t.type){case"CHANGE_VIEW_TYPE":return r.build(t.dateMarker||n);case"CHANGE_DATE":return r.build(t.dateMarker);case"PREV":if(i=r.buildPrev(e,n),i.isValid)return i;break;case"NEXT":if(i=r.buildNext(e,n),i.isValid)return i}return e}(d,e,c,a.dateProfileGenerator),"PREV"!==e.type&&"NEXT"!==e.type&&ar(d.currentRange,c)||(c=d.currentRange.start);let u=Vo(n.eventSources,e,d,l),h=jr(n.eventStore,e,u,d,l),f=Go(u)&&!a.options.progressiveEventRendering&&n.renderableEventStore||h,{eventUiSingleBase:g,selectionConfig:p}=this.buildViewUiProps(l),m=this.buildEventUiBySource(u),v={dynamicOptionOverrides:i,currentViewType:o,currentDate:c,dateProfile:d,eventSources:u,eventStore:h,renderableEventStore:f,selectionConfig:p,eventUiBases:this.buildEventUiBases(f.defs,g,m),businessHours:this.parseContextBusinessHours(l),dateSelection:Jo(n.dateSelection,e),eventSelection:Ko(n.eventSelection,e),eventDrag:ea(n.eventDrag,e),eventResize:ta(n.eventResize,e)},y=Object.assign(Object.assign({},l),v);for(let t of s.pluginHooks.reducers)Object.assign(v,t(n,e,y));let b=ba(n,l),E=ba(v,l);!b&&E?r.trigger("loading",!0):b&&!E&&r.trigger("loading",!1),this.state=v,t.onAction&&t.onAction(e)}updateData(){let{props:e,state:t}=this,n=this.data,r=this.computeOptionsData(e.optionOverrides,t.dynamicOptionOverrides,e.calendarApi),i=this.computeCurrentViewData(t.currentViewType,r,e.optionOverrides,t.dynamicOptionOverrides),s=this.data=Object.assign(Object.assign(Object.assign({nowManager:this.nowManager,viewTitle:this.buildTitle(t.dateProfile,i.options,r.dateEnv),calendarApi:e.calendarApi,dispatch:this.dispatch,emitter:this.emitter,getCurrentData:this.getCurrentData},r),i),t),o=r.pluginHooks.optionChangeHandlers,a=n&&n.calendarOptions,l=r.calendarOptions;if(a&&a!==l){a.timeZone!==l.timeZone&&(t.eventSources=s.eventSources=function(e,t,n){let r=t?t.activeRange:null;return Yo(e,Xo(e,n),r,!0,n)}(s.eventSources,t.dateProfile,s),t.eventStore=s.eventStore=Lr(s.eventStore,n.dateEnv,s.dateEnv),t.renderableEventStore=s.renderableEventStore=Lr(s.renderableEventStore,n.dateEnv,s.dateEnv));for(let e in o)-1===this.optionsForHandling.indexOf(e)&&a[e]===l[e]||o[e](l[e],s)}this.optionsForHandling=[],e.onData&&e.onData(s)}computeOptionsData(e,t,n){if(!this.optionsForRefining.length&&e===this.stableOptionOverrides&&t===this.stableDynamicOptionOverrides)return this.stableCalendarOptionsData;let{refinedOptions:r,pluginHooks:i,localeDefaults:s,availableLocaleData:o,extra:a}=this.processRawCalendarOptions(e,t);Sa(a);let l=this.buildDateEnv(r.timeZone,r.locale,r.weekNumberCalculation,r.firstDay,r.weekText,i,o,r.defaultRangeSeparator),c=this.buildViewSpecs(i.views,this.stableOptionOverrides,this.stableDynamicOptionOverrides,s),d=this.buildTheme(r,i),u=this.parseToolbars(r,this.stableOptionOverrides,d,c,n);return this.stableCalendarOptionsData={calendarOptions:r,pluginHooks:i,dateEnv:l,viewSpecs:c,theme:d,toolbarConfig:u,localeDefaults:s,availableRawLocales:o.map}}processRawCalendarOptions(e,t){let{locales:n,locale:r}=mn([cn,e,t]),i=this.organizeRawLocales(n),s=i.map,o=this.buildLocale(r||i.defaultCode,s).options,a=this.buildPluginHooks(e.plugins||[],aa),l=this.currentCalendarOptionsRefiners=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},ln),dn),un),a.listenerRefiners),a.optionRefiners),c={},d=mn([cn,o,e,t]),u={},h=this.currentCalendarOptionsInput,f=this.currentCalendarOptionsRefined,g=!1;for(let e in d)-1===this.optionsForRefining.indexOf(e)&&(d[e]===h[e]||hn[e]&&e in h&&hn[e](h[e],d[e]))?u[e]=f[e]:l[e]?(u[e]=l[e](d[e]),g=!0):c[e]=h[e];return g&&(this.currentCalendarOptionsInput=d,this.currentCalendarOptionsRefined=u,this.stableOptionOverrides=e,this.stableDynamicOptionOverrides=t),this.optionsForHandling.push(...this.optionsForRefining),this.optionsForRefining=[],{rawOptions:this.currentCalendarOptionsInput,refinedOptions:this.currentCalendarOptionsRefined,pluginHooks:a,availableLocaleData:i,localeDefaults:o,extra:c}}_computeCurrentViewData(e,t,n,r){let i=t.viewSpecs[e];if(!i)throw new Error(`viewType "${e}" is not available. Please make sure you've loaded all neccessary plugins`);let{refinedOptions:s,extra:o}=this.processRawViewOptions(i,t.pluginHooks,t.localeDefaults,n,r);return Sa(o),this.nowManager.handleInput(t.dateEnv,s.now),{viewSpec:i,options:s,dateProfileGenerator:this.buildDateProfileGenerator({dateProfileGeneratorClass:i.optionDefaults.dateProfileGeneratorClass,nowManager:this.nowManager,duration:i.duration,durationUnit:i.durationUnit,usesMinMaxTime:i.optionDefaults.usesMinMaxTime,dateEnv:t.dateEnv,calendarApi:this.props.calendarApi,slotMinTime:s.slotMinTime,slotMaxTime:s.slotMaxTime,showNonCurrentDates:s.showNonCurrentDates,dayCount:s.dayCount,dateAlignment:s.dateAlignment,dateIncrement:s.dateIncrement,hiddenDays:s.hiddenDays,weekends:s.weekends,validRangeInput:s.validRange,visibleRangeInput:s.visibleRange,fixedWeekCount:s.fixedWeekCount}),viewApi:this.buildViewApi(e,this.getCurrentData,t.dateEnv)}}processRawViewOptions(e,t,n,r,i){let s=mn([cn,e.optionDefaults,n,r,e.optionOverrides,i]),o=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},ln),dn),un),pn),t.listenerRefiners),t.optionRefiners),a={},l=this.currentViewOptionsInput,c=this.currentViewOptionsRefined,d=!1,u={};for(let e in s)s[e]===l[e]||hn[e]&&hn[e](s[e],l[e])?a[e]=c[e]:(s[e]===this.currentCalendarOptionsInput[e]||hn[e]&&hn[e](s[e],this.currentCalendarOptionsInput[e])?e in this.currentCalendarOptionsRefined&&(a[e]=this.currentCalendarOptionsRefined[e]):o[e]?a[e]=o[e](s[e]):u[e]=s[e],d=!0);return d&&(this.currentViewOptionsInput=s,this.currentViewOptionsRefined=a),{rawOptions:this.currentViewOptionsInput,refinedOptions:this.currentViewOptionsRefined,extra:u}}}function ha(e,t,n,r,i,s,o,a){let l=Mo(t||o.defaultCode,o.map);return new Pn({calendarSystem:"gregory",timeZone:e,namedTimeZoneImpl:s.namedTimeZonedImpl,locale:l,weekNumberCalculation:n,firstDay:r,weekText:i,cmdFormatter:s.cmdFormatter,defaultSeparator:a})}function fa(e,t){return new(t.themeClasses[e.themeSystem]||Ho)(e)}function ga(e){return new(e.dateProfileGeneratorClass||hr)(e)}function pa(e,t,n){return new sa(e,t,n)}function ma(e){return An(e,e=>e.ui)}function va(e,t,n){let r={"":t};for(let t in e){let i=e[t];i.sourceId&&n[i.sourceId]&&(r[t]=n[i.sourceId])}return r}function ya(e){let{options:t}=e;return{eventUiSingleBase:Ir({display:t.eventDisplay,editable:t.editable,startEditable:t.eventStartEditable,durationEditable:t.eventDurationEditable,constraint:t.eventConstraint,overlap:"boolean"==typeof t.eventOverlap?t.eventOverlap:void 0,allow:t.eventAllow,backgroundColor:t.eventBackgroundColor,borderColor:t.eventBorderColor,textColor:t.eventTextColor,color:t.eventColor},e),selectionConfig:Ir({constraint:t.selectConstraint,overlap:"boolean"==typeof t.selectOverlap?t.selectOverlap:void 0,allow:t.selectAllow},e)}}function ba(e,t){for(let n of t.pluginHooks.isLoadingFuncs)if(n(e))return!0;return!1}function Ea(e){return Gr(e.options.businessHours,e)}function Sa(e,t){for(let n in e)console.warn(`Unknown option '${n}'`+(t?` for view '${t}'`:""))}class Aa extends Vn{render(){return p("div",{className:"fc-toolbar-chunk"},...this.props.widgetGroups.map(e=>this.renderWidgetGroup(e)))}renderWidgetGroup(e){let{props:t}=this,{theme:n}=this.context,r=[],i=!0;for(let s of e){let{buttonName:e,buttonClick:o,buttonText:a,buttonIcon:l,buttonHint:c}=s;if("title"===e)i=!1,r.push(p("h2",{className:"fc-toolbar-title",id:t.titleId},t.title));else{let i=e===t.activeButton,s=!t.isTodayEnabled&&"today"===e||!t.isPrevEnabled&&"prev"===e||!t.isNextEnabled&&"next"===e,d=[`fc-${e}-button`,n.getClass("button")];i&&d.push(n.getClass("buttonActive")),r.push(p("button",{type:"button",title:"function"==typeof c?c(t.navUnit):c,disabled:s,"aria-pressed":i,className:d.join(" "),onClick:o},a||(l?p("span",{className:l,role:"img"}):"")))}}if(r.length>1){return p("div",{className:i&&n.getClass("buttonGroup")||""},...r)}return r[0]}}class Da extends Vn{render(){let e,t,{model:n,extraClassName:r}=this.props,i=!1,s=n.sectionWidgets,o=s.center;return s.left?(i=!0,e=s.left):e=s.start,s.right?(i=!0,t=s.right):t=s.end,p("div",{className:[r||"","fc-toolbar",i?"fc-toolbar-ltr":""].join(" ")},this.renderSection("start",e||[]),this.renderSection("center",o||[]),this.renderSection("end",t||[]))}renderSection(e,t){let{props:n}=this;return p(Aa,{key:e,widgetGroups:t,title:n.title,navUnit:n.navUnit,activeButton:n.activeButton,isTodayEnabled:n.isTodayEnabled,isPrevEnabled:n.isPrevEnabled,isNextEnabled:n.isNextEnabled,titleId:n.titleId})}}class wa extends Vn{constructor(){super(...arguments),this.state={availableWidth:null},this.handleEl=e=>{this.el=e,Gn(this.props.elRef,e),this.updateAvailableWidth()},this.handleResize=()=>{this.updateAvailableWidth()}}render(){let{props:e,state:t}=this,{aspectRatio:n}=e,r=["fc-view-harness",n||e.liquid||e.height?"fc-view-harness-active":"fc-view-harness-passive"],i="",s="";return n?null!==t.availableWidth?i=t.availableWidth/n:s=1/n*100+"%":i=e.height||"",p("div",{"aria-labelledby":e.labeledById,ref:this.handleEl,className:r.join(" "),style:{height:i,paddingBottom:s}},e.children)}componentDidMount(){this.context.addResizeHandler(this.handleResize)}componentWillUnmount(){this.context.removeResizeHandler(this.handleResize)}updateAvailableWidth(){this.el&&this.props.aspectRatio&&this.setState({availableWidth:this.el.offsetWidth})}}class Ca extends Ti{constructor(e){super(e),this.handleSegClick=(e,t)=>{let{component:n}=this,{context:r}=n,i=si(t);if(i&&n.isValidSegDownEl(e.target)){let s=Oe(e.target,".fc-event-forced-url"),o=s?s.querySelector("a[href]").href:"";r.emitter.trigger("eventClick",{el:t,event:new Kr(n.context,i.eventRange.def,i.eventRange.instance),jsEvent:e,view:r.viewApi}),o&&!e.defaultPrevented&&(window.location.href=o)}},this.destroy=Fe(e.el,"click",".fc-event",this.handleSegClick)}}class Ra extends Ti{constructor(e){super(e),this.handleEventElRemove=e=>{e===this.currentSegEl&&this.handleSegLeave(null,this.currentSegEl)},this.handleSegEnter=(e,t)=>{si(t)&&(this.currentSegEl=t,this.triggerEvent("eventMouseEnter",e,t))},this.handleSegLeave=(e,t)=>{this.currentSegEl&&(this.currentSegEl=null,this.triggerEvent("eventMouseLeave",e,t))},this.removeHoverListeners=function(e,t,n,r){let i;return Fe(e,"mouseover",t,(e,t)=>{if(t!==i){i=t,n(e,t);let s=e=>{i=null,r(e,t),t.removeEventListener("mouseleave",s)};t.addEventListener("mouseleave",s)}})}(e.el,".fc-event",this.handleSegEnter,this.handleSegLeave)}destroy(){this.removeHoverListeners()}triggerEvent(e,t,n){let{component:r}=this,{context:i}=r,s=si(n);t&&!r.isValidSegDownEl(t.target)||i.emitter.trigger(e,{el:n,event:new Kr(i,s.eventRange.def,s.eventRange.instance),jsEvent:t,view:i.viewApi})}}class xa extends Fn{constructor(){super(...arguments),this.buildViewContext=Gt(Wn),this.buildViewPropTransformers=Gt(Ta),this.buildToolbarProps=Gt(_a),this.headerRef={current:null},this.footerRef={current:null},this.interactionsStore={},this.state={viewLabelId:Le()},this.registerInteractiveComponent=(e,t)=>{let n=function(e,t){return{component:e,el:t.el,useEventCenter:null==t.useEventCenter||t.useEventCenter,isHitComboAllowed:t.isHitComboAllowed||null}}(e,t),r=[Ca,Ra].concat(this.props.pluginHooks.componentInteractions).map(e=>new e(n));this.interactionsStore[e.uid]=r,Mi[e.uid]=n},this.unregisterInteractiveComponent=e=>{let t=this.interactionsStore[e.uid];if(t){for(let e of t)e.destroy();delete this.interactionsStore[e.uid]}delete Mi[e.uid]},this.resizeRunner=new Me(()=>{this.props.emitter.trigger("_resize",!0),this.props.emitter.trigger("windowResize",{view:this.props.viewApi})}),this.handleWindowResize=e=>{let{options:t}=this.props;t.handleWindowResize&&e.target===window&&this.resizeRunner.request(t.windowResizeDelay)}}render(){let e,{props:t}=this,{toolbarConfig:n,options:r}=t,i=!1,s="";t.isHeightAuto||t.forPrint?s="":null!=r.height?i=!0:null!=r.contentHeight?s=r.contentHeight:e=Math.max(r.aspectRatio,.5);let o=this.buildViewContext(t.viewSpec,t.viewApi,t.options,t.dateProfileGenerator,t.dateEnv,t.nowManager,t.theme,t.pluginHooks,t.dispatch,t.getCurrentData,t.emitter,t.calendarApi,this.registerInteractiveComponent,this.unregisterInteractiveComponent),a=n.header&&n.header.hasTitle?this.state.viewLabelId:void 0;return p(Ln.Provider,{value:o},p(Ii,{unit:"day"},r=>{let o=this.buildToolbarProps(t.viewSpec,t.dateProfile,t.dateProfileGenerator,t.currentDate,r,t.viewTitle);return p(y,null,n.header&&p(Da,Object.assign({ref:this.headerRef,extraClassName:"fc-header-toolbar",model:n.header,titleId:a},o)),p(wa,{liquid:i,height:s,aspectRatio:e,labeledById:a},this.renderView(t),this.buildAppendContent()),n.footer&&p(Da,Object.assign({ref:this.footerRef,extraClassName:"fc-footer-toolbar",model:n.footer,titleId:""},o)))}))}componentDidMount(){let{props:e}=this;this.calendarInteractions=e.pluginHooks.calendarInteractions.map(t=>new t(e)),window.addEventListener("resize",this.handleWindowResize);let{propSetHandlers:t}=e.pluginHooks;for(let n in t)t[n](e[n],e)}componentDidUpdate(e){let{props:t}=this,{propSetHandlers:n}=t.pluginHooks;for(let r in n)t[r]!==e[r]&&n[r](t[r],t)}componentWillUnmount(){window.removeEventListener("resize",this.handleWindowResize),this.resizeRunner.clear();for(let e of this.calendarInteractions)e.destroy();this.props.emitter.trigger("_unmount")}buildAppendContent(){let{props:e}=this;return p(y,{},...e.pluginHooks.viewContainerAppends.map(t=>t(e)))}renderView(e){let{pluginHooks:t}=e,{viewSpec:n}=e,r={dateProfile:e.dateProfile,businessHours:e.businessHours,eventStore:e.renderableEventStore,eventUiBases:e.eventUiBases,dateSelection:e.dateSelection,eventSelection:e.eventSelection,eventDrag:e.eventDrag,eventResize:e.eventResize,isHeightAuto:e.isHeightAuto,forPrint:e.forPrint},i=this.buildViewPropTransformers(t.viewPropsTransformers);for(let t of i)Object.assign(r,t.transform(r,e));return p(n.component,Object.assign({},r))}}function _a(e,t,n,r,i,s){let o=n.build(i,void 0,!1),a=n.buildPrev(t,r,!1),l=n.buildNext(t,r,!1);return{title:s,activeButton:e.type,navUnit:e.singleUnit,isTodayEnabled:o.isValid&&!ar(t.currentRange,i),isPrevEnabled:a.isValid,isNextEnabled:l.isValid}}function Ta(e){return e.map(e=>new e)}function ka(e){let t=Mo(e.locale||"en",ko([]).map);return new Pn(Object.assign(Object.assign({timeZone:cn.timeZone,calendarSystem:"gregory"},e),{locale:t}))}vs.touchMouseIgnoreWait=500;let Ma=0,Ia=0,Oa=!1;class Na{constructor(e){this.subjectEl=null,this.selector="",this.handleSelector="",this.shouldIgnoreMove=!1,this.shouldWatchScroll=!0,this.isDragging=!1,this.isTouchDragging=!1,this.wasTouchScroll=!1,this.handleMouseDown=e=>{if(!this.shouldIgnoreMouse()&&function(e){return 0===e.button&&!e.ctrlKey}(e)&&this.tryStart(e)){let t=this.createEventFromMouse(e,!0);this.emitter.trigger("pointerdown",t),this.initScrollWatch(t),this.shouldIgnoreMove||document.addEventListener("mousemove",this.handleMouseMove),document.addEventListener("mouseup",this.handleMouseUp)}},this.handleMouseMove=e=>{let t=this.createEventFromMouse(e);this.recordCoords(t),this.emitter.trigger("pointermove",t)},this.handleMouseUp=e=>{document.removeEventListener("mousemove",this.handleMouseMove),document.removeEventListener("mouseup",this.handleMouseUp),this.emitter.trigger("pointerup",this.createEventFromMouse(e)),this.cleanup()},this.handleTouchStart=e=>{if(this.tryStart(e)){this.isTouchDragging=!0;let t=this.createEventFromTouch(e,!0);this.emitter.trigger("pointerdown",t),this.initScrollWatch(t);let n=e.target;this.shouldIgnoreMove||n.addEventListener("touchmove",this.handleTouchMove),n.addEventListener("touchend",this.handleTouchEnd),n.addEventListener("touchcancel",this.handleTouchEnd),window.addEventListener("scroll",this.handleTouchScroll,!0)}},this.handleTouchMove=e=>{let t=this.createEventFromTouch(e);this.recordCoords(t),this.emitter.trigger("pointermove",t)},this.handleTouchEnd=e=>{if(this.isDragging){let t=e.target;t.removeEventListener("touchmove",this.handleTouchMove),t.removeEventListener("touchend",this.handleTouchEnd),t.removeEventListener("touchcancel",this.handleTouchEnd),window.removeEventListener("scroll",this.handleTouchScroll,!0),this.emitter.trigger("pointerup",this.createEventFromTouch(e)),this.cleanup(),this.isTouchDragging=!1,Ma+=1,setTimeout(()=>{Ma-=1},vs.touchMouseIgnoreWait)}},this.handleTouchScroll=()=>{this.wasTouchScroll=!0},this.handleScroll=e=>{if(!this.shouldIgnoreMove){let t=window.scrollX-this.prevScrollX+this.prevPageX,n=window.scrollY-this.prevScrollY+this.prevPageY;this.emitter.trigger("pointermove",{origEvent:e,isTouch:this.isTouchDragging,subjectEl:this.subjectEl,pageX:t,pageY:n,deltaX:t-this.origPageX,deltaY:n-this.origPageY})}},this.containerEl=e,this.emitter=new Fr,e.addEventListener("mousedown",this.handleMouseDown),e.addEventListener("touchstart",this.handleTouchStart,{passive:!0}),Ia+=1,1===Ia&&window.addEventListener("touchmove",Pa,{passive:!1})}destroy(){this.containerEl.removeEventListener("mousedown",this.handleMouseDown),this.containerEl.removeEventListener("touchstart",this.handleTouchStart,{passive:!0}),Ia-=1,Ia||window.removeEventListener("touchmove",Pa,{passive:!1})}tryStart(e){let t=this.querySubjectEl(e),n=e.target;return!(!t||this.handleSelector&&!Oe(n,this.handleSelector))&&(this.subjectEl=t,this.isDragging=!0,this.wasTouchScroll=!1,!0)}cleanup(){Oa=!1,this.isDragging=!1,this.subjectEl=null,this.destroyScrollWatch()}querySubjectEl(e){return this.selector?Oe(e.target,this.selector):this.containerEl}shouldIgnoreMouse(){return Ma||this.isTouchDragging}cancelTouchScroll(){this.isDragging&&(Oa=!0)}initScrollWatch(e){this.shouldWatchScroll&&(this.recordCoords(e),window.addEventListener("scroll",this.handleScroll,!0))}recordCoords(e){this.shouldWatchScroll&&(this.prevPageX=e.pageX,this.prevPageY=e.pageY,this.prevScrollX=window.scrollX,this.prevScrollY=window.scrollY)}destroyScrollWatch(){this.shouldWatchScroll&&window.removeEventListener("scroll",this.handleScroll,!0)}createEventFromMouse(e,t){let n=0,r=0;return t?(this.origPageX=e.pageX,this.origPageY=e.pageY):(n=e.pageX-this.origPageX,r=e.pageY-this.origPageY),{origEvent:e,isTouch:!1,subjectEl:this.subjectEl,pageX:e.pageX,pageY:e.pageY,deltaX:n,deltaY:r}}createEventFromTouch(e,t){let n,r,i=e.touches,s=0,o=0;return i&&i.length?(n=i[0].pageX,r=i[0].pageY):(n=e.pageX,r=e.pageY),t?(this.origPageX=n,this.origPageY=r):(s=n-this.origPageX,o=r-this.origPageY),{origEvent:e,isTouch:!0,subjectEl:this.subjectEl,pageX:n,pageY:r,deltaX:s,deltaY:o}}}function Pa(e){Oa&&e.preventDefault()}class Ha{constructor(){this.isVisible=!1,this.sourceEl=null,this.mirrorEl=null,this.sourceElRect=null,this.parentNode=document.body,this.zIndex=9999,this.revertDuration=0}start(e,t,n){this.sourceEl=e,this.sourceElRect=this.sourceEl.getBoundingClientRect(),this.origScreenX=t-window.scrollX,this.origScreenY=n-window.scrollY,this.deltaX=0,this.deltaY=0,this.updateElPosition()}handleMove(e,t){this.deltaX=e-window.scrollX-this.origScreenX,this.deltaY=t-window.scrollY-this.origScreenY,this.updateElPosition()}setIsVisible(e){e?this.isVisible||(this.mirrorEl&&(this.mirrorEl.style.display=""),this.isVisible=e,this.updateElPosition()):this.isVisible&&(this.mirrorEl&&(this.mirrorEl.style.display="none"),this.isVisible=e)}stop(e,t){let n=()=>{this.cleanup(),t()};e&&this.mirrorEl&&this.isVisible&&this.revertDuration&&(this.deltaX||this.deltaY)?this.doRevertAnimation(n,this.revertDuration):setTimeout(n,0)}doRevertAnimation(e,t){let n=this.mirrorEl,r=this.sourceEl.getBoundingClientRect();n.style.transition="top "+t+"ms,left "+t+"ms",Be(n,{left:r.left,top:r.top}),Ge(n,()=>{n.style.transition="",e()})}cleanup(){this.mirrorEl&&(Ie(this.mirrorEl),this.mirrorEl=null),this.sourceEl=null}updateElPosition(){this.sourceEl&&this.isVisible&&Be(this.getMirrorEl(),{left:this.sourceElRect.left+this.deltaX,top:this.sourceElRect.top+this.deltaY})}getMirrorEl(){let e=this.sourceElRect,t=this.mirrorEl;return t||(t=this.mirrorEl=this.sourceEl.cloneNode(!0),t.style.userSelect="none",t.style.webkitUserSelect="none",t.style.pointerEvents="none",t.classList.add("fc-event-dragging"),Be(t,{position:"fixed",zIndex:this.zIndex,visibility:"",boxSizing:"border-box",width:e.right-e.left,height:e.bottom-e.top,right:"auto",bottom:"auto",margin:0}),this.parentNode.appendChild(t)),t}}class Ba extends ss{constructor(e,t){super(),this.handleScroll=()=>{this.scrollTop=this.scrollController.getScrollTop(),this.scrollLeft=this.scrollController.getScrollLeft(),this.handleScrollChange()},this.scrollController=e,this.doesListening=t,this.scrollTop=this.origScrollTop=e.getScrollTop(),this.scrollLeft=this.origScrollLeft=e.getScrollLeft(),this.scrollWidth=e.getScrollWidth(),this.scrollHeight=e.getScrollHeight(),this.clientWidth=e.getClientWidth(),this.clientHeight=e.getClientHeight(),this.clientRect=this.computeClientRect(),this.doesListening&&this.getEventTarget().addEventListener("scroll",this.handleScroll)}destroy(){this.doesListening&&this.getEventTarget().removeEventListener("scroll",this.handleScroll)}getScrollTop(){return this.scrollTop}getScrollLeft(){return this.scrollLeft}setScrollTop(e){this.scrollController.setScrollTop(e),this.doesListening||(this.scrollTop=Math.max(Math.min(e,this.getMaxScrollTop()),0),this.handleScrollChange())}setScrollLeft(e){this.scrollController.setScrollLeft(e),this.doesListening||(this.scrollLeft=Math.max(Math.min(e,this.getMaxScrollLeft()),0),this.handleScrollChange())}getClientWidth(){return this.clientWidth}getClientHeight(){return this.clientHeight}getScrollWidth(){return this.scrollWidth}getScrollHeight(){return this.scrollHeight}handleScrollChange(){}}class ja extends Ba{constructor(e,t){super(new os(e),t)}getEventTarget(){return this.scrollController.el}computeClientRect(){return es(this.scrollController.el)}}class za extends Ba{constructor(e){super(new as,e)}getEventTarget(){return window}computeClientRect(){return{left:this.scrollLeft,right:this.scrollLeft+this.clientWidth,top:this.scrollTop,bottom:this.scrollTop+this.clientHeight}}handleScrollChange(){this.clientRect=this.computeClientRect()}}const Ua="function"==typeof performance?performance.now:Date.now;class La{constructor(){this.isEnabled=!0,this.scrollQuery=[window,".fc-scroller"],this.edgeThreshold=50,this.maxVelocity=300,this.pointerScreenX=null,this.pointerScreenY=null,this.isAnimating=!1,this.scrollCaches=null,this.everMovedUp=!1,this.everMovedDown=!1,this.everMovedLeft=!1,this.everMovedRight=!1,this.animate=()=>{if(this.isAnimating){let e=this.computeBestEdge(this.pointerScreenX+window.scrollX,this.pointerScreenY+window.scrollY);if(e){let t=Ua();this.handleSide(e,(t-this.msSinceRequest)/1e3),this.requestAnimation(t)}else this.isAnimating=!1}}}start(e,t,n){this.isEnabled&&(this.scrollCaches=this.buildCaches(n),this.pointerScreenX=null,this.pointerScreenY=null,this.everMovedUp=!1,this.everMovedDown=!1,this.everMovedLeft=!1,this.everMovedRight=!1,this.handleMove(e,t))}handleMove(e,t){if(this.isEnabled){let n=e-window.scrollX,r=t-window.scrollY,i=null===this.pointerScreenY?0:r-this.pointerScreenY,s=null===this.pointerScreenX?0:n-this.pointerScreenX;i<0?this.everMovedUp=!0:i>0&&(this.everMovedDown=!0),s<0?this.everMovedLeft=!0:s>0&&(this.everMovedRight=!0),this.pointerScreenX=n,this.pointerScreenY=r,this.isAnimating||(this.isAnimating=!0,this.requestAnimation(Ua()))}}stop(){if(this.isEnabled){this.isAnimating=!1;for(let e of this.scrollCaches)e.destroy();this.scrollCaches=null}}requestAnimation(e){this.msSinceRequest=e,requestAnimationFrame(this.animate)}handleSide(e,t){let{scrollCache:n}=e,{edgeThreshold:r}=this,i=r-e.distance,s=i*i/(r*r)*this.maxVelocity*t,o=1;switch(e.name){case"left":o=-1;case"right":n.setScrollLeft(n.getScrollLeft()+s*o);break;case"top":o=-1;case"bottom":n.setScrollTop(n.getScrollTop()+s*o)}}computeBestEdge(e,t){let{edgeThreshold:n}=this,r=null,i=this.scrollCaches||[];for(let s of i){let i=s.clientRect,o=e-i.left,a=i.right-e,l=t-i.top,c=i.bottom-t;o>=0&&a>=0&&l>=0&&c>=0&&(l<=n&&this.everMovedUp&&s.canScrollUp()&&(!r||r.distance>l)&&(r={scrollCache:s,name:"top",distance:l}),c<=n&&this.everMovedDown&&s.canScrollDown()&&(!r||r.distance>c)&&(r={scrollCache:s,name:"bottom",distance:c}),o<=n&&this.everMovedLeft&&s.canScrollLeft()&&(!r||r.distance>o)&&(r={scrollCache:s,name:"left",distance:o}),a<=n&&this.everMovedRight&&s.canScrollRight()&&(!r||r.distance>a)&&(r={scrollCache:s,name:"right",distance:a}))}return r}buildCaches(e){return this.queryScrollEls(e).map(e=>e===window?new za(!1):new ja(e,!1))}queryScrollEls(e){let t=[];for(let n of this.scrollQuery)"object"==typeof n?t.push(n):t.push(...Array.prototype.slice.call(e.getRootNode().querySelectorAll(n)));return t}}class Wa extends ms{constructor(e,t){super(e),this.containerEl=e,this.delay=null,this.minDistance=0,this.touchScrollAllowed=!0,this.mirrorNeedsRevert=!1,this.isInteracting=!1,this.isDragging=!1,this.isDelayEnded=!1,this.isDistanceSurpassed=!1,this.delayTimeoutId=null,this.onPointerDown=e=>{this.isDragging||(this.isInteracting=!0,this.isDelayEnded=!1,this.isDistanceSurpassed=!1,Je(document.body),et(document.body),e.isTouch||e.origEvent.preventDefault(),this.emitter.trigger("pointerdown",e),this.isInteracting&&!this.pointer.shouldIgnoreMove&&(this.mirror.setIsVisible(!1),this.mirror.start(e.subjectEl,e.pageX,e.pageY),this.startDelay(e),this.minDistance||this.handleDistanceSurpassed(e)))},this.onPointerMove=e=>{if(this.isInteracting){if(this.emitter.trigger("pointermove",e),!this.isDistanceSurpassed){let t,n=this.minDistance,{deltaX:r,deltaY:i}=e;t=r*r+i*i,t>=n*n&&this.handleDistanceSurpassed(e)}this.isDragging&&("scroll"!==e.origEvent.type&&(this.mirror.handleMove(e.pageX,e.pageY),this.autoScroller.handleMove(e.pageX,e.pageY)),this.emitter.trigger("dragmove",e))}},this.onPointerUp=e=>{this.isInteracting&&(this.isInteracting=!1,Ke(document.body),tt(document.body),this.emitter.trigger("pointerup",e),this.isDragging&&(this.autoScroller.stop(),this.tryStopDrag(e)),this.delayTimeoutId&&(clearTimeout(this.delayTimeoutId),this.delayTimeoutId=null))};let n=this.pointer=new Na(e);n.emitter.on("pointerdown",this.onPointerDown),n.emitter.on("pointermove",this.onPointerMove),n.emitter.on("pointerup",this.onPointerUp),t&&(n.selector=t),this.mirror=new Ha,this.autoScroller=new La}destroy(){this.pointer.destroy(),this.onPointerUp({})}startDelay(e){"number"==typeof this.delay?this.delayTimeoutId=setTimeout(()=>{this.delayTimeoutId=null,this.handleDelayEnd(e)},this.delay):this.handleDelayEnd(e)}handleDelayEnd(e){this.isDelayEnded=!0,this.tryStartDrag(e)}handleDistanceSurpassed(e){this.isDistanceSurpassed=!0,this.tryStartDrag(e)}tryStartDrag(e){this.isDelayEnded&&this.isDistanceSurpassed&&(this.pointer.wasTouchScroll&&!this.touchScrollAllowed||(this.isDragging=!0,this.mirrorNeedsRevert=!1,this.autoScroller.start(e.pageX,e.pageY,this.containerEl),this.emitter.trigger("dragstart",e),!1===this.touchScrollAllowed&&this.pointer.cancelTouchScroll()))}tryStopDrag(e){this.mirror.stop(this.mirrorNeedsRevert,this.stopDrag.bind(this,e))}stopDrag(e){this.isDragging=!1,this.emitter.trigger("dragend",e)}setIgnoreMove(e){this.pointer.shouldIgnoreMove=e}setMirrorIsVisible(e){this.mirror.setIsVisible(e)}setMirrorNeedsRevert(e){this.mirrorNeedsRevert=e}setAutoScrollEnabled(e){this.autoScroller.isEnabled=e}}class Fa{constructor(e){this.el=e,this.origRect=ts(e),this.scrollCaches=ns(e).map(e=>new ja(e,!0))}destroy(){for(let e of this.scrollCaches)e.destroy()}computeLeft(){let e=this.origRect.left;for(let t of this.scrollCaches)e+=t.origScrollLeft-t.getScrollLeft();return e}computeTop(){let e=this.origRect.top;for(let t of this.scrollCaches)e+=t.origScrollTop-t.getScrollTop();return e}isWithinClipping(e,t){let n={left:e,top:t};for(let e of this.scrollCaches)if(!Va(e.getEventTarget())&&!Pi(n,e.clientRect))return!1;return!0}}function Va(e){let t=e.tagName;return"HTML"===t||"BODY"===t}class Ga{constructor(e,t){this.useSubjectCenter=!1,this.requireInitial=!0,this.disablePointCheck=!1,this.initialHit=null,this.movingHit=null,this.finalHit=null,this.handlePointerDown=e=>{let{dragging:t}=this;this.initialHit=null,this.movingHit=null,this.finalHit=null,this.prepareHits(),this.processFirstCoord(e),this.initialHit||!this.requireInitial?(t.setIgnoreMove(!1),this.emitter.trigger("pointerdown",e)):t.setIgnoreMove(!0)},this.handleDragStart=e=>{this.emitter.trigger("dragstart",e),this.handleMove(e,!0)},this.handleDragMove=e=>{this.emitter.trigger("dragmove",e),this.handleMove(e)},this.handlePointerUp=e=>{this.releaseHits(),this.emitter.trigger("pointerup",e)},this.handleDragEnd=e=>{this.movingHit&&this.emitter.trigger("hitupdate",null,!0,e),this.finalHit=this.movingHit,this.movingHit=null,this.emitter.trigger("dragend",e)},this.droppableStore=t,e.emitter.on("pointerdown",this.handlePointerDown),e.emitter.on("dragstart",this.handleDragStart),e.emitter.on("dragmove",this.handleDragMove),e.emitter.on("pointerup",this.handlePointerUp),e.emitter.on("dragend",this.handleDragEnd),this.dragging=e,this.emitter=new Fr}processFirstCoord(e){let t,n={left:e.pageX,top:e.pageY},r=n,i=e.subjectEl;i instanceof HTMLElement&&(t=ts(i),r=Bi(r,t));let s=this.initialHit=this.queryHitForOffset(r.left,r.top);if(s){if(this.useSubjectCenter&&t){let e=Hi(t,s.rect);e&&(r=ji(e))}this.coordAdjust=zi(r,n)}else this.coordAdjust={left:0,top:0}}handleMove(e,t){let n=this.queryHitForOffset(e.pageX+this.coordAdjust.left,e.pageY+this.coordAdjust.top);!t&&Qa(this.movingHit,n)||(this.movingHit=n,this.emitter.trigger("hitupdate",n,!1,e))}prepareHits(){this.offsetTrackers=An(this.droppableStore,e=>(e.component.prepareHits(),new Fa(e.el)))}releaseHits(){let{offsetTrackers:e}=this;for(let t in e)e[t].destroy();this.offsetTrackers={}}queryHitForOffset(e,t){let{droppableStore:n,offsetTrackers:r}=this,i=null;for(let s in n){let o=n[s].component,a=r[s];if(a&&a.isWithinClipping(e,t)){let n=a.computeLeft(),r=a.computeTop(),l=e-n,c=t-r,{origRect:d}=a,u=d.right-d.left,h=d.bottom-d.top;if(l>=0&&l=0&&ci.layer)&&(e.componentId=s,e.context=o.context,e.rect.left+=n,e.rect.right+=n,e.rect.top+=r,e.rect.bottom+=r,i=e)}}}return i}}function Qa(e,t){return!e&&!t||Boolean(e)===Boolean(t)&&Ei(e.dateSpan,t.dateSpan)}function qa(e,t){let n={};for(let r of t.pluginHooks.datePointTransforms)Object.assign(n,r(e,t));var r,i;return Object.assign(n,(r=e,{date:(i=t.dateEnv).toDate(r.range.start),dateStr:i.formatIso(r.range.start,{omitTime:r.allDay}),allDay:r.allDay})),n}class Ya extends Ti{constructor(e){super(e),this.subjectEl=null,this.subjectSeg=null,this.isDragging=!1,this.eventRange=null,this.relevantEvents=null,this.receivingContext=null,this.validMutation=null,this.mutatedRelevantEvents=null,this.handlePointerDown=e=>{let t=e.origEvent.target,{component:n,dragging:r}=this,{mirror:i}=r,{options:s}=n.context,o=n.context;this.subjectEl=e.subjectEl;let a=this.subjectSeg=si(e.subjectEl),l=(this.eventRange=a.eventRange).instance.instanceId;this.relevantEvents=Cr(o.getCurrentData().eventStore,l),r.minDistance=e.isTouch?0:s.eventDragMinDistance,r.delay=e.isTouch&&l!==n.props.eventSelection?function(e){let{options:t}=e.context,n=t.eventLongPressDelay;null==n&&(n=t.longPressDelay);return n}(n):null,s.fixedMirrorParent?i.parentNode=s.fixedMirrorParent:i.parentNode=Oe(t,".fc"),i.revertDuration=s.dragRevertDuration;let c=n.isValidSegDownEl(t)&&!Oe(t,".fc-event-resizer");r.setIgnoreMove(!c),this.isDragging=c&&e.subjectEl.classList.contains("fc-event-draggable")},this.handleDragStart=e=>{let t=this.component.context,n=this.eventRange,r=n.instance.instanceId;e.isTouch?r!==this.component.props.eventSelection&&t.dispatch({type:"SELECT_EVENT",eventInstanceId:r}):t.dispatch({type:"UNSELECT_EVENT"}),this.isDragging&&(t.calendarApi.unselect(e),t.emitter.trigger("eventDragStart",{el:this.subjectEl,event:new Kr(t,n.def,n.instance),jsEvent:e.origEvent,view:t.viewApi}))},this.handleHitUpdate=(e,t)=>{if(!this.isDragging)return;let n=this.relevantEvents,r=this.hitDragging.initialHit,i=this.component.context,s=null,o=null,a=null,l=!1,c={affectedEvents:n,mutatedEvents:{defs:{},instances:{}},isEvent:!0};if(e){s=e.context;let t=s.options;i===s||t.editable&&t.droppable?(o=function(e,t,n,r){let i=e.dateSpan,s=t.dateSpan,o=i.range.start,a=s.range.start,l={};i.allDay!==s.allDay&&(l.allDay=s.allDay,l.hasEnd=t.context.options.allDayMaintainDuration,o=s.allDay?Mt(n):n);let c=ur(o,a,e.context.dateEnv,e.componentId===t.componentId?e.largeUnit:null);c.milliseconds&&(l.allDay=!1);let d={datesDelta:c,standardProps:l};for(let n of r)n(d,e,t);return d}(r,e,this.eventRange.instance.range.start,s.getCurrentData().pluginHooks.eventDragMutationMassagers),o&&(a=Zr(n,s.getCurrentData().eventUiBases,o,s),c.mutatedEvents=a,Is(c,e.dateProfile,s)||(l=!0,o=null,a=null,c.mutatedEvents={defs:{},instances:{}}))):s=null}this.displayDrag(s,c),l?Xe():$e(),t||(i===s&&Qa(r,e)&&(o=null),this.dragging.setMirrorNeedsRevert(!o),this.dragging.setMirrorIsVisible(!e||!this.subjectEl.getRootNode().querySelector(".fc-event-mirror")),this.receivingContext=s,this.validMutation=o,this.mutatedRelevantEvents=a)},this.handlePointerUp=()=>{this.isDragging||this.cleanup()},this.handleDragEnd=e=>{if(this.isDragging){let t=this.component.context,n=t.viewApi,{receivingContext:r,validMutation:i}=this,s=this.eventRange.def,o=this.eventRange.instance,a=new Kr(t,s,o),l=this.relevantEvents,c=this.mutatedRelevantEvents,{finalHit:d}=this.hitDragging;if(this.clearDrag(),t.emitter.trigger("eventDragStop",{el:this.subjectEl,event:a,jsEvent:e.origEvent,view:n}),i){if(r===t){let r=new Kr(t,c.defs[s.defId],o?c.instances[o.instanceId]:null);t.dispatch({type:"MERGE_EVENTS",eventStore:c});let d={oldEvent:a,event:r,relatedEvents:ti(c,t,o),revert(){t.dispatch({type:"MERGE_EVENTS",eventStore:l})}},u={};for(let e of t.getCurrentData().pluginHooks.eventDropTransformers)Object.assign(u,e(i,t));t.emitter.trigger("eventDrop",Object.assign(Object.assign(Object.assign({},d),u),{el:e.subjectEl,delta:i.datesDelta,jsEvent:e.origEvent,view:n})),t.emitter.trigger("eventChange",d)}else if(r){let i={event:a,relatedEvents:ti(l,t,o),revert(){t.dispatch({type:"MERGE_EVENTS",eventStore:l})}};t.emitter.trigger("eventLeave",Object.assign(Object.assign({},i),{draggedEl:e.subjectEl,view:n})),t.dispatch({type:"REMOVE_EVENTS",eventStore:l}),t.emitter.trigger("eventRemove",i);let u=c.defs[s.defId],h=c.instances[o.instanceId],f=new Kr(r,u,h);r.dispatch({type:"MERGE_EVENTS",eventStore:c});let g={event:f,relatedEvents:ti(c,r,h),revert(){r.dispatch({type:"REMOVE_EVENTS",eventStore:c})}};r.emitter.trigger("eventAdd",g),e.isTouch&&r.dispatch({type:"SELECT_EVENT",eventInstanceId:o.instanceId}),r.emitter.trigger("drop",Object.assign(Object.assign({},qa(d.dateSpan,r)),{draggedEl:e.subjectEl,jsEvent:e.origEvent,view:d.context.viewApi})),r.emitter.trigger("eventReceive",Object.assign(Object.assign({},g),{draggedEl:e.subjectEl,view:d.context.viewApi}))}}else t.emitter.trigger("_noEventDrop")}this.cleanup()};let{component:t}=this,{options:n}=t.context,r=this.dragging=new Wa(e.el);r.pointer.selector=Ya.SELECTOR,r.touchScrollAllowed=!1,r.autoScroller.isEnabled=n.dragScroll;let i=this.hitDragging=new Ga(this.dragging,Mi);i.useSubjectCenter=e.useEventCenter,i.emitter.on("pointerdown",this.handlePointerDown),i.emitter.on("dragstart",this.handleDragStart),i.emitter.on("hitupdate",this.handleHitUpdate),i.emitter.on("pointerup",this.handlePointerUp),i.emitter.on("dragend",this.handleDragEnd)}destroy(){this.dragging.destroy()}displayDrag(e,t){let n=this.component.context,r=this.receivingContext;r&&r!==e&&(r===n?r.dispatch({type:"SET_EVENT_DRAG",state:{affectedEvents:t.affectedEvents,mutatedEvents:{defs:{},instances:{}},isEvent:!0}}):r.dispatch({type:"UNSET_EVENT_DRAG"})),e&&e.dispatch({type:"SET_EVENT_DRAG",state:t})}clearDrag(){let e=this.component.context,{receivingContext:t}=this;t&&t.dispatch({type:"UNSET_EVENT_DRAG"}),e!==t&&e.dispatch({type:"UNSET_EVENT_DRAG"})}cleanup(){this.subjectSeg=null,this.isDragging=!1,this.eventRange=null,this.relevantEvents=null,this.receivingContext=null,this.validMutation=null,this.mutatedRelevantEvents=null}}Ya.SELECTOR=".fc-event-draggable, .fc-event-resizable";const Za={fixedMirrorParent:yn},Xa={dateClick:yn,eventDragStart:yn,eventDragStop:yn,eventDrop:yn,eventResizeStart:yn,eventResizeStop:yn,eventResize:yn,drop:yn,eventReceive:yn,eventLeave:yn};class $a{constructor(e,t){this.receivingContext=null,this.droppableEvent=null,this.suppliedDragMeta=null,this.dragMeta=null,this.handleDragStart=e=>{this.dragMeta=this.buildDragMeta(e.subjectEl)},this.handleHitUpdate=(e,t,n)=>{let{dragging:r}=this.hitDragging,i=null,s=null,o=!1,a={affectedEvents:{defs:{},instances:{}},mutatedEvents:{defs:{},instances:{}},isEvent:this.dragMeta.create};e&&(i=e.context,this.canDropElOnCalendar(n.subjectEl,i)&&(s=function(e,t,n){let r=Object.assign({},t.leftoverProps);for(let i of n.pluginHooks.externalDefTransforms)Object.assign(r,i(e,t));let{refined:i,extra:s}=Er(r,n),o=Ar(i,s,t.sourceId,e.allDay,n.options.forceEventDuration||Boolean(t.duration),n),a=e.range.start;e.allDay&&t.startTime&&(a=n.dateEnv.add(a,t.startTime));let l=t.duration?n.dateEnv.add(a,t.duration):Yr(e.allDay,a,n),c=fr(o.defId,{start:a,end:l});return{def:o,instance:c}}(e.dateSpan,this.dragMeta,i),a.mutatedEvents=wr(s),o=!Is(a,e.dateProfile,i),o&&(a.mutatedEvents={defs:{},instances:{}},s=null))),this.displayDrag(i,a),r.setMirrorIsVisible(t||!s||!document.querySelector(".fc-event-mirror")),o?Xe():$e(),t||(r.setMirrorNeedsRevert(!s),this.receivingContext=i,this.droppableEvent=s)},this.handleDragEnd=e=>{let{receivingContext:t,droppableEvent:n}=this;if(this.clearDrag(),t&&n){let r=this.hitDragging.finalHit,i=r.context.viewApi,s=this.dragMeta;if(t.emitter.trigger("drop",Object.assign(Object.assign({},qa(r.dateSpan,t)),{draggedEl:e.subjectEl,jsEvent:e.origEvent,view:i})),s.create){let r=wr(n);t.dispatch({type:"MERGE_EVENTS",eventStore:r}),e.isTouch&&t.dispatch({type:"SELECT_EVENT",eventInstanceId:n.instance.instanceId}),t.emitter.trigger("eventReceive",{event:new Kr(t,n.def,n.instance),relatedEvents:[],revert(){t.dispatch({type:"REMOVE_EVENTS",eventStore:r})},draggedEl:e.subjectEl,view:i})}}this.receivingContext=null,this.droppableEvent=null};let n=this.hitDragging=new Ga(e,Mi);n.requireInitial=!1,n.emitter.on("dragstart",this.handleDragStart),n.emitter.on("hitupdate",this.handleHitUpdate),n.emitter.on("dragend",this.handleDragEnd),this.suppliedDragMeta=t}buildDragMeta(e){return"object"==typeof this.suppliedDragMeta?bs(this.suppliedDragMeta):"function"==typeof this.suppliedDragMeta?bs(this.suppliedDragMeta(e)):function(e){let t=function(e,t){let n=vs.dataAttrPrefix,r=(n?n+"-":"")+t;return e.getAttribute("data-"+r)||""}(e,"event");return bs(t?JSON.parse(t):{create:!1})}(e)}displayDrag(e,t){let n=this.receivingContext;n&&n!==e&&n.dispatch({type:"UNSET_EVENT_DRAG"}),e&&e.dispatch({type:"SET_EVENT_DRAG",state:t})}clearDrag(){this.receivingContext&&this.receivingContext.dispatch({type:"UNSET_EVENT_DRAG"})}canDropElOnCalendar(e,t){let n=t.options.dropAccept;return"function"==typeof n?n.call(t.calendarApi,e):"string"!=typeof n||!n||Boolean(Ne(e,n))}}vs.dataAttrPrefix="";class Ja extends ms{constructor(e){super(e),this.shouldIgnoreMove=!1,this.mirrorSelector="",this.currentMirrorEl=null,this.handlePointerDown=e=>{this.emitter.trigger("pointerdown",e),this.shouldIgnoreMove||this.emitter.trigger("dragstart",e)},this.handlePointerMove=e=>{this.shouldIgnoreMove||this.emitter.trigger("dragmove",e)},this.handlePointerUp=e=>{this.emitter.trigger("pointerup",e),this.shouldIgnoreMove||this.emitter.trigger("dragend",e)};let t=this.pointer=new Na(e);t.emitter.on("pointerdown",this.handlePointerDown),t.emitter.on("pointermove",this.handlePointerMove),t.emitter.on("pointerup",this.handlePointerUp)}destroy(){this.pointer.destroy()}setIgnoreMove(e){this.shouldIgnoreMove=e}setMirrorIsVisible(e){if(e)this.currentMirrorEl&&(this.currentMirrorEl.style.visibility="",this.currentMirrorEl=null);else{let e=this.mirrorSelector?document.querySelector(this.mirrorSelector):null;e&&(this.currentMirrorEl=e,e.style.visibility="hidden")}}}var Ka=Oo({name:"@fullcalendar/interaction",componentInteractions:[class extends Ti{constructor(e){super(e),this.handlePointerDown=e=>{let{dragging:t}=this,n=e.origEvent.target;t.setIgnoreMove(!this.component.isValidDateDownEl(n))},this.handleDragEnd=e=>{let{component:t}=this,{pointer:n}=this.dragging;if(!n.wasTouchScroll){let{initialHit:n,finalHit:r}=this.hitDragging;if(n&&r&&Qa(n,r)){let{context:r}=t,i=Object.assign(Object.assign({},qa(n.dateSpan,r)),{dayEl:n.dayEl,jsEvent:e.origEvent,view:r.viewApi||r.calendarApi.view});r.emitter.trigger("dateClick",i)}}},this.dragging=new Wa(e.el),this.dragging.autoScroller.isEnabled=!1;let t=this.hitDragging=new Ga(this.dragging,ki(e));t.emitter.on("pointerdown",this.handlePointerDown),t.emitter.on("dragend",this.handleDragEnd)}destroy(){this.dragging.destroy()}},class extends Ti{constructor(e){super(e),this.dragSelection=null,this.handlePointerDown=e=>{let{component:t,dragging:n}=this,{options:r}=t.context,i=r.selectable&&t.isValidDateDownEl(e.origEvent.target);n.setIgnoreMove(!i),n.delay=e.isTouch?function(e){let{options:t}=e.context,n=t.selectLongPressDelay;null==n&&(n=t.longPressDelay);return n}(t):null},this.handleDragStart=e=>{this.component.context.calendarApi.unselect(e)},this.handleHitUpdate=(e,t)=>{let{context:n}=this.component,r=null,i=!1;if(e){let t=this.hitDragging.initialHit;e.componentId===t.componentId&&this.isHitComboAllowed&&!this.isHitComboAllowed(t,e)||(r=function(e,t,n){let r=e.dateSpan,i=t.dateSpan,s=[r.range.start,r.range.end,i.range.start,i.range.end];s.sort(lt);let o={};for(let r of n){let n=r(e,t);if(!1===n)return null;n&&Object.assign(o,n)}return o.range={start:s[0],end:s[3]},o.allDay=r.allDay,o}(t,e,n.pluginHooks.dateSelectionTransformers)),r&&Os(r,e.dateProfile,n)||(i=!0,r=null)}r?n.dispatch({type:"SELECT_DATES",selection:r}):t||n.dispatch({type:"UNSELECT_DATES"}),i?Xe():$e(),t||(this.dragSelection=r)},this.handlePointerUp=e=>{this.dragSelection&&(Qr(this.dragSelection,e,this.component.context),this.dragSelection=null)};let{component:t}=e,{options:n}=t.context,r=this.dragging=new Wa(e.el);r.touchScrollAllowed=!1,r.minDistance=n.selectMinDistance||0,r.autoScroller.isEnabled=n.dragScroll;let i=this.hitDragging=new Ga(this.dragging,ki(e));i.emitter.on("pointerdown",this.handlePointerDown),i.emitter.on("dragstart",this.handleDragStart),i.emitter.on("hitupdate",this.handleHitUpdate),i.emitter.on("pointerup",this.handlePointerUp)}destroy(){this.dragging.destroy()}},Ya,class extends Ti{constructor(e){super(e),this.draggingSegEl=null,this.draggingSeg=null,this.eventRange=null,this.relevantEvents=null,this.validMutation=null,this.mutatedRelevantEvents=null,this.handlePointerDown=e=>{let{component:t}=this,n=si(this.querySegEl(e)),r=this.eventRange=n.eventRange;this.dragging.minDistance=t.context.options.eventDragMinDistance,this.dragging.setIgnoreMove(!this.component.isValidSegDownEl(e.origEvent.target)||e.isTouch&&this.component.props.eventSelection!==r.instance.instanceId)},this.handleDragStart=e=>{let{context:t}=this.component,n=this.eventRange;this.relevantEvents=Cr(t.getCurrentData().eventStore,this.eventRange.instance.instanceId);let r=this.querySegEl(e);this.draggingSegEl=r,this.draggingSeg=si(r),t.calendarApi.unselect(),t.emitter.trigger("eventResizeStart",{el:r,event:new Kr(t,n.def,n.instance),jsEvent:e.origEvent,view:t.viewApi})},this.handleHitUpdate=(e,t,n)=>{let{context:r}=this.component,i=this.relevantEvents,s=this.hitDragging.initialHit,o=this.eventRange.instance,a=null,l=null,c=!1,d={affectedEvents:i,mutatedEvents:{defs:{},instances:{}},isEvent:!0};if(e){e.componentId===s.componentId&&this.isHitComboAllowed&&!this.isHitComboAllowed(s,e)||(a=function(e,t,n,r){let i=e.context.dateEnv,s=e.dateSpan.range.start,o=t.dateSpan.range.start,a=ur(s,o,i,e.largeUnit);if(n){if(i.add(r.start,a)r.start)return{endDelta:a};return null}(s,e,n.subjectEl.classList.contains("fc-event-resizer-start"),o.range))}a&&(l=Zr(i,r.getCurrentData().eventUiBases,a,r),d.mutatedEvents=l,Is(d,e.dateProfile,r)||(c=!0,a=null,l=null,d.mutatedEvents=null)),l?r.dispatch({type:"SET_EVENT_RESIZE",state:d}):r.dispatch({type:"UNSET_EVENT_RESIZE"}),c?Xe():$e(),t||(a&&Qa(s,e)&&(a=null),this.validMutation=a,this.mutatedRelevantEvents=l)},this.handleDragEnd=e=>{let{context:t}=this.component,n=this.eventRange.def,r=this.eventRange.instance,i=new Kr(t,n,r),s=this.relevantEvents,o=this.mutatedRelevantEvents;if(t.emitter.trigger("eventResizeStop",{el:this.draggingSegEl,event:i,jsEvent:e.origEvent,view:t.viewApi}),this.validMutation){let a=new Kr(t,o.defs[n.defId],r?o.instances[r.instanceId]:null);t.dispatch({type:"MERGE_EVENTS",eventStore:o});let l={oldEvent:i,event:a,relatedEvents:ti(o,t,r),revert(){t.dispatch({type:"MERGE_EVENTS",eventStore:s})}};t.emitter.trigger("eventResize",Object.assign(Object.assign({},l),{el:this.draggingSegEl,startDelta:this.validMutation.startDelta||ft(0),endDelta:this.validMutation.endDelta||ft(0),jsEvent:e.origEvent,view:t.viewApi})),t.emitter.trigger("eventChange",l)}else t.emitter.trigger("_noEventResize");this.draggingSeg=null,this.relevantEvents=null,this.validMutation=null};let{component:t}=e,n=this.dragging=new Wa(e.el);n.pointer.selector=".fc-event-resizer",n.touchScrollAllowed=!1,n.autoScroller.isEnabled=t.context.options.dragScroll;let r=this.hitDragging=new Ga(this.dragging,ki(e));r.emitter.on("pointerdown",this.handlePointerDown),r.emitter.on("dragstart",this.handleDragStart),r.emitter.on("hitupdate",this.handleHitUpdate),r.emitter.on("dragend",this.handleDragEnd)}destroy(){this.dragging.destroy()}querySegEl(e){return Oe(e.subjectEl,".fc-event")}}],calendarInteractions:[class{constructor(e){this.context=e,this.isRecentPointerDateSelect=!1,this.matchesCancel=!1,this.matchesEvent=!1,this.onSelect=e=>{e.jsEvent&&(this.isRecentPointerDateSelect=!0)},this.onDocumentPointerDown=e=>{let t=this.context.options.unselectCancel,n=ze(e.origEvent);this.matchesCancel=!!Oe(n,t),this.matchesEvent=!!Oe(n,Ya.SELECTOR)},this.onDocumentPointerUp=e=>{let{context:t}=this,{documentPointer:n}=this,r=t.getCurrentData();if(!n.wasTouchScroll){if(r.dateSelection&&!this.isRecentPointerDateSelect){let n=t.options.unselectAuto;!n||n&&this.matchesCancel||t.calendarApi.unselect(e)}r.eventSelection&&!this.matchesEvent&&t.dispatch({type:"UNSELECT_EVENT"})}this.isRecentPointerDateSelect=!1};let t=this.documentPointer=new Na(document);t.shouldIgnoreMove=!0,t.shouldWatchScroll=!1,t.emitter.on("pointerdown",this.onDocumentPointerDown),t.emitter.on("pointerup",this.onDocumentPointerUp),e.emitter.on("select",this.onSelect)}destroy(){this.context.emitter.off("select",this.onSelect),this.documentPointer.destroy()}}],elementDraggingImpl:Wa,optionRefiners:Za,listenerRefiners:Xa});class el extends ls{constructor(){super(...arguments),this.headerElRef={current:null}}renderSimpleLayout(e,t){let{props:n,context:r}=this,i=[],s=eo(r.options);return e&&i.push({type:"header",key:"header",isSticky:s,chunk:{elRef:this.headerElRef,tableClassName:"fc-col-header",rowContent:e}}),i.push({type:"body",key:"body",liquid:!0,chunk:{content:t}}),p(Kn,{elClasses:["fc-daygrid"],viewSpec:r.viewSpec},p(no,{liquid:!n.isHeightAuto&&!n.forPrint,collapsibleWidth:n.forPrint,cols:[],sections:i}))}renderHScrollLayout(e,t,n,r){let i=this.context.pluginHooks.scrollGridImpl;if(!i)throw new Error("No ScrollGrid implementation");let{props:s,context:o}=this,a=!s.forPrint&&eo(o.options),l=!s.forPrint&&to(o.options),c=[];return e&&c.push({type:"header",key:"header",isSticky:a,chunks:[{key:"main",elRef:this.headerElRef,tableClassName:"fc-col-header",rowContent:e}]}),c.push({type:"body",key:"body",liquid:!0,chunks:[{key:"main",content:t}]}),l&&c.push({type:"footer",key:"footer",isSticky:!0,chunks:[{key:"main",content:Ks}]}),p(Kn,{elClasses:["fc-daygrid"],viewSpec:o.viewSpec},p(i,{liquid:!s.isHeightAuto&&!s.forPrint,forPrint:s.forPrint,collapsibleWidth:s.forPrint,colGroups:[{cols:[{span:n,minWidth:r}]}],sections:c}))}}function tl(e,t){let n=[];for(let e=0;e{let n=(e.eventDrag?e.eventDrag.affectedInstances:null)||(e.eventResize?e.eventResize.affectedInstances:null)||{};return p(y,null,t.map(t=>{let r=t.eventRange.instance.instanceId;return p("div",{className:"fc-daygrid-event-harness",key:r,style:{visibility:n[r]?"hidden":""}},sl(t)?p(al,Object.assign({seg:t,isDragging:!1,isSelected:r===e.eventSelection,defaultDisplayEventEnd:!1},gi(t,e.todayRange))):p(ol,Object.assign({seg:t,isDragging:!1,isResizing:!1,isDateSelecting:!1,isSelected:r===e.eventSelection,defaultDisplayEventEnd:!1},gi(t,e.todayRange))))}))}})}}function dl(e){let t=[],n=[];for(let r of e)t.push(r.seg),r.isVisible||n.push(r.seg);return{allSegs:t,invisibleSegs:n}}const ul=an({week:"narrow"});class hl extends ls{constructor(){super(...arguments),this.rootElRef={current:null},this.state={dayNumberId:Le()},this.handleRootEl=e=>{Gn(this.rootElRef,e),Gn(this.props.elRef,e)}}render(){let{context:e,props:t,state:n,rootElRef:r}=this,{options:i,dateEnv:s}=e,{date:o,dateProfile:a}=t;const l=t.showDayNumber&&function(e,t,n){const{start:r,end:i}=t,s=Ct(i,-1),o=n.getYear(r),a=n.getMonth(r),l=n.getYear(s),c=n.getMonth(s);return!(o===l&&a===c)&&Boolean(e.valueOf()===r.valueOf()||1===n.getDay(e)&&e.valueOf()p("div",{ref:t.innerElRef,className:"fc-daygrid-day-frame fc-scrollgrid-sync-inner",style:{minHeight:t.minHeight}},t.showWeekNumber&&p(po,{elTag:"a",elClasses:["fc-daygrid-week-number"],elAttrs:qi(e,o,"week"),date:o,defaultFormat:ul}),!a.isDisabled&&(t.showDayNumber||co(i)||t.forceDayTop)?p("div",{className:"fc-daygrid-day-top"},p(s,{elTag:"a",elClasses:["fc-daygrid-day-number",l&&"fc-daygrid-month-start"],elAttrs:Object.assign(Object.assign({},qi(e,o)),{id:n.dayNumberId})})):t.showDayNumber?p("div",{className:"fc-daygrid-day-top",style:{visibility:"hidden"}},p("a",{className:"fc-daygrid-day-number"}," ")):void 0,p("div",{className:"fc-daygrid-day-events",ref:t.fgContentElRef},t.fgContent,p("div",{className:"fc-daygrid-day-bottom",style:{marginTop:t.moreMarginTop}},p(cl,{allDayDate:o,singlePlacements:t.singlePlacements,moreCnt:t.moreCnt,alignmentElRef:r,alignGridTop:!t.showDayNumber,extraDateSpan:t.extraDateSpan,dateProfile:t.dateProfile,eventSelection:t.eventSelection,eventDrag:t.eventDrag,eventResize:t.eventResize,todayRange:t.todayRange}))),p("div",{className:"fc-daygrid-day-bg"},t.bgContent)))}}function fl(e){return e.dayNumberText||p(y,null," ")}function gl(e){return e.eventRange.instance.instanceId+":"+e.firstCol}function pl(e){return gl(e)+":"+e.lastCol}function ml(e,t,n,r,i,s,o){let a=new yl(t=>{let n=e[t.index].eventRange.instance.instanceId+":"+t.span.start+":"+(t.span.end-1);return i[n]||1});a.allowReslicing=!0,a.strictOrder=r,!0===t||!0===n?(a.maxCoord=s,a.hiddenConsumes=!0):"number"==typeof t?a.maxStackCnt=t:"number"==typeof n&&(a.maxStackCnt=n,a.hiddenConsumes=!0);let l=[],c=[];for(let t=0;t1,o=r.span.start===e;d+=r.levelCoord-c,c=r.levelCoord+r.thickness,s?(d+=r.thickness,o&&u.push({seg:vl(i,r.span.start,r.span.end,n),isVisible:!0,isAbsolute:!0,absoluteTop:r.levelCoord,marginTop:0})):o&&(u.push({seg:vl(i,r.span.start,r.span.end,n),isVisible:!0,isAbsolute:!1,absoluteTop:r.levelCoord,marginTop:d}),d=0)}i.push(l),s.push(u),o.push(d)}return{singleColPlacements:i,multiColPlacements:s,leftoverMargins:o}}(u,e,o),p=[],m=[];for(let e of c){f[e.firstCol].push({seg:e,isVisible:!1,isAbsolute:!0,absoluteTop:0,marginTop:0});for(let t=e.firstCol;t<=e.lastCol;t+=1)h[t].push({seg:vl(e,t,t+1,o),isVisible:!1,isAbsolute:!1,absoluteTop:0,marginTop:0})}for(let e=0;e!this.forceHidden[us(e)];for(let e=0;e{e&&this.updateSizing(!0)}}render(){let{props:e,state:t,context:n}=this,{options:r}=n,i=e.cells.length,s=nl(e.businessHourSegs,i),o=nl(e.bgEventSegs,i),a=nl(this.getHighlightSegs(),i),l=nl(this.getMirrorSegs(),i),{singleColPlacements:c,multiColPlacements:d,moreCnts:u,moreMarginTops:h}=ml(li(e.fgEventSegs,r.eventOrder),e.dayMaxEvents,e.dayMaxEventRows,r.eventOrderStrict,t.segHeights,t.maxContentHeight,e.cells),f=e.eventDrag&&e.eventDrag.affectedInstances||e.eventResize&&e.eventResize.affectedInstances||{};return p("tr",{ref:this.rootElRef,role:"row"},e.renderIntro&&e.renderIntro(),e.cells.map((t,n)=>{let r=this.renderFgSegs(n,e.forPrint?c[n]:d[n],e.todayRange,f),i=this.renderFgSegs(n,function(e,t){if(!e.length)return[];let n=function(e){let t={};for(let n of e)for(let e of n)t[e.seg.eventRange.instance.instanceId]=e.absoluteTop;return t}(t);return e.map(e=>({seg:e,isVisible:!0,isAbsolute:!0,absoluteTop:n[e.eventRange.instance.instanceId],marginTop:0}))}(l[n],d),e.todayRange,{},Boolean(e.eventDrag),Boolean(e.eventResize),!1);return p(hl,{key:t.key,elRef:this.cellElRefs.createRef(t.key),innerElRef:this.frameElRefs.createRef(t.key),dateProfile:e.dateProfile,date:t.date,showDayNumber:e.showDayNumbers,showWeekNumber:e.showWeekNumbers&&0===n,forceDayTop:e.showWeekNumbers,todayRange:e.todayRange,eventSelection:e.eventSelection,eventDrag:e.eventDrag,eventResize:e.eventResize,extraRenderProps:t.extraRenderProps,extraDataAttrs:t.extraDataAttrs,extraClassNames:t.extraClassNames,extraDateSpan:t.extraDateSpan,moreCnt:u[n],moreMarginTop:h[n],singlePlacements:c[n],fgContentElRef:this.fgElRefs.createRef(t.key),fgContent:p(y,null,p(y,null,r),p(y,null,i)),bgContent:p(y,null,this.renderFillSegs(a[n],"highlight"),this.renderFillSegs(s[n],"non-business"),this.renderFillSegs(o[n],"bg-event")),minHeight:e.cellMinHeight})}))}componentDidMount(){this.updateSizing(!0),this.context.addResizeHandler(this.handleResize)}componentDidUpdate(e,t){let n=this.props;this.updateSizing(!Cn(e,n))}componentWillUnmount(){this.context.removeResizeHandler(this.handleResize)}getHighlightSegs(){let{props:e}=this;return e.eventDrag&&e.eventDrag.segs.length?e.eventDrag.segs:e.eventResize&&e.eventResize.segs.length?e.eventResize.segs:e.dateSelectionSegs}getMirrorSegs(){let{props:e}=this;return e.eventResize&&e.eventResize.segs.length?e.eventResize.segs:[]}renderFgSegs(e,t,n,r,i,s,o){let{context:a}=this,{eventSelection:l}=this.props,{framePositions:c}=this.state,d=1===this.props.cells.length,u=i||s||o,h=[];if(c)for(let e of t){let{seg:t}=e,{instanceId:f}=t.eventRange.instance,g=e.isVisible&&!r[f],m=e.isAbsolute,v="",y="";m&&(a.isRtl?(y=0,v=c.lefts[t.lastCol]-c.lefts[t.firstCol]):(v=0,y=c.rights[t.firstCol]-c.rights[t.lastCol])),h.push(p("div",{className:"fc-daygrid-event-harness"+(m?" fc-daygrid-event-harness-abs":""),key:gl(t),ref:u?null:this.segHarnessRefs.createRef(pl(t)),style:{visibility:g?"":"hidden",marginTop:m?"":e.marginTop,top:m?e.absoluteTop:"",left:v,right:y}},sl(t)?p(al,Object.assign({seg:t,isDragging:i,isSelected:f===l,defaultDisplayEventEnd:d},gi(t,n))):p(ol,Object.assign({seg:t,isDragging:i,isResizing:s,isDateSelecting:o,isSelected:f===l,defaultDisplayEventEnd:d},gi(t,n)))))}return h}renderFillSegs(e,t){let{isRtl:n}=this.context,{todayRange:r}=this.props,{framePositions:i}=this.state,s=[];if(i)for(let o of e){let e=n?{right:0,left:i.lefts[o.lastCol]-i.lefts[o.firstCol]}:{left:0,right:i.rights[o.firstCol]-i.rights[o.lastCol]};s.push(p("div",{key:mi(o.eventRange),className:"fc-daygrid-bg-harness",style:e},"bg-event"===t?p(ho,Object.assign({seg:o},gi(o,r))):go(t)))}return p(y,{},...s)}updateSizing(e){let{props:t,state:n,frameElRefs:r}=this;if(!t.forPrint&&null!==t.clientWidth){if(e){let e=t.cells.map(e=>r.currentMap[e.key]);if(e.length){let t=this.rootElRef.current,r=new rs(t,e,!0,!1);n.framePositions&&n.framePositions.similarTo(r)||this.setState({framePositions:new rs(t,e,!0,!1)})}}const i=this.state.segHeights,s=this.querySegHeights(),o=!0===t.dayMaxEvents||!0===t.dayMaxEventRows;this.safeSetState({segHeights:Object.assign(Object.assign({},i),s),maxContentHeight:o?this.computeMaxContentHeight():null})}}querySegHeights(){let e=this.segHarnessRefs.currentMap,t={};for(let n in e){let r=Math.round(e[n].getBoundingClientRect().height);t[n]=Math.max(t[n]||0,r)}return t}computeMaxContentHeight(){let e=this.props.cells[0].key,t=this.cellElRefs.currentMap[e],n=this.fgElRefs.currentMap[e];return t.getBoundingClientRect().bottom-n.getBoundingClientRect().top}getCellEls(){let e=this.cellElRefs.currentMap;return this.props.cells.map(t=>e[t.key])}}bl.addStateEquality({segHeights:Cn});class El extends ls{constructor(){super(...arguments),this.splitBusinessHourSegs=Gt(tl),this.splitBgEventSegs=Gt(Sl),this.splitFgEventSegs=Gt(tl),this.splitDateSelectionSegs=Gt(tl),this.splitEventDrag=Gt(rl),this.splitEventResize=Gt(rl),this.rowRefs=new Ws}render(){let{props:e,context:t}=this,n=e.cells.length,r=this.splitBusinessHourSegs(e.businessHourSegs,n),i=this.splitBgEventSegs(e.bgEventSegs,n),s=this.splitFgEventSegs(e.fgEventSegs,n),o=this.splitDateSelectionSegs(e.dateSelectionSegs,n),a=this.splitEventDrag(e.eventDrag,n),l=this.splitEventResize(e.eventResize,n),c=n>=7&&e.clientWidth?e.clientWidth/t.options.aspectRatio/6:null;return p(Ii,{unit:"day"},(t,d)=>p(y,null,e.cells.map((t,u)=>p(bl,{ref:this.rowRefs.createRef(u),key:t.length?t[0].date.toISOString():u,showDayNumbers:n>1,showWeekNumbers:e.showWeekNumbers,todayRange:d,dateProfile:e.dateProfile,cells:t,renderIntro:e.renderRowIntro,businessHourSegs:r[u],eventSelection:e.eventSelection,bgEventSegs:i[u],fgEventSegs:s[u],dateSelectionSegs:o[u],eventDrag:a[u],eventResize:l[u],dayMaxEvents:e.dayMaxEvents,dayMaxEventRows:e.dayMaxEventRows,clientWidth:e.clientWidth,clientHeight:e.clientHeight,cellMinHeight:c,forPrint:e.forPrint}))))}componentDidMount(){this.registerInteractiveComponent()}componentDidUpdate(){this.registerInteractiveComponent()}registerInteractiveComponent(){if(!this.rootEl){const e=this.rowRefs.currentMap[0].getCellEls()[0],t=e?e.closest(".fc-daygrid-body"):null;t&&(this.rootEl=t,this.context.registerInteractiveComponent(this,{el:t,isHitComboAllowed:this.props.isHitComboAllowed}))}}componentWillUnmount(){this.rootEl&&(this.context.unregisterInteractiveComponent(this),this.rootEl=null)}prepareHits(){this.rowPositions=new rs(this.rootEl,this.rowRefs.collect().map(e=>e.getCellEls()[0]),!1,!0),this.colPositions=new rs(this.rootEl,this.rowRefs.currentMap[0].getCellEls(),!0,!1)}queryHit(e,t){let{colPositions:n,rowPositions:r}=this,i=n.leftToIndex(e),s=r.topToIndex(t);if(null!=s&&null!=i){let e=this.props.cells[s][i];return{dateProfile:this.props.dateProfile,dateSpan:Object.assign({range:this.getCellRange(s,i),allDay:!0},e.extraDateSpan),dayEl:this.getCellEl(s,i),rect:{left:n.lefts[i],right:n.rights[i],top:r.tops[s],bottom:r.bottoms[s]},layer:0}}return null}getCellEl(e,t){return this.rowRefs.currentMap[e].getCellEls()[t]}getCellRange(e,t){let n=this.props.cells[e][t].date;return{start:n,end:wt(n,1)}}}function Sl(e,t){return tl(e.filter(Al),t)}function Al(e){return e.eventRange.def.allDay}class Dl extends ls{constructor(){super(...arguments),this.elRef={current:null},this.needsScrollReset=!1}render(){let{props:e}=this,{dayMaxEventRows:t,dayMaxEvents:n,expandRows:r}=e,i=!0===n||!0===t;i&&!r&&(i=!1,t=null,n=null);let s=["fc-daygrid-body",i?"fc-daygrid-body-balanced":"fc-daygrid-body-unbalanced",r?"":"fc-daygrid-body-natural"];return p("div",{ref:this.elRef,className:s.join(" "),style:{width:e.clientWidth,minWidth:e.tableMinWidth}},p("table",{role:"presentation",className:"fc-scrollgrid-sync-table",style:{width:e.clientWidth,minWidth:e.tableMinWidth,height:r?e.clientHeight:""}},e.colGroupNode,p("tbody",{role:"presentation"},p(El,{dateProfile:e.dateProfile,cells:e.cells,renderRowIntro:e.renderRowIntro,showWeekNumbers:e.showWeekNumbers,clientWidth:e.clientWidth,clientHeight:e.clientHeight,businessHourSegs:e.businessHourSegs,bgEventSegs:e.bgEventSegs,fgEventSegs:e.fgEventSegs,dateSelectionSegs:e.dateSelectionSegs,eventSelection:e.eventSelection,eventDrag:e.eventDrag,eventResize:e.eventResize,dayMaxEvents:n,dayMaxEventRows:t,forPrint:e.forPrint,isHitComboAllowed:e.isHitComboAllowed}))))}componentDidMount(){this.requestScrollReset()}componentDidUpdate(e){e.dateProfile!==this.props.dateProfile?this.requestScrollReset():this.flushScrollReset()}requestScrollReset(){this.needsScrollReset=!0,this.flushScrollReset()}flushScrollReset(){if(this.needsScrollReset&&this.props.clientWidth){const e=function(e,t){let n;t.currentRangeUnit.match(/year|month/)&&(n=e.querySelector(`[data-date="${Wt(t.currentDate)}-01"]`));n||(n=e.querySelector(`[data-date="${Lt(t.currentDate)}"]`));return n}(this.elRef.current,this.props.dateProfile);if(e){const t=e.closest(".fc-daygrid-body"),n=t.closest(".fc-scroller"),r=e.getBoundingClientRect().top-t.getBoundingClientRect().top;n.scrollTop=r?r+1:0}this.needsScrollReset=!1}}}class wl extends ks{constructor(){super(...arguments),this.forceDayIfListItem=!0}sliceRange(e,t){return t.sliceRange(e)}}class Cl extends ls{constructor(){super(...arguments),this.slicer=new wl,this.tableRef={current:null}}render(){let{props:e,context:t}=this;return p(Dl,Object.assign({ref:this.tableRef},this.slicer.sliceProps(e,e.dateProfile,e.nextDayThreshold,t,e.dayTableModel),{dateProfile:e.dateProfile,cells:e.dayTableModel.cells,colGroupNode:e.colGroupNode,tableMinWidth:e.tableMinWidth,renderRowIntro:e.renderRowIntro,dayMaxEvents:e.dayMaxEvents,dayMaxEventRows:e.dayMaxEventRows,showWeekNumbers:e.showWeekNumbers,expandRows:e.expandRows,headerAlignElRef:e.headerAlignElRef,clientWidth:e.clientWidth,clientHeight:e.clientHeight,forPrint:e.forPrint}))}}function Rl(e,t){let n=new _s(e.renderRange,t);return new Ts(n,/year|month|week/.test(e.currentRangeUnit))}class xl extends hr{buildRenderRange(e,t,n){let r=super.buildRenderRange(e,t,n),{props:i}=this;return _l({currentRange:r,snapToWeek:/^(year|month)$/.test(t),fixedWeekCount:i.fixedWeekCount,dateEnv:i.dateEnv})}}function _l(e){let t,{dateEnv:n,currentRange:r}=e,{start:i,end:s}=r;if(e.snapToWeek&&(i=n.startOfWeek(i),t=n.startOfWeek(s),t.valueOf()!==s.valueOf()&&(s=Dt(t,1))),e.fixedWeekCount){let e=n.startOfWeek(n.startOfMonth(wt(r.end,-1)));s=Dt(s,6-Math.ceil(Rt(e,s)))}return{start:i,end:s}}xe(':root{--fc-daygrid-event-dot-width:8px}.fc-daygrid-day-events:after,.fc-daygrid-day-events:before,.fc-daygrid-day-frame:after,.fc-daygrid-day-frame:before,.fc-daygrid-event-harness:after,.fc-daygrid-event-harness:before{clear:both;content:"";display:table}.fc .fc-daygrid-body{position:relative;z-index:1}.fc .fc-daygrid-day.fc-day-today{background-color:var(--fc-today-bg-color)}.fc .fc-daygrid-day-frame{min-height:100%;position:relative}.fc .fc-daygrid-day-top{display:flex;flex-direction:row-reverse}.fc .fc-day-other .fc-daygrid-day-top{opacity:.3}.fc .fc-daygrid-day-number{padding:4px;position:relative;z-index:4}.fc .fc-daygrid-month-start{font-size:1.1em;font-weight:700}.fc .fc-daygrid-day-events{margin-top:1px}.fc .fc-daygrid-body-balanced .fc-daygrid-day-events{left:0;position:absolute;right:0}.fc .fc-daygrid-body-unbalanced .fc-daygrid-day-events{min-height:2em;position:relative}.fc .fc-daygrid-body-natural .fc-daygrid-day-events{margin-bottom:1em}.fc .fc-daygrid-event-harness{position:relative}.fc .fc-daygrid-event-harness-abs{left:0;position:absolute;right:0;top:0}.fc .fc-daygrid-bg-harness{bottom:0;position:absolute;top:0}.fc .fc-daygrid-day-bg .fc-non-business{z-index:1}.fc .fc-daygrid-day-bg .fc-bg-event{z-index:2}.fc .fc-daygrid-day-bg .fc-highlight{z-index:3}.fc .fc-daygrid-event{margin-top:1px;z-index:6}.fc .fc-daygrid-event.fc-event-mirror{z-index:7}.fc .fc-daygrid-day-bottom{font-size:.85em;margin:0 2px}.fc .fc-daygrid-day-bottom:after,.fc .fc-daygrid-day-bottom:before{clear:both;content:"";display:table}.fc .fc-daygrid-more-link{border-radius:3px;cursor:pointer;line-height:1;margin-top:1px;max-width:100%;overflow:hidden;padding:2px;position:relative;white-space:nowrap;z-index:4}.fc .fc-daygrid-more-link:hover{background-color:rgba(0,0,0,.1)}.fc .fc-daygrid-week-number{background-color:var(--fc-neutral-bg-color);color:var(--fc-neutral-text-color);min-width:1.5em;padding:2px;position:absolute;text-align:center;top:0;z-index:5}.fc .fc-more-popover .fc-popover-body{min-width:220px;padding:10px}.fc-direction-ltr .fc-daygrid-event.fc-event-start,.fc-direction-rtl .fc-daygrid-event.fc-event-end{margin-left:2px}.fc-direction-ltr .fc-daygrid-event.fc-event-end,.fc-direction-rtl .fc-daygrid-event.fc-event-start{margin-right:2px}.fc-direction-ltr .fc-daygrid-more-link{float:left}.fc-direction-ltr .fc-daygrid-week-number{border-radius:0 0 3px 0;left:0}.fc-direction-rtl .fc-daygrid-more-link{float:right}.fc-direction-rtl .fc-daygrid-week-number{border-radius:0 0 0 3px;right:0}.fc-liquid-hack .fc-daygrid-day-frame{position:static}.fc-daygrid-event{border-radius:3px;font-size:var(--fc-small-font-size);position:relative;white-space:nowrap}.fc-daygrid-block-event .fc-event-time{font-weight:700}.fc-daygrid-block-event .fc-event-time,.fc-daygrid-block-event .fc-event-title{padding:1px}.fc-daygrid-dot-event{align-items:center;display:flex;padding:2px 0}.fc-daygrid-dot-event .fc-event-title{flex-grow:1;flex-shrink:1;font-weight:700;min-width:0;overflow:hidden}.fc-daygrid-dot-event.fc-event-mirror,.fc-daygrid-dot-event:hover{background:rgba(0,0,0,.1)}.fc-daygrid-dot-event.fc-event-selected:before{bottom:-10px;top:-10px}.fc-daygrid-event-dot{border:calc(var(--fc-daygrid-event-dot-width)/2) solid var(--fc-event-border-color);border-radius:calc(var(--fc-daygrid-event-dot-width)/2);box-sizing:content-box;height:0;margin:0 4px;width:0}.fc-direction-ltr .fc-daygrid-event .fc-event-time{margin-right:3px}.fc-direction-rtl .fc-daygrid-event .fc-event-time{margin-left:3px}');var Tl=Oo({name:"@fullcalendar/daygrid",initialView:"dayGridMonth",views:{dayGrid:{component:class extends el{constructor(){super(...arguments),this.buildDayTableModel=Gt(Rl),this.headerRef={current:null},this.tableRef={current:null}}render(){let{options:e,dateProfileGenerator:t}=this.context,{props:n}=this,r=this.buildDayTableModel(n.dateProfile,t),i=e.dayHeaders&&p(Rs,{ref:this.headerRef,dateProfile:n.dateProfile,dates:r.headerDates,datesRepDistinctDays:1===r.rowCnt}),s=t=>p(Cl,{ref:this.tableRef,dateProfile:n.dateProfile,dayTableModel:r,businessHours:n.businessHours,dateSelection:n.dateSelection,eventStore:n.eventStore,eventUiBases:n.eventUiBases,eventSelection:n.eventSelection,eventDrag:n.eventDrag,eventResize:n.eventResize,nextDayThreshold:e.nextDayThreshold,colGroupNode:t.tableColGroupNode,tableMinWidth:t.tableMinWidth,dayMaxEvents:e.dayMaxEvents,dayMaxEventRows:e.dayMaxEventRows,showWeekNumbers:e.weekNumbers,expandRows:!n.isHeightAuto,headerAlignElRef:this.headerElRef,clientWidth:t.clientWidth,clientHeight:t.clientHeight,forPrint:n.forPrint});return e.dayMinWidth?this.renderHScrollLayout(i,s,r.colCnt,e.dayMinWidth):this.renderSimpleLayout(i,s)}},dateProfileGeneratorClass:xl},dayGridDay:{type:"dayGrid",duration:{days:1}},dayGridWeek:{type:"dayGrid",duration:{weeks:1}},dayGridMonth:{type:"dayGrid",duration:{months:1},fixedWeekCount:!0},dayGridYear:{type:"dayGrid",duration:{years:1}}}});class kl extends Li{getKeyInfo(){return{allDay:{},timed:{}}}getKeysForDateSpan(e){return e.allDay?["allDay"]:["timed"]}getKeysForEventDef(e){return e.allDay?ri(e)?["timed","allDay"]:["allDay"]:["timed"]}}const Ml=an({hour:"numeric",minute:"2-digit",omitZeroMinute:!0,meridiem:"short"});function Il(e){let t=["fc-timegrid-slot","fc-timegrid-slot-label",e.isLabeled?"fc-scrollgrid-shrink":"fc-timegrid-slot-minor"];return p(Ln.Consumer,null,n=>{if(!e.isLabeled)return p("td",{className:t.join(" "),"data-time":e.isoTimeStr});let{dateEnv:r,options:i,viewApi:s}=n,o=null==i.slotLabelFormat?Ml:Array.isArray(i.slotLabelFormat)?an(i.slotLabelFormat[0]):an(i.slotLabelFormat),a={level:0,time:e.time,date:r.toDate(e.date),view:s,text:r.format(e.date,o)};return p($n,{elTag:"td",elClasses:t,elAttrs:{"data-time":e.isoTimeStr},renderProps:a,generatorName:"slotLabelContent",customGenerator:i.slotLabelContent,defaultGenerator:Ol,classNameGenerator:i.slotLabelClassNames,didMount:i.slotLabelDidMount,willUnmount:i.slotLabelWillUnmount},e=>p("div",{className:"fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame"},p(e,{elTag:"div",elClasses:["fc-timegrid-slot-label-cushion","fc-scrollgrid-shrink-cushion"]})))})}function Ol(e){return e.text}class Nl extends Vn{render(){return this.props.slatMetas.map(e=>p("tr",{key:e.key},p(Il,Object.assign({},e))))}}const Pl=an({week:"short"});class Hl extends ls{constructor(){super(...arguments),this.allDaySplitter=new kl,this.headerElRef={current:null},this.rootElRef={current:null},this.scrollerElRef={current:null},this.state={slatCoords:null},this.handleScrollTopRequest=e=>{let t=this.scrollerElRef.current;t&&(t.scrollTop=e)},this.renderHeadAxis=(e,t="")=>{let{options:n}=this.context,{dateProfile:r}=this.props,i=r.renderRange,s=1===xt(i.start,i.end)?qi(this.context,i.start,"week"):{};return n.weekNumbers&&"day"===e?p(po,{elTag:"th",elClasses:["fc-timegrid-axis","fc-scrollgrid-shrink"],elAttrs:{"aria-hidden":!0},date:i.start,defaultFormat:Pl},e=>p("div",{className:["fc-timegrid-axis-frame","fc-scrollgrid-shrink-frame","fc-timegrid-axis-frame-liquid"].join(" "),style:{height:t}},p(e,{elTag:"a",elClasses:["fc-timegrid-axis-cushion","fc-scrollgrid-shrink-cushion","fc-scrollgrid-sync-inner"],elAttrs:s}))):p("th",{"aria-hidden":!0,className:"fc-timegrid-axis"},p("div",{className:"fc-timegrid-axis-frame",style:{height:t}}))},this.renderTableRowAxis=e=>{let{options:t,viewApi:n}=this.context,r={text:t.allDayText,view:n};return p($n,{elTag:"td",elClasses:["fc-timegrid-axis","fc-scrollgrid-shrink"],elAttrs:{"aria-hidden":!0},renderProps:r,generatorName:"allDayContent",customGenerator:t.allDayContent,defaultGenerator:Bl,classNameGenerator:t.allDayClassNames,didMount:t.allDayDidMount,willUnmount:t.allDayWillUnmount},t=>p("div",{className:["fc-timegrid-axis-frame","fc-scrollgrid-shrink-frame",null==e?" fc-timegrid-axis-frame-liquid":""].join(" "),style:{height:e}},p(t,{elTag:"span",elClasses:["fc-timegrid-axis-cushion","fc-scrollgrid-shrink-cushion","fc-scrollgrid-sync-inner"]})))},this.handleSlatCoords=e=>{this.setState({slatCoords:e})}}renderSimpleLayout(e,t,n){let{context:r,props:i}=this,s=[],o=eo(r.options);return e&&s.push({type:"header",key:"header",isSticky:o,chunk:{elRef:this.headerElRef,tableClassName:"fc-col-header",rowContent:e}}),t&&(s.push({type:"body",key:"all-day",chunk:{content:t}}),s.push({type:"body",key:"all-day-divider",outerContent:p("tr",{role:"presentation",className:"fc-scrollgrid-section"},p("td",{className:"fc-timegrid-divider "+r.theme.getClass("tableCellShaded")}))})),s.push({type:"body",key:"body",liquid:!0,expandRows:Boolean(r.options.expandRows),chunk:{scrollerElRef:this.scrollerElRef,content:n}}),p(Kn,{elRef:this.rootElRef,elClasses:["fc-timegrid"],viewSpec:r.viewSpec},p(no,{liquid:!i.isHeightAuto&&!i.forPrint,collapsibleWidth:i.forPrint,cols:[{width:"shrink"}],sections:s}))}renderHScrollLayout(e,t,n,r,i,s,o){let a=this.context.pluginHooks.scrollGridImpl;if(!a)throw new Error("No ScrollGrid implementation");let{context:l,props:c}=this,d=!c.forPrint&&eo(l.options),u=!c.forPrint&&to(l.options),h=[];e&&h.push({type:"header",key:"header",isSticky:d,syncRowHeights:!0,chunks:[{key:"axis",rowContent:e=>p("tr",{role:"presentation"},this.renderHeadAxis("day",e.rowSyncHeights[0]))},{key:"cols",elRef:this.headerElRef,tableClassName:"fc-col-header",rowContent:e}]}),t&&(h.push({type:"body",key:"all-day",syncRowHeights:!0,chunks:[{key:"axis",rowContent:e=>p("tr",{role:"presentation"},this.renderTableRowAxis(e.rowSyncHeights[0]))},{key:"cols",content:t}]}),h.push({key:"all-day-divider",type:"body",outerContent:p("tr",{role:"presentation",className:"fc-scrollgrid-section"},p("td",{colSpan:2,className:"fc-timegrid-divider "+l.theme.getClass("tableCellShaded")}))}));let f=l.options.nowIndicator;return h.push({type:"body",key:"body",liquid:!0,expandRows:Boolean(l.options.expandRows),chunks:[{key:"axis",content:e=>p("div",{className:"fc-timegrid-axis-chunk"},p("table",{"aria-hidden":!0,style:{height:e.expandRows?e.clientHeight:""}},e.tableColGroupNode,p("tbody",null,p(Nl,{slatMetas:s}))),p("div",{className:"fc-timegrid-now-indicator-container"},p(Ii,{unit:f?"minute":"day"},e=>{let t=f&&o&&o.safeComputeTop(e);return"number"==typeof t?p(oo,{elClasses:["fc-timegrid-now-indicator-arrow"],elStyle:{top:t},isAxis:!0,date:e}):null})))},{key:"cols",scrollerElRef:this.scrollerElRef,content:n}]}),u&&h.push({key:"footer",type:"footer",isSticky:!0,chunks:[{key:"axis",content:Ks},{key:"cols",content:Ks}]}),p(Kn,{elRef:this.rootElRef,elClasses:["fc-timegrid"],viewSpec:l.viewSpec},p(a,{liquid:!c.isHeightAuto&&!c.forPrint,forPrint:c.forPrint,collapsibleWidth:!1,colGroups:[{width:"shrink",cols:[{width:"shrink"}]},{cols:[{span:r,minWidth:i}]}],sections:h}))}getAllDayMaxEventProps(){let{dayMaxEvents:e,dayMaxEventRows:t}=this.context.options;return!0!==e&&!0!==t||(e=void 0,t=5),{dayMaxEvents:e,dayMaxEventRows:t}}}function Bl(e){return e.text}class jl{constructor(e,t,n){this.positions=e,this.dateProfile=t,this.slotDuration=n}safeComputeTop(e){let{dateProfile:t}=this;if(ar(t.currentRange,e)){let n=Mt(e),r=e.valueOf()-n.valueOf();if(r>=yt(t.slotMinTime)&&r{let o={time:i.time,date:t.dateEnv.toDate(i.date),view:t.viewApi};return p("tr",{key:i.key,ref:r.createRef(i.key)},e.axis&&p(Il,Object.assign({},i)),p($n,{elTag:"td",elClasses:["fc-timegrid-slot","fc-timegrid-slot-lane",!i.isLabeled&&"fc-timegrid-slot-minor"],elAttrs:{"data-time":i.isoTimeStr},renderProps:o,generatorName:"slotLaneContent",customGenerator:n.slotLaneContent,classNameGenerator:n.slotLaneClassNames,didMount:n.slotLaneDidMount,willUnmount:n.slotLaneWillUnmount}))}))}}class Ul extends Vn{constructor(){super(...arguments),this.rootElRef={current:null},this.slatElRefs=new Ws}render(){let{props:e,context:t}=this;return p("div",{ref:this.rootElRef,className:"fc-timegrid-slots"},p("table",{"aria-hidden":!0,className:t.theme.getClass("table"),style:{minWidth:e.tableMinWidth,width:e.clientWidth,height:e.minHeight}},e.tableColGroupNode,p(zl,{slatElRefs:this.slatElRefs,axis:e.axis,slatMetas:e.slatMetas})))}componentDidMount(){this.updateSizing()}componentDidUpdate(){this.updateSizing()}componentWillUnmount(){this.props.onCoords&&this.props.onCoords(null)}updateSizing(){let{context:e,props:t}=this;if(t.onCoords&&null!==t.clientWidth){this.rootElRef.current.offsetHeight&&t.onCoords(new jl(new rs(this.rootElRef.current,(n=this.slatElRefs.currentMap,t.slatMetas.map(e=>n[e.key])),!1,!0),this.props.dateProfile,e.options.slotDuration))}var n}}function Ll(e,t){let n,r=[];for(n=0;nec(e.hiddenSegs,e),defaultGenerator:Vl,forceTimed:!0},e=>p(e,{elTag:"div",elClasses:["fc-timegrid-more-link-inner","fc-sticky"]}))}}function Vl(e){return e.shortText}function Gl(e,t,n){let r=new cs;null!=t&&(r.strictOrder=t),null!=n&&(r.maxStackCnt=n);let i=hs(r.addSegs(e)),s=function(e){const{entriesByLevel:t}=e,n=Zl((e,t)=>e+":"+t,(r,i)=>{let s=Ql(function(e,t,n){let{levelCoords:r,entriesByLevel:i}=e,s=i[t][n],o=r[t]+s.thickness,a=r.length,l=t;for(;lus(e),(e,r,i)=>{let s,{nextLevelNodes:o,thickness:a}=e,l=a+i,c=a/l,d=[];if(o.length)for(let e of o)if(void 0===s){let t=n(e,r,l);s=t[0],d.push(t[1])}else{let t=n(e,s,0);d.push(t[1])}else s=t;let u=(s-r)*c;return[s-u,Object.assign(Object.assign({},e),{thickness:u,nextLevelNodes:d})]});return e.map(e=>n(e,0,0)[1])}(s,1),{segRects:function(e){let t=[];const n=Zl((e,t,n)=>us(e),(e,n,i)=>{let s=Object.assign(Object.assign({},e),{levelCoord:n,stackDepth:i,stackForward:0});return t.push(s),s.stackForward=r(e.nextLevelNodes,n+e.thickness,i+1)+1});function r(e,t,r){let i=0;for(let s of e)i=Math.max(n(s,t,r),i);return i}return r(e,0,0),t}(s),hiddenGroups:i}}function Ql(e,t){if(!e)return[[],0];let{level:n,lateralStart:r,lateralEnd:i}=e,s=r,o=[];for(;s{let i=e(...r);return i in n?n[i]:n[i]=t(...r)}}function Xl(e,t,n=null,r=0){let i=[];if(n)for(let s=0;sp("div",{className:"fc-timegrid-col-frame"},p("div",{className:"fc-timegrid-col-bg"},this.renderFillSegs(e.businessHourSegs,"non-business"),this.renderFillSegs(e.bgEventSegs,"bg-event"),this.renderFillSegs(e.dateSelectionSegs,"highlight")),p("div",{className:"fc-timegrid-col-events"},this.renderFgSegs(o,s,!1,!1,!1)),p("div",{className:"fc-timegrid-col-events"},this.renderFgSegs(i,{},Boolean(e.eventDrag),Boolean(e.eventResize),Boolean(r),"mirror")),p("div",{className:"fc-timegrid-now-indicator-container"},this.renderNowIndicator(e.nowIndicatorSegs)),co(n)&&p(t,{elTag:"div",elClasses:["fc-timegrid-col-misc"]})))}renderFgSegs(e,t,n,r,i,s){let{props:o}=this;return o.forPrint?ec(e,o):this.renderPositionedFgSegs(e,t,n,r,i,s)}renderPositionedFgSegs(e,t,n,r,i,s){let{eventMaxStack:o,eventShortHeight:a,eventOrderStrict:l,eventMinHeight:c}=this.context.options,{date:d,slatCoords:u,eventSelection:h,todayRange:f,nowDate:g}=this.props,m=n||r||i,v=Xl(e,d,u,c),{segPlacements:b,hiddenGroups:E}=function(e,t,n,r){let i=[],s=[];for(let n=0;n{let{seg:o,rect:l}=e,c=o.eventRange.instance.instanceId,d=m||Boolean(!t[c]&&l),u=tc(l&&l.span),v=!m&&l?this.computeSegHStyle(l):{left:0,right:0},y=Boolean(l)&&l.stackForward>0,b=Boolean(l)&&l.span.end-l.span.start{let c=tc(e.span),d=(u=e.entries,h=t,u.map(e=>h[e.index]));var u,h;return p(Fl,{key:Ut(Ao(d)),hiddenSegs:d,top:c.top,bottom:c.bottom,extraDateSpan:n,dateProfile:r,todayRange:i,nowDate:s,eventSelection:o,eventDrag:a,eventResize:l})}))}renderFillSegs(e,t){let{props:n,context:r}=this,i=Xl(e,n.date,n.slatCoords,r.options.eventMinHeight).map((r,i)=>{let s=e[i];return p("div",{key:mi(s.eventRange),className:"fc-timegrid-bg-harness",style:tc(r)},"bg-event"===t?p(ho,Object.assign({seg:s},gi(s,n.todayRange,n.nowDate))):go(t))});return p(y,null,i)}renderNowIndicator(e){let{slatCoords:t,date:n}=this.props;return t?e.map((e,r)=>p(oo,{key:r,elClasses:["fc-timegrid-now-indicator-line"],elStyle:{top:t.computeDateTop(e.start,n)},isAxis:!1,date:n})):null}computeSegHStyle(e){let t,n,{isRtl:r,options:i}=this.context,s=i.slotEventOverlap,o=e.levelCoord,a=e.levelCoord+e.thickness;s&&(a=Math.min(1,o+2*(a-o))),r?(t=1-a,n=o):(t=o,n=1-a);let l={zIndex:e.stackDepth+1,left:100*t+"%",right:100*n+"%"};return s&&!e.stackForward&&(l[r?"marginLeft":"marginRight"]=20),l}}function ec(e,{todayRange:t,nowDate:n,eventSelection:r,eventDrag:i,eventResize:s}){let o=(i?i.affectedInstances:null)||(s?s.affectedInstances:null)||{};return p(y,null,e.map(e=>{let i=e.eventRange.instance.instanceId;return p("div",{key:i,style:{visibility:o[i]?"hidden":""}},p(Jl,Object.assign({seg:e,isDragging:!1,isResizing:!1,isDateSelecting:!1,isSelected:i===r,isShort:!1},gi(e,t,n))))}))}function tc(e){return e?{top:e.start,bottom:-e.end}:{top:"",bottom:""}}class nc extends Vn{constructor(){super(...arguments),this.splitFgEventSegs=Gt(Ll),this.splitBgEventSegs=Gt(Ll),this.splitBusinessHourSegs=Gt(Ll),this.splitNowIndicatorSegs=Gt(Ll),this.splitDateSelectionSegs=Gt(Ll),this.splitEventDrag=Gt(Wl),this.splitEventResize=Gt(Wl),this.rootElRef={current:null},this.cellElRefs=new Ws}render(){let{props:e,context:t}=this,n=t.options.nowIndicator&&e.slatCoords&&e.slatCoords.safeComputeTop(e.nowDate),r=e.cells.length,i=this.splitFgEventSegs(e.fgEventSegs,r),s=this.splitBgEventSegs(e.bgEventSegs,r),o=this.splitBusinessHourSegs(e.businessHourSegs,r),a=this.splitNowIndicatorSegs(e.nowIndicatorSegs,r),l=this.splitDateSelectionSegs(e.dateSelectionSegs,r),c=this.splitEventDrag(e.eventDrag,r),d=this.splitEventResize(e.eventResize,r);return p("div",{className:"fc-timegrid-cols",ref:this.rootElRef},p("table",{role:"presentation",style:{minWidth:e.tableMinWidth,width:e.clientWidth}},e.tableColGroupNode,p("tbody",{role:"presentation"},p("tr",{role:"row"},e.axis&&p("td",{"aria-hidden":!0,className:"fc-timegrid-col fc-timegrid-axis"},p("div",{className:"fc-timegrid-col-frame"},p("div",{className:"fc-timegrid-now-indicator-container"},"number"==typeof n&&p(oo,{elClasses:["fc-timegrid-now-indicator-arrow"],elStyle:{top:n},isAxis:!0,date:e.nowDate})))),e.cells.map((t,n)=>p(Kl,{key:t.key,elRef:this.cellElRefs.createRef(t.key),dateProfile:e.dateProfile,date:t.date,nowDate:e.nowDate,todayRange:e.todayRange,extraRenderProps:t.extraRenderProps,extraDataAttrs:t.extraDataAttrs,extraClassNames:t.extraClassNames,extraDateSpan:t.extraDateSpan,fgEventSegs:i[n],bgEventSegs:s[n],businessHourSegs:o[n],nowIndicatorSegs:a[n],dateSelectionSegs:l[n],eventDrag:c[n],eventResize:d[n],slatCoords:e.slatCoords,eventSelection:e.eventSelection,forPrint:e.forPrint}))))))}componentDidMount(){this.updateCoords()}componentDidUpdate(){this.updateCoords()}updateCoords(){let{props:e}=this;var t;e.onColCoords&&null!==e.clientWidth&&e.onColCoords(new rs(this.rootElRef.current,(t=this.cellElRefs.currentMap,e.cells.map(e=>t[e.key])),!0,!1))}}class rc extends ls{constructor(){super(...arguments),this.processSlotOptions=Gt(ic),this.state={slatCoords:null},this.handleRootEl=e=>{e?this.context.registerInteractiveComponent(this,{el:e,isHitComboAllowed:this.props.isHitComboAllowed}):this.context.unregisterInteractiveComponent(this)},this.handleScrollRequest=e=>{let{onScrollTopRequest:t}=this.props,{slatCoords:n}=this.state;if(t&&n){if(e.time){let r=n.computeTimeTop(e.time);r=Math.ceil(r),r&&(r+=1),t(r)}return!0}return!1},this.handleColCoords=e=>{this.colCoords=e},this.handleSlatCoords=e=>{this.setState({slatCoords:e}),this.props.onSlatCoords&&this.props.onSlatCoords(e)}}render(){let{props:e,state:t}=this;return p("div",{className:"fc-timegrid-body",ref:this.handleRootEl,style:{width:e.clientWidth,minWidth:e.tableMinWidth}},p(Ul,{axis:e.axis,dateProfile:e.dateProfile,slatMetas:e.slatMetas,clientWidth:e.clientWidth,minHeight:e.expandRows?e.clientHeight:"",tableMinWidth:e.tableMinWidth,tableColGroupNode:e.axis?e.tableColGroupNode:null,onCoords:this.handleSlatCoords}),p(nc,{cells:e.cells,axis:e.axis,dateProfile:e.dateProfile,businessHourSegs:e.businessHourSegs,bgEventSegs:e.bgEventSegs,fgEventSegs:e.fgEventSegs,dateSelectionSegs:e.dateSelectionSegs,eventSelection:e.eventSelection,eventDrag:e.eventDrag,eventResize:e.eventResize,todayRange:e.todayRange,nowDate:e.nowDate,nowIndicatorSegs:e.nowIndicatorSegs,clientWidth:e.clientWidth,tableMinWidth:e.tableMinWidth,tableColGroupNode:e.tableColGroupNode,slatCoords:t.slatCoords,onColCoords:this.handleColCoords,forPrint:e.forPrint}))}componentDidMount(){this.scrollResponder=this.context.createScrollResponder(this.handleScrollRequest)}componentDidUpdate(e){this.scrollResponder.update(e.dateProfile!==this.props.dateProfile)}componentWillUnmount(){this.scrollResponder.detach()}queryHit(e,t){let{dateEnv:n,options:r}=this.context,{colCoords:i}=this,{dateProfile:s}=this.props,{slatCoords:o}=this.state,{snapDuration:a,snapsPerSlot:l}=this.processSlotOptions(this.props.slotDuration,r.snapDuration),c=i.leftToIndex(e),d=o.positions.topToIndex(t);if(null!=c&&null!=d){let e=this.props.cells[c],r=o.positions.tops[d],u=o.positions.getHeight(d),h=(t-r)/u,f=d*l+Math.floor(h*l),g=this.props.cells[c].date,p=pt(s.slotMinTime,mt(a,f)),m=n.add(g,p),v=n.add(m,a);return{dateProfile:s,dateSpan:Object.assign({range:{start:m,end:v},allDay:!1},e.extraDateSpan),dayEl:i.els[c],rect:{left:i.lefts[c],right:i.rights[c],top:r,bottom:r+u},layer:0}}return null}}function ic(e,t){let n=t||e,r=bt(e,n);return null===r&&(n=e,r=1),{snapDuration:n,snapsPerSlot:r}}class sc extends ks{sliceRange(e,t){let n=[];for(let r=0;rp(rc,Object.assign({ref:this.timeColsRef},this.slicer.sliceProps(e,n,null,t,o),{forPrint:e.forPrint,axis:e.axis,dateProfile:n,slatMetas:e.slatMetas,slotDuration:e.slotDuration,cells:r.cells[0],tableColGroupNode:e.tableColGroupNode,tableMinWidth:e.tableMinWidth,clientWidth:e.clientWidth,clientHeight:e.clientHeight,expandRows:e.expandRows,nowDate:a,nowIndicatorSegs:i&&this.slicer.sliceNowDate(a,n,s,t,o),todayRange:l,onScrollTopRequest:e.onScrollTopRequest,onSlatCoords:e.onSlatCoords})))}}function ac(e,t,n){let r=[];for(let i of e.headerDates)r.push({start:n.add(i,t.slotMinTime),end:n.add(i,t.slotMaxTime)});return r}const lc=[{hours:1},{minutes:30},{minutes:15},{seconds:30},{seconds:15}];function cc(e,t,n,r,i){let s=new Date(0),o=e,a=ft(0),l=n||function(e){let t,n,r;for(t=lc.length-1;t>=0;t-=1)if(n=ft(lc[t]),r=bt(n,e),null!==r&&r>1)return n;return e}(r),c=[];for(;yt(o)table,.fc .fc-timegrid-slots{position:relative;z-index:1}.fc .fc-timegrid-slot{border-bottom:0;height:1.5em}.fc .fc-timegrid-slot:empty:before{content:"\\00a0"}.fc .fc-timegrid-slot-minor{border-top-style:dotted}.fc .fc-timegrid-slot-label-cushion{display:inline-block;white-space:nowrap}.fc .fc-timegrid-slot-label{vertical-align:middle}.fc .fc-timegrid-axis-cushion,.fc .fc-timegrid-slot-label-cushion{padding:0 4px}.fc .fc-timegrid-axis-frame-liquid{height:100%}.fc .fc-timegrid-axis-frame{align-items:center;display:flex;justify-content:flex-end;overflow:hidden}.fc .fc-timegrid-axis-cushion{flex-shrink:0;max-width:60px}.fc-direction-ltr .fc-timegrid-slot-label-frame{text-align:right}.fc-direction-rtl .fc-timegrid-slot-label-frame{text-align:left}.fc-liquid-hack .fc-timegrid-axis-frame-liquid{bottom:0;height:auto;left:0;position:absolute;right:0;top:0}.fc .fc-timegrid-col.fc-day-today{background-color:var(--fc-today-bg-color)}.fc .fc-timegrid-col-frame{min-height:100%;position:relative}.fc-media-screen.fc-liquid-hack .fc-timegrid-col-frame{bottom:0;height:auto;left:0;position:absolute;right:0;top:0}.fc-media-screen .fc-timegrid-cols{bottom:0;left:0;position:absolute;right:0;top:0}.fc-media-screen .fc-timegrid-cols>table{height:100%}.fc-media-screen .fc-timegrid-col-bg,.fc-media-screen .fc-timegrid-col-events,.fc-media-screen .fc-timegrid-now-indicator-container{left:0;position:absolute;right:0;top:0}.fc .fc-timegrid-col-bg{z-index:2}.fc .fc-timegrid-col-bg .fc-non-business{z-index:1}.fc .fc-timegrid-col-bg .fc-bg-event{z-index:2}.fc .fc-timegrid-col-bg .fc-highlight{z-index:3}.fc .fc-timegrid-bg-harness{left:0;position:absolute;right:0}.fc .fc-timegrid-col-events{z-index:3}.fc .fc-timegrid-now-indicator-container{bottom:0;overflow:hidden}.fc-direction-ltr .fc-timegrid-col-events{margin:0 2.5% 0 2px}.fc-direction-rtl .fc-timegrid-col-events{margin:0 2px 0 2.5%}.fc-timegrid-event-harness{position:absolute}.fc-timegrid-event-harness>.fc-timegrid-event{bottom:0;left:0;position:absolute;right:0;top:0}.fc-timegrid-event-harness-inset .fc-timegrid-event,.fc-timegrid-event.fc-event-mirror,.fc-timegrid-more-link{box-shadow:0 0 0 1px var(--fc-page-bg-color)}.fc-timegrid-event,.fc-timegrid-more-link{border-radius:3px;font-size:var(--fc-small-font-size)}.fc-timegrid-event{margin-bottom:1px}.fc-timegrid-event .fc-event-main{padding:1px 1px 0}.fc-timegrid-event .fc-event-time{font-size:var(--fc-small-font-size);margin-bottom:1px;white-space:nowrap}.fc-timegrid-event-short .fc-event-main-frame{flex-direction:row;overflow:hidden}.fc-timegrid-event-short .fc-event-time:after{content:"\\00a0-\\00a0"}.fc-timegrid-event-short .fc-event-title{font-size:var(--fc-small-font-size)}.fc-timegrid-more-link{background:var(--fc-more-link-bg-color);color:var(--fc-more-link-text-color);cursor:pointer;margin-bottom:1px;position:absolute;z-index:9999}.fc-timegrid-more-link-inner{padding:3px 2px;top:0}.fc-direction-ltr .fc-timegrid-more-link{right:0}.fc-direction-rtl .fc-timegrid-more-link{left:0}.fc .fc-timegrid-now-indicator-arrow,.fc .fc-timegrid-now-indicator-line{pointer-events:none}.fc .fc-timegrid-now-indicator-line{border-color:var(--fc-now-indicator-color);border-style:solid;border-width:1px 0 0;left:0;position:absolute;right:0;z-index:4}.fc .fc-timegrid-now-indicator-arrow{border-color:var(--fc-now-indicator-color);border-style:solid;margin-top:-5px;position:absolute;z-index:4}.fc-direction-ltr .fc-timegrid-now-indicator-arrow{border-bottom-color:transparent;border-top-color:transparent;border-width:5px 0 5px 6px;left:0}.fc-direction-rtl .fc-timegrid-now-indicator-arrow{border-bottom-color:transparent;border-top-color:transparent;border-width:5px 6px 5px 0;right:0}');var uc=Oo({name:"@fullcalendar/timegrid",initialView:"timeGridWeek",optionRefiners:{allDaySlot:Boolean},views:{timeGrid:{component:class extends Hl{constructor(){super(...arguments),this.buildTimeColsModel=Gt(dc),this.buildSlatMetas=Gt(cc)}render(){let{options:e,dateEnv:t,dateProfileGenerator:n}=this.context,{props:r}=this,{dateProfile:i}=r,s=this.buildTimeColsModel(i,n),o=this.allDaySplitter.splitProps(r),a=this.buildSlatMetas(i.slotMinTime,i.slotMaxTime,e.slotLabelInterval,e.slotDuration,t),{dayMinWidth:l}=e,c=!l,d=l,u=e.dayHeaders&&p(Rs,{dates:s.headerDates,dateProfile:i,datesRepDistinctDays:!0,renderIntro:c?this.renderHeadAxis:null}),h=!1!==e.allDaySlot&&(t=>p(Cl,Object.assign({},o.allDay,{dateProfile:i,dayTableModel:s,nextDayThreshold:e.nextDayThreshold,tableMinWidth:t.tableMinWidth,colGroupNode:t.tableColGroupNode,renderRowIntro:c?this.renderTableRowAxis:null,showWeekNumbers:!1,expandRows:!1,headerAlignElRef:this.headerElRef,clientWidth:t.clientWidth,clientHeight:t.clientHeight,forPrint:r.forPrint},this.getAllDayMaxEventProps()))),f=t=>p(oc,Object.assign({},o.timed,{dayTableModel:s,dateProfile:i,axis:c,slotDuration:e.slotDuration,slatMetas:a,forPrint:r.forPrint,tableColGroupNode:t.tableColGroupNode,tableMinWidth:t.tableMinWidth,clientWidth:t.clientWidth,clientHeight:t.clientHeight,onSlatCoords:this.handleSlatCoords,expandRows:t.expandRows,onScrollTopRequest:this.handleScrollTopRequest}));return d?this.renderHScrollLayout(u,h,f,s.colCnt,l,a,this.state.slatCoords):this.renderSimpleLayout(u,h,f)}},usesMinMaxTime:!0,allDaySlot:!0,slotDuration:"00:30:00",slotEventOverlap:!0},timeGridDay:{type:"timeGrid",duration:{days:1}},timeGridWeek:{type:"timeGrid",duration:{weeks:1}}}});class hc extends Vn{constructor(){super(...arguments),this.state={textId:Le()}}render(){let{theme:e,dateEnv:t,options:n,viewApi:r}=this.context,{cellId:i,dayDate:s,todayRange:o}=this.props,{textId:a}=this.state,l=Fi(s,o),c=n.listDayFormat?t.format(s,n.listDayFormat):"",d=n.listDaySideFormat?t.format(s,n.listDaySideFormat):"",u=Object.assign({date:t.toDate(s),view:r,textId:a,text:c,sideText:d,navLinkAttrs:qi(this.context,s),sideNavLinkAttrs:qi(this.context,s,"day",!1)},l);return p($n,{elTag:"tr",elClasses:["fc-list-day",...Vi(l,e)],elAttrs:{"data-date":Lt(s)},renderProps:u,generatorName:"dayHeaderContent",customGenerator:n.dayHeaderContent,defaultGenerator:fc,classNameGenerator:n.dayHeaderClassNames,didMount:n.dayHeaderDidMount,willUnmount:n.dayHeaderWillUnmount},t=>p("th",{scope:"colgroup",colSpan:3,id:i,"aria-labelledby":a},p(t,{elTag:"div",elClasses:["fc-list-day-cushion",e.getClass("tableCellShaded")]})))}}function fc(e){return p(y,null,e.text&&p("a",Object.assign({id:e.textId,className:"fc-list-day-text"},e.navLinkAttrs),e.text),e.sideText&&p("a",Object.assign({"aria-hidden":!0,className:"fc-list-day-side-text"},e.sideNavLinkAttrs),e.sideText))}const gc=an({hour:"numeric",minute:"2-digit",meridiem:"short"});class pc extends Vn{render(){let{props:e,context:t}=this,{options:n}=t,{seg:r,timeHeaderId:i,eventHeaderId:s,dateHeaderId:o}=e,a=n.eventTimeFormat||gc;return p(ro,Object.assign({},e,{elTag:"tr",elClasses:["fc-list-event",r.eventRange.def.url&&"fc-event-forced-url"],defaultGenerator:()=>function(e,t){let n=vi(e,t);return p("a",Object.assign({},n),e.eventRange.def.title)}(r,t),seg:r,timeText:"",disableDragging:!0,disableResizing:!0}),(e,n)=>p(y,null,function(e,t,n,r,i){let{options:s}=n;if(!1!==s.displayEventTime){let o,a=e.eventRange.def,l=e.eventRange.instance,c=!1;if(a.allDay?c=!0:dr(e.eventRange.range)?e.isStart?o=fi(e,t,n,null,null,l.range.start,e.end):e.isEnd?o=fi(e,t,n,null,null,e.start,l.range.end):c=!0:o=fi(e,t,n),c){let e={text:n.options.allDayText,view:n.viewApi};return p($n,{elTag:"td",elClasses:["fc-list-event-time"],elAttrs:{headers:`${r} ${i}`},renderProps:e,generatorName:"allDayContent",customGenerator:s.allDayContent,defaultGenerator:mc,classNameGenerator:s.allDayClassNames,didMount:s.allDayDidMount,willUnmount:s.allDayWillUnmount})}return p("td",{className:"fc-list-event-time"},o)}return null}(r,a,t,i,o),p("td",{"aria-hidden":!0,className:"fc-list-event-graphic"},p("span",{className:"fc-list-event-dot",style:{borderColor:n.borderColor||n.backgroundColor}})),p(e,{elTag:"td",elClasses:["fc-list-event-title"],elAttrs:{headers:`${s} ${o}`}})))}}function mc(e){return e.text}function vc(e){return e.text}function yc(e){let t=Mt(e.renderRange.start),n=e.renderRange.end,r=[],i=[];for(;t*{border-left:0;border-right:0}.fc .fc-list-sticky .fc-list-day>*{background:var(--fc-page-bg-color);position:sticky;top:0}.fc .fc-list-table thead{left:-10000px;position:absolute}.fc .fc-list-table tbody>tr:first-child th{border-top:0}.fc .fc-list-table th{padding:0}.fc .fc-list-day-cushion,.fc .fc-list-table td{padding:8px 14px}.fc .fc-list-day-cushion:after{clear:both;content:"";display:table}.fc-theme-standard .fc-list-day-cushion{background-color:var(--fc-neutral-bg-color)}.fc-direction-ltr .fc-list-day-text,.fc-direction-rtl .fc-list-day-side-text{float:left}.fc-direction-ltr .fc-list-day-side-text,.fc-direction-rtl .fc-list-day-text{float:right}.fc-direction-ltr .fc-list-table .fc-list-event-graphic{padding-right:0}.fc-direction-rtl .fc-list-table .fc-list-event-graphic{padding-left:0}.fc .fc-list-event.fc-event-forced-url{cursor:pointer}.fc .fc-list-event:hover td{background-color:var(--fc-list-event-hover-bg-color)}.fc .fc-list-event-graphic,.fc .fc-list-event-time{white-space:nowrap;width:1px}.fc .fc-list-event-dot{border:calc(var(--fc-list-event-dot-width)/2) solid var(--fc-event-border-color);border-radius:calc(var(--fc-list-event-dot-width)/2);box-sizing:content-box;display:inline-block;height:0;width:0}.fc .fc-list-event-title a{color:inherit;text-decoration:none}.fc .fc-list-event.fc-event-forced-url:hover a{text-decoration:underline}');function bc(e){return!1===e?null:an(e)}var Ec=Oo({name:"@fullcalendar/list",optionRefiners:{listDayFormat:bc,listDaySideFormat:bc,noEventsClassNames:yn,noEventsContent:yn,noEventsDidMount:yn,noEventsWillUnmount:yn},views:{list:{component:class extends ls{constructor(){super(...arguments),this.computeDateVars=Gt(yc),this.eventStoreToSegs=Gt(this._eventStoreToSegs),this.state={timeHeaderId:Le(),eventHeaderId:Le(),dateHeaderIdRoot:Le()},this.setRootEl=e=>{e?this.context.registerInteractiveComponent(this,{el:e}):this.context.unregisterInteractiveComponent(this)}}render(){let{props:e,context:t}=this,{dayDates:n,dayRanges:r}=this.computeDateVars(e.dateProfile),i=this.eventStoreToSegs(e.eventStore,e.eventUiBases,r);return p(Kn,{elRef:this.setRootEl,elClasses:["fc-list",t.theme.getClass("table"),!1!==t.options.stickyHeaderDates?"fc-list-sticky":""],viewSpec:t.viewSpec},p(Ls,{liquid:!e.isHeightAuto,overflowX:e.isHeightAuto?"visible":"hidden",overflowY:e.isHeightAuto?"visible":"auto"},i.length>0?this.renderSegList(i,n):this.renderEmptyMessage()))}renderEmptyMessage(){let{options:e,viewApi:t}=this.context,n={text:e.noEventsText,view:t};return p($n,{elTag:"div",elClasses:["fc-list-empty"],renderProps:n,generatorName:"noEventsContent",customGenerator:e.noEventsContent,defaultGenerator:vc,classNameGenerator:e.noEventsClassNames,didMount:e.noEventsDidMount,willUnmount:e.noEventsWillUnmount},e=>p(e,{elTag:"div",elClasses:["fc-list-empty-cushion"]}))}renderSegList(e,t){let{theme:n,options:r}=this.context,{timeHeaderId:i,eventHeaderId:s,dateHeaderIdRoot:o}=this.state,a=function(e){let t,n,r=[];for(t=0;t{let c=[];for(let n=0;n{e&&this.updateSize()}}render(){const{context:e,props:t,state:n}=this,{options:r}=e,{clientWidth:i,clientHeight:s}=n,o=n.monthHPadding||0,a=Math.min(null!=i?Math.floor(i/(r.multiMonthMinWidth+o)):1,r.multiMonthMaxColumns)||1,l=100/a+"%",c=null==i?null:i/a-o,d=null!=i&&1===a,u=this.splitDateProfileByMonth(e.dateProfileGenerator,t.dateProfile,e.dateEnv,!d&&r.fixedWeekCount,r.showNonCurrentDates),h=this.buildMonthFormat(r.multiMonthTitleFormat,u),f=["fc-multimonth",d?"fc-multimonth-singlecol":"fc-multimonth-multicol",null!=c&&c<400?"fc-multimonth-compact":"",t.isHeightAuto?"":"fc-scroller"];return p(Kn,{elRef:this.scrollElRef,elClasses:f,viewSpec:e.viewSpec},u.map((e,n)=>{const r=Wt(e.currentRange.start);return p(Sc,Object.assign({},t,{key:r,isoDateStr:r,elRef:0===n?this.firstMonthElRef:void 0,titleFormat:h,dateProfile:e,width:l,tableWidth:c,clientWidth:i,clientHeight:s}))}))}componentDidMount(){this.updateSize(),this.context.addResizeHandler(this.handleSizing),this.requestScrollReset()}componentDidUpdate(e){Cn(e,this.props)||this.handleSizing(!1),e.dateProfile!==this.props.dateProfile?this.requestScrollReset():this.flushScrollReset()}componentWillUnmount(){this.context.removeResizeHandler(this.handleSizing)}updateSize(){const e=this.scrollElRef.current,t=this.firstMonthElRef.current;e&&this.setState({clientWidth:e.clientWidth,clientHeight:e.clientHeight}),t&&e&&null==this.state.monthHPadding&&this.setState({monthHPadding:e.clientWidth-t.firstChild.offsetWidth})}requestScrollReset(){this.needsScrollReset=!0,this.flushScrollReset()}flushScrollReset(){if(this.needsScrollReset&&null!=this.state.monthHPadding){const{currentDate:e}=this.props.dateProfile,t=this.scrollElRef.current,n=t.querySelector(`[data-date="${Wt(e)}"]`);t.scrollTop=n.getBoundingClientRect().top-this.firstMonthElRef.current.getBoundingClientRect().top,this.needsScrollReset=!1}}shouldComponentUpdate(){return!0}},dateProfileGeneratorClass:xl,multiMonthMinWidth:350,multiMonthMaxColumns:3},multiMonthYear:{type:"multiMonth",duration:{years:1},fixedWeekCount:!0,showNonCurrentDates:!1}}});return aa.push(Ka,Tl,uc,Ec,_c),e.Calendar=class extends Ni{constructor(e,t={}){super(),this.isRendering=!1,this.isRendered=!1,this.currentClassNames=[],this.customContentRenderId=0,this.handleAction=e=>{switch(e.type){case"SET_EVENT_DRAG":case"SET_EVENT_RESIZE":this.renderRunner.tryDrain()}},this.handleData=e=>{this.currentData=e,this.renderRunner.request(e.calendarOptions.rerenderDelay)},this.handleRenderRequest=()=>{if(this.isRendering){this.isRendered=!0;let{currentData:e}=this;Bn(()=>{U(p(_i,{options:e.calendarOptions,theme:e.theme,emitter:e.emitter},(t,n,r,i)=>(this.setClassNames(t),this.setHeight(n),p(Xn.Provider,{value:this.customContentRenderId},p(xa,Object.assign({isHeightAuto:r,forPrint:i},e))))),this.el)})}else this.isRendered&&(this.isRendered=!1,U(null,this.el),this.setClassNames([]),this.setHeight(""))},function(e){e.isConnected&&e.getRootNode&&_e(e.getRootNode())}(e),this.el=e,this.renderRunner=new Me(this.handleRenderRequest),new ua({optionOverrides:t,calendarApi:this,onAction:this.handleAction,onData:this.handleData})}render(){let e=this.isRendering;e?this.customContentRenderId+=1:this.isRendering=!0,this.renderRunner.request(),e&&this.updateSize()}destroy(){this.isRendering&&(this.isRendering=!1,this.renderRunner.request())}updateSize(){Bn(()=>{super.updateSize()})}batchRendering(e){this.renderRunner.pause("batchRendering"),e(),this.renderRunner.resume("batchRendering")}pauseRendering(){this.renderRunner.pause("pauseRendering")}resumeRendering(){this.renderRunner.resume("pauseRendering",!0)}resetOptions(e,t){this.currentDataManager.resetOptions(e,t)}setClassNames(e){if(!St(e,this.currentClassNames)){let{classList:t}=this.el;for(let e of this.currentClassNames)t.remove(e);for(let n of e)t.add(n);this.currentClassNames=e}}setHeight(e){je(this.el,"height",e)}},e.Draggable=class{constructor(e,t={}){this.handlePointerDown=e=>{let{dragging:t}=this,{minDistance:n,longPressDelay:r}=this.settings;t.minDistance=null!=n?n:e.isTouch?0:cn.eventDragMinDistance,t.delay=e.isTouch?null!=r?r:cn.longPressDelay:0},this.handleDragStart=e=>{e.isTouch&&this.dragging.delay&&e.subjectEl.classList.contains("fc-event")&&this.dragging.mirror.getMirrorEl().classList.add("fc-event-selected")},this.settings=t;let n=this.dragging=new Wa(e);n.touchScrollAllowed=!1,null!=t.itemSelector&&(n.pointer.selector=t.itemSelector),null!=t.appendTo&&(n.mirror.parentNode=t.appendTo),n.emitter.on("pointerdown",this.handlePointerDown),n.emitter.on("dragstart",this.handleDragStart),new $a(n,t.eventData)}destroy(){this.dragging.destroy()}},e.Internal=Co,e.JsonRequestError=wi,e.Preact=Ro,e.ThirdPartyDraggable=class{constructor(e,t){let n=document;e===document||e instanceof Element?(n=e,t=t||{}):t=e||{};let r=this.dragging=new Ja(n);"string"==typeof t.itemSelector?r.pointer.selector=t.itemSelector:n===document&&(r.pointer.selector="[data-event]"),"string"==typeof t.mirrorSelector&&(r.mirrorSelector=t.mirrorSelector),new $a(r,t.eventData).hitDragging.disablePointCheck=!0}destroy(){this.dragging.destroy()}},e.createPlugin=Oo,e.formatDate=function(e,t={}){let n=ka(t),r=an(t),i=n.createMarkerMeta(e);return i?n.format(i.marker,r,{forcedTzo:i.forcedTzo}):""},e.formatRange=function(e,t,n){let r=ka("object"==typeof n&&n?n:{}),i=an(n),s=r.createMarkerMeta(e),o=r.createMarkerMeta(t);return s&&o?r.formatRange(s.marker,o.marker,i,{forcedStartTzo:s.forcedTzo,forcedEndTzo:o.forcedTzo,isEndExclusive:n.isEndExclusive,defaultSeparator:cn.defaultRangeSeparator}):""},e.globalLocales=xo,e.globalPlugins=aa,e.sliceEvents=function(e,t){return ni(e.eventStore,e.eventUiBases,e.dateProfile.activeRange,t?e.nextDayThreshold:null).fg},e.version="6.1.19",Object.defineProperty(e,"__esModule",{value:!0}),e}({}); return FullCalendar; } /* 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. --- The following Lucide icons are derived from the Feather project: airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out The MIT License (MIT) (for the icons listed above) Copyright (c) 2013-present Cole Bemis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ let organizationLayout; function loadOrganizationLayout() { if (!organizationLayout) { const exports = {}; const module = { exports }; /* Copyright 2010-2021 Mike Bostock 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. */ // https://d3js.org/d3-hierarchy/ v3.1.2 Copyright 2010-2021 Mike Bostock !function(n,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r((n="undefined"!=typeof globalThis?globalThis:n||self).d3=n.d3||{})}(this,(function(n){"use strict";function r(n,r){return n.parent===r.parent?1:2}function t(n,r){return n+r.x}function e(n,r){return Math.max(n,r.y)}function i(n){var r=0,t=n.children,e=t&&t.length;if(e)for(;--e>=0;)r+=t[e].value;else r=1;n.value=r}function u(n,r){n instanceof Map?(n=[void 0,n],void 0===r&&(r=f)):void 0===r&&(r=o);for(var t,e,i,u,a,l=new h(n),p=[l];t=p.pop();)if((i=r(t.data))&&(a=(i=Array.from(i)).length))for(t.children=i,u=a-1;u>=0;--u)p.push(e=i[u]=new h(i[u])),e.parent=t,e.depth=t.depth+1;return l.eachBefore(c)}function o(n){return n.children}function f(n){return Array.isArray(n)?n[1]:null}function a(n){void 0!==n.data.value&&(n.value=n.data.value),n.data=n.data.data}function c(n){var r=0;do{n.height=r}while((n=n.parent)&&n.height<++r)}function h(n){this.data=n,this.depth=this.height=0,this.parent=null}function l(n){return null==n?null:p(n)}function p(n){if("function"!=typeof n)throw new Error;return n}function d(){return 0}function s(n){return function(){return n}}h.prototype=u.prototype={constructor:h,count:function(){return this.eachAfter(i)},each:function(n,r){let t=-1;for(const e of this)n.call(r,e,++t,this);return this},eachAfter:function(n,r){for(var t,e,i,u=this,o=[u],f=[],a=-1;u=o.pop();)if(f.push(u),t=u.children)for(e=0,i=t.length;e=0;--e)u.push(t[e]);return this},find:function(n,r){let t=-1;for(const e of this)if(n.call(r,e,++t,this))return e},sum:function(n){return this.eachAfter((function(r){for(var t=+n(r.data)||0,e=r.children,i=e&&e.length;--i>=0;)t+=e[i].value;r.value=t}))},sort:function(n){return this.eachBefore((function(r){r.children&&r.children.sort(n)}))},path:function(n){for(var r=this,t=function(n,r){if(n===r)return n;var t=n.ancestors(),e=r.ancestors(),i=null;n=t.pop(),r=e.pop();for(;n===r;)i=n,n=t.pop(),r=e.pop();return i}(r,n),e=[r];r!==t;)r=r.parent,e.push(r);for(var i=e.length;n!==t;)e.splice(i,0,n),n=n.parent;return e},ancestors:function(){for(var n=this,r=[n];n=n.parent;)r.push(n);return r},descendants:function(){return Array.from(this)},leaves:function(){var n=[];return this.eachBefore((function(r){r.children||n.push(r)})),n},links:function(){var n=this,r=[];return n.each((function(t){t!==n&&r.push({source:t.parent,target:t})})),r},copy:function(){return u(this).eachBefore(a)},[Symbol.iterator]:function*(){var n,r,t,e,i=this,u=[i];do{for(n=u.reverse(),u=[];i=n.pop();)if(yield i,r=i.children)for(t=0,e=r.length;t(n=(1664525*n+1013904223)%v)/v}function y(n,r){for(var t,e,i=0,u=(n=function(n,r){let t,e,i=n.length;for(;i;)e=r()*i--|0,t=n[i],n[i]=n[e],n[e]=t;return n}(Array.from(n),r)).length,o=[];i0&&t*t>e*e+i*i}function _(n,r){for(var t=0;t1e-6?(q+Math.sqrt(q*q-4*b*E))/(2*b):E/q);return{x:e+M+z*S,y:i+B+A*S,r:S}}function A(n,r,t){var e,i,u,o,f=n.x-r.x,a=n.y-r.y,c=f*f+a*a;c?(i=r.r+t.r,i*=i,o=n.r+t.r,i>(o*=o)?(e=(c+o-i)/(2*c),u=Math.sqrt(Math.max(0,o/c-e*e)),t.x=n.x-e*f-u*a,t.y=n.y-e*a+u*f):(e=(c+i-o)/(2*c),u=Math.sqrt(Math.max(0,i/c-e*e)),t.x=r.x+e*f-u*a,t.y=r.y+e*a+u*f)):(t.x=r.x+t.r,t.y=r.y)}function b(n,r){var t=n.r+r.r-1e-6,e=r.x-n.x,i=r.y-n.y;return t>0&&t*t>e*e+i*i}function q(n){var r=n._,t=n.next._,e=r.r+t.r,i=(r.x*t.r+t.x*r.r)/e,u=(r.y*t.r+t.y*r.r)/e;return i*i+u*u}function Node(n){this._=n,this.next=null,this.previous=null}function E(n,r){if(!(o=(t=n,n="object"==typeof t&&"length"in t?t:Array.from(t)).length))return 0;var t,e,i,u,o,f,a,c,h,l,p,d;if((e=n[0]).x=0,e.y=0,!(o>1))return e.r;if(i=n[1],e.x=-i.r,i.x=e.r,i.y=0,!(o>2))return e.r+i.r;A(i,e,u=n[2]),e=new Node(e),i=new Node(i),u=new Node(u),e.next=u.previous=i,i.next=e.previous=u,u.next=i.previous=e;n:for(c=3;c1&&!C(n,r););return n.slice(0,r)}function C(n,r){if("/"===n[r]){let t=0;for(;r>0&&"\\"===n[--r];)++t;if(0==(1&t))return!0}return!1}function F(n,r){return n.parent===r.parent?1:2}function G(n){var r=n.children;return r?r[0]:n.t}function H(n){var r=n.children;return r?r[r.length-1]:n.t}function J(n,r,t){var e=t/(r.i-n.i);r.c-=e,r.s+=t,n.c+=e,r.z+=t,r.m+=t}function K(n,r,t){return n.a.parent===r.parent?n.a:t}function Q(n,r){this._=n,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=r}function U(n,r,t,e,i){for(var u,o=n.children,f=-1,a=o.length,c=n.value&&(i-t)/n.value;++fp&&(p=f),x=h*h*v,(d=Math.max(p/x,x/l))>s){h-=f;break}s=d}y.push(o={value:h,dice:a1?r:1)},t}(V);var Y=function n(r){function t(n,t,e,i,u){if((o=n._squarify)&&o.ratio===r)for(var o,f,a,c,h,l=-1,p=o.length,d=n.value;++l1?r:1)},t}(V);n.Node=h,n.cluster=function(){var n=r,i=1,u=1,o=!1;function f(r){var f,a=0;r.eachAfter((function(r){var i=r.children;i?(r.x=function(n){return n.reduce(t,0)/n.length}(i),r.y=function(n){return 1+n.reduce(e,0)}(i)):(r.x=f?a+=n(r,f):0,r.y=0,f=r)}));var c=function(n){for(var r;r=n.children;)n=r[0];return n}(r),h=function(n){for(var r;r=n.children;)n=r[r.length-1];return n}(r),l=c.x-n(c,h)/2,p=h.x+n(h,c)/2;return r.eachAfter(o?function(n){n.x=(n.x-r.x)*i,n.y=(r.y-n.y)*u}:function(n){n.x=(n.x-l)/(p-l)*i,n.y=(1-(r.y?n.y/r.y:1))*u})}return f.separation=function(r){return arguments.length?(n=r,f):n},f.size=function(n){return arguments.length?(o=!1,i=+n[0],u=+n[1],f):o?null:[i,u]},f.nodeSize=function(n){return arguments.length?(o=!0,i=+n[0],u=+n[1],f):o?[i,u]:null},f},n.hierarchy=u,n.pack=function(){var n=null,r=1,t=1,e=d;function i(i){const u=x();return i.x=r/2,i.y=t/2,n?i.eachBefore(k(n)).eachAfter(I(e,.5,u)).eachBefore(T(1)):i.eachBefore(k(S)).eachAfter(I(d,1,u)).eachAfter(I(e,i.r/Math.min(r,t),u)).eachBefore(T(Math.min(r,t)/(2*i.r))),i}return i.radius=function(r){return arguments.length?(n=l(r),i):n},i.size=function(n){return arguments.length?(r=+n[0],t=+n[1],i):[r,t]},i.padding=function(n){return arguments.length?(e="function"==typeof n?n:s(+n),i):e},i},n.packEnclose=function(n){return y(n,x())},n.packSiblings=function(n){return E(n,x()),n},n.partition=function(){var n=1,r=1,t=0,e=!1;function i(i){var u=i.height+1;return i.x0=i.y0=t,i.x1=n,i.y1=r/u,i.eachBefore(function(n,r){return function(e){e.children&&O(e,e.x0,n*(e.depth+1)/r,e.x1,n*(e.depth+2)/r);var i=e.x0,u=e.y0,o=e.x1-t,f=e.y1-t;ofunction(n){let r=(n=`${n}`).length;C(n,r-1)&&!C(n,r-2)&&(n=n.slice(0,-1));return"/"===n[0]?n:`/${n}`}(n(r,t,e)))),t=r.map(P),i=new Set(r).add("");for(const n of t)i.has(n)||(i.add(n),r.push(n),t.push(P(n)),s.push(L));v=(n,t)=>r[t],x=(n,r)=>t[r]}for(o=0,i=s.length;o=0&&(l=s[n],l.data===L);--n)l.data=null}if(f.parent=R,f.eachBefore((function(n){n.depth=n.parent.depth+1,--i})).eachBefore(c),f.parent=null,i>0)throw new Error("cycle");return f}return e.id=function(n){return arguments.length?(r=l(n),e):r},e.parentId=function(n){return arguments.length?(t=l(n),e):t},e.path=function(r){return arguments.length?(n=l(r),e):n},e},n.tree=function(){var n=F,r=1,t=1,e=null;function i(i){var a=function(n){for(var r,t,e,i,u,o=new Q(n,0),f=[o];r=f.pop();)if(e=r._.children)for(r.children=new Array(u=e.length),i=u-1;i>=0;--i)f.push(t=r.children[i]=new Q(e[i],i)),t.parent=r;return(o.parent=new Q(null,0)).children=[o],o}(i);if(a.eachAfter(u),a.parent.m=-a.z,a.eachBefore(o),e)i.eachBefore(f);else{var c=i,h=i,l=i;i.eachBefore((function(n){n.xh.x&&(h=n),n.depth>l.depth&&(l=n)}));var p=c===h?1:n(c,h)/2,d=p-c.x,s=r/(h.x+p+d),v=t/(l.depth||1);i.eachBefore((function(n){n.x=(n.x+d)*s,n.y=n.depth*v}))}return i}function u(r){var t=r.children,e=r.parent.children,i=r.i?e[r.i-1]:null;if(t){!function(n){for(var r,t=0,e=0,i=n.children,u=i.length;--u>=0;)(r=i[u]).z+=t,r.m+=t,t+=r.s+(e+=r.c)}(r);var u=(t[0].z+t[t.length-1].z)/2;i?(r.z=i.z+n(r._,i._),r.m=r.z-u):r.z=u}else i&&(r.z=i.z+n(r._,i._));r.parent.A=function(r,t,e){if(t){for(var i,u=r,o=r,f=t,a=u.parent.children[0],c=u.m,h=o.m,l=f.m,p=a.m;f=H(f),u=G(u),f&&u;)a=G(a),(o=H(o)).a=r,(i=f.z+l-u.z-c+n(f._,u._))>0&&(J(K(f,r,e),r,i),c+=i,h+=i),l+=f.m,c+=u.m,p+=a.m,h+=o.m;f&&!H(o)&&(o.t=f,o.m+=l-h),u&&!G(a)&&(a.t=u,a.m+=c-p,e=r)}return e}(r,i,r.parent.A||e[0])}function o(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function f(n){n.x*=r,n.y=n.depth*t}return i.separation=function(r){return arguments.length?(n=r,i):n},i.size=function(n){return arguments.length?(e=!1,r=+n[0],t=+n[1],i):e?null:[r,t]},i.nodeSize=function(n){return arguments.length?(e=!0,r=+n[0],t=+n[1],i):e?[r,t]:null},i},n.treemap=function(){var n=X,r=!1,t=1,e=1,i=[0],u=d,o=d,f=d,a=d,c=d;function h(n){return n.x0=n.y0=0,n.x1=t,n.y1=e,n.eachBefore(l),i=[0],r&&n.eachBefore(j),n}function l(r){var t=i[r.depth],e=r.x0+t,h=r.y0+t,l=r.x1-t,p=r.y1-t;l=t-1){var h=f[r];return h.x0=i,h.y0=u,h.x1=o,void(h.y1=a)}var l=c[r],p=e/2+l,d=r+1,s=t-1;for(;d>>1;c[v]a-u){var g=e?(i*y+o*x)/e:o;n(r,d,x,i,u,g,a),n(d,t,y,g,u,o,a)}else{var m=e?(u*y+a*x)/e:a;n(r,d,x,i,u,o,m),n(d,t,y,i,m,o,a)}}(0,a,n.value,r,t,e,i)},n.treemapDice=O,n.treemapResquarify=Y,n.treemapSlice=U,n.treemapSliceDice=function(n,r,t,e,i){(1&n.depth?U:O)(n,r,t,e,i)},n.treemapSquarify=X,Object.defineProperty(n,"__esModule",{value:!0})})); organizationLayout = exports; } return organizationLayout; } // Source: src/features/org-chart/model.js function createOrgChartModel() { function normalize(nodes) { if (!Array.isArray(nodes) || nodes.length > 500) throw new Error("An organization chart supports up to 500 people."); const ids = new Set(); const result = nodes.map(node => { if (!node || typeof node.id !== "string" || !/^[\w-]{1,80}$/.test(node.id) || ids.has(node.id)) throw new Error("Invalid or duplicate person ID."); ids.add(node.id); const item = { id: node.id, parentId: node.parentId == null ? null : node.parentId, collapsed: node.collapsed === true }; const jobTitle = node.jobTitle ?? node.role ?? ""; if (typeof jobTitle !== "string" || jobTitle.length > 160) throw new Error("Invalid Job Title."); item.jobTitle = jobTitle.trim(); if (node.PartyNumber != null && node.PartyNumber !== "") { if (!/^[A-Za-z0-9]{1,100}$/.test(String(node.PartyNumber))) throw new Error("Invalid contact PartyNumber."); item.PartyNumber = String(node.PartyNumber); } if (node.contactPartyId != null) { if (typeof node.contactPartyId !== "string" || !/^[\w-]{1,100}$/.test(node.contactPartyId)) throw new Error("Invalid contact ID."); item.contactPartyId = node.contactPartyId; } for (const [key, max] of [["name", 160], ["notes", 4000]]) { if (typeof node[key] !== "string" || node[key].length > max || (key === "name" && !node[key].trim())) throw new Error(`Enter a valid ${key} (up to ${max} characters).`); item[key] = key === "notes" ? node[key] : node[key].trim(); } return item; }); const byId = new Map(result.map(node => [node.id, node])); for (const node of result) { const seen = new Set([node.id]); let parent = node.parentId; while (parent !== null) { if (!byId.has(parent) || seen.has(parent)) throw new Error("The reporting structure contains a missing manager or a cycle."); seen.add(parent); if (seen.size > 60) throw new Error("The reporting structure supports up to 60 levels."); parent = byId.get(parent).parentId; } } return result; } function descendants(nodes, id) { const found = new Set([id]); let changed = true; while (changed) { changed = false; for (const node of nodes) if (found.has(node.parentId) && !found.has(node.id)) { found.add(node.id); changed = true; } } return found; } function upsert(nodes, person) { return normalize(nodes.some(node => node.id === person.id) ? nodes.map(node => node.id === person.id ? person : node) : [...nodes, person]); } function remove(nodes, id) { const person = nodes.find(node => node.id === id); if (!person) return nodes; return normalize(nodes.filter(node => node.id !== id).map(node => node.parentId === id ? { ...node, parentId: person.parentId } : node)); } function hierarchy(nodes) { const root = { id: "", children: [] }, byId = new Map([[null, root]]); nodes.forEach(node => byId.set(node.id, { ...node, children: [] })); nodes.forEach(node => { if (!byId.get(node.parentId).collapsed) byId.get(node.parentId).children.push(byId.get(node.id)); }); return root; } return { normalize, descendants, upsert, remove, hierarchy }; } // Source: src/features/org-chart/icons.js function createOrgChartIcons({ environment }) { // Lucide geometry; license included in src/vendor/lucide/LICENSE. const paths = { opportunities: ["M3 7h18v14H3Z", "M8 7V3h8v4", "M3 12h18", "M10 12v3h4v-3"], contact: ["M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2", "M13 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0", "M20 8v6", "M17 11h6"], chart: ["M12 8v4", "M12 12H5v4", "M12 12h7v4"], plus: ["M12 5v14", "M5 12h14"], close: ["m6 6 12 12", "M18 6 6 18"], check: ["m9 12 2 2 4-4"], down: ["m6 9 6 6 6-6"], up: ["m18 15-6-6-6 6"], expand: ["m7 10 5-5 5 5", "m7 14 5 5 5-5"], collapse: ["m7 4 5 5 5-5", "m7 20 5-5 5 5"], minus: ["M5 12h14"], fit: ["M8 3H5a2 2 0 0 0-2 2v3", "M16 3h3a2 2 0 0 1 2 2v3", "M21 16v3a2 2 0 0 1-2 2h-3", "M8 21H5a2 2 0 0 1-2-2v-3"], save: ["M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h12l4 4v12a2 2 0 0 1-2 2", "M7 21v-8h10v8", "M7 3v6h10V3"], trash: ["M3 6h18", "M19 6v14H5V6", "M10 11v6", "M14 11v6", "M9 6V3h6v3"], note: ["M12 3H5v18h14V10Z", "M12 3v7h7", "M8 15h8", "M8 18h5"] }; function icon(name) { const svg = environment.document.createElementNS("http://www.w3.org/2000/svg", "svg"); for (const [key, value] of Object.entries({ viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.8", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", focusable: "false" })) svg.setAttribute(key, value); for (const d of paths[name] || paths.chart) { const path = environment.document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", d); svg.append(path); } if (name === "chart") for (const [x, y] of [[8, 2], [1, 16], [15, 16]]) { const rect = environment.document.createElementNS("http://www.w3.org/2000/svg", "rect"); for (const [key, value] of Object.entries({ x, y, width: 8, height: 6, rx: 1 })) rect.setAttribute(key, value); svg.append(rect); } return svg; } return { icon }; } // Source: src/features/org-chart/calling-codes.js // Calling codes: Google libphonenumber, Apache-2.0. Retrieved 2026-10-26. // https://github.com/google/libphonenumber/blob/master/resources/PhoneNumberMetadata.xml const orgChartCallingCodes = [["AC","247"],["AD","376"],["AE","971"],["AF","93"],["AG","1"],["AI","1"],["AL","355"],["AM","374"],["AO","244"],["AR","54"],["AS","1"],["AT","43"],["AU","61"],["AW","297"],["AX","358"],["AZ","994"],["BA","387"],["BB","1"],["BD","880"],["BE","32"],["BF","226"],["BG","359"],["BH","973"],["BI","257"],["BJ","229"],["BL","590"],["BM","1"],["BN","673"],["BO","591"],["BQ","599"],["BR","55"],["BS","1"],["BT","975"],["BW","267"],["BY","375"],["BZ","501"],["CA","1"],["CC","61"],["CD","243"],["CF","236"],["CG","242"],["CH","41"],["CI","225"],["CK","682"],["CL","56"],["CM","237"],["CN","86"],["CO","57"],["CR","506"],["CU","53"],["CV","238"],["CW","599"],["CX","61"],["CY","357"],["CZ","420"],["DE","49"],["DJ","253"],["DK","45"],["DM","1"],["DO","1"],["DZ","213"],["EC","593"],["EE","372"],["EG","20"],["EH","212"],["ER","291"],["ES","34"],["ET","251"],["FI","358"],["FJ","679"],["FK","500"],["FM","691"],["FO","298"],["FR","33"],["GA","241"],["GB","44"],["GD","1"],["GE","995"],["GF","594"],["GG","44"],["GH","233"],["GI","350"],["GL","299"],["GM","220"],["GN","224"],["GP","590"],["GQ","240"],["GR","30"],["GT","502"],["GU","1"],["GW","245"],["GY","592"],["HK","852"],["HN","504"],["HR","385"],["HT","509"],["HU","36"],["ID","62"],["IE","353"],["IL","972"],["IM","44"],["IN","91"],["IO","246"],["IQ","964"],["IR","98"],["IS","354"],["IT","39"],["JE","44"],["JM","1"],["JO","962"],["JP","81"],["KE","254"],["KG","996"],["KH","855"],["KI","686"],["KM","269"],["KN","1"],["KP","850"],["KR","82"],["KW","965"],["KY","1"],["KZ","7"],["LA","856"],["LB","961"],["LC","1"],["LI","423"],["LK","94"],["LR","231"],["LS","266"],["LT","370"],["LU","352"],["LV","371"],["LY","218"],["MA","212"],["MC","377"],["MD","373"],["ME","382"],["MF","590"],["MG","261"],["MH","692"],["MK","389"],["ML","223"],["MM","95"],["MN","976"],["MO","853"],["MP","1"],["MQ","596"],["MR","222"],["MS","1"],["MT","356"],["MU","230"],["MV","960"],["MW","265"],["MX","52"],["MY","60"],["MZ","258"],["NA","264"],["NC","687"],["NE","227"],["NF","672"],["NG","234"],["NI","505"],["NL","31"],["NO","47"],["NP","977"],["NR","674"],["NU","683"],["NZ","64"],["OM","968"],["PA","507"],["PE","51"],["PF","689"],["PG","675"],["PH","63"],["PK","92"],["PL","48"],["PM","508"],["PR","1"],["PS","970"],["PT","351"],["PW","680"],["PY","595"],["QA","974"],["RE","262"],["RO","40"],["RS","381"],["RU","7"],["RW","250"],["SA","966"],["SB","677"],["SC","248"],["SD","249"],["SE","46"],["SG","65"],["SH","290"],["SI","386"],["SJ","47"],["SK","421"],["SL","232"],["SM","378"],["SN","221"],["SO","252"],["SR","597"],["SS","211"],["ST","239"],["SV","503"],["SX","1"],["SY","963"],["SZ","268"],["TA","290"],["TC","1"],["TD","235"],["TG","228"],["TH","66"],["TJ","992"],["TK","690"],["TL","670"],["TM","993"],["TN","216"],["TO","676"],["TR","90"],["TT","1"],["TV","688"],["TW","886"],["TZ","255"],["UA","380"],["UG","256"],["US","1"],["UY","598"],["UZ","998"],["VA","39"],["VC","1"],["VE","58"],["VG","1"],["VI","1"],["VN","84"],["VU","678"],["WF","681"],["WS","685"],["XK","383"],["YE","967"],["YT","262"],["ZA","27"],["ZM","260"],["ZW","263"]]; // Source: src/features/org-chart/view.js function createOrgChartView({ environment, tableSorting, model, loadLayout, icon, searchableSelect, request, requestQueryAccountContact, requestPersistContact, requestOpportunitiesByContact, getPrincipal, modalId, notify }) { const { document } = environment; let active = null; const nodeWidth = 236, nodeHeight = 146; function el(tag, className = "", text) { const element = document.createElement(tag); element.className = className; if (text !== undefined) element.textContent = text; return element; } function button(label, symbol, action, text = false) { const item = el("button", "oe-org-button"); item.type = "button"; item.title = label; item.setAttribute("aria-label", label); item.append(icon(symbol)); if (text) item.append(el("span", "", label)); item.addEventListener("click", action); return item; } function createButton(opportunity) { const item = button("View customer organization chart", "chart", () => open(opportunity, item)); item.className = "opportunities-extension-action-button"; item.setAttribute("aria-haspopup", "dialog"); return item; } function close(force = false) { if (!active) return false; const entry = active; if (entry.saving) return true; if (!force && entry.hasChanges()) { entry.confirm("Discard unsaved changes?", "Your organization chart has unsaved changes.", "Discard changes", () => close(true)); return true; } entry.contactCleanup?.(); active = null; entry.edit?.nameCombo.destroy(); entry.observer?.disconnect(); entry.modal.remove(); entry.siblings.forEach(([element, inert]) => { element.inert = inert; }); if (entry.trigger?.isConnected) entry.trigger.focus(); return true; } function open(opportunity, trigger) { if (active) return; const host = document.getElementById(modalId); if (!host || host.hidden) return; const customer = opportunity.CustomerAccount || {}; const customerId = String(customer.PartyId || customer.PartyNumber || ""), principal = getPrincipal(); const modal = el("section", "opportunities-extension-action-form-modal oe-org-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "oe-org-title"); modal.setAttribute("lang", "en"); const dialog = el("div", "opportunities-extension-action-form-dialog oe-org-dialog"); const header = el("header", "opportunities-extension-account-plans-header"), heading = el("div", "oe-org-heading"); const title = el("h2", "", "Organization chart"); title.id = "oe-org-title"; heading.append(title, el("p", "", customer.PartyUniqueName || "Customer")); const dismiss = button("Close organization chart", "close", () => close()); dismiss.className = "opportunities-extension-icon-button"; header.append(heading, dismiss); const toolbar = el("div", "oe-org-toolbar"), tools = el("div", "oe-org-tools"), count = el("span", "oe-org-count"); const add = button("Add person", "plus", () => guardEditor(() => editPerson(null)), true); const expand = button("Expand all", "expand", () => foldAll(false)), collapse = button("Collapse all", "collapse", () => foldAll(true)); const zoomOut = button("Zoom out", "minus", () => setZoom(entry.zoom - .1)), zoomIn = button("Zoom in", "plus", () => setZoom(entry.zoom + .1)); const zoomValue = el("output", "oe-org-zoom", "100%"); zoomValue.setAttribute("aria-label", "Zoom level"); const fitButton = button("Fit chart to view", "fit", fit); tools.append(expand, collapse, zoomOut, zoomValue, zoomIn, fitButton); toolbar.append(add, count, tools); const workspace = el("div", "oe-org-workspace"), viewport = el("div", "oe-org-viewport"); viewport.tabIndex = 0; viewport.setAttribute("role", "region"); viewport.setAttribute("aria-label", "Organization chart canvas"); const sizing = el("div", "oe-org-sizing"), canvas = el("div", "oe-org-canvas"); sizing.append(canvas); viewport.append(sizing); const empty = el("div", "oe-org-empty"); empty.append(icon("chart"), el("h3", "", "No people yet"), button("Add first person", "plus", () => editPerson(null), true)); viewport.append(empty); const editor = el("aside", "oe-org-editor"); editor.hidden = true; editor.setAttribute("aria-label", "Person details"); workspace.append(viewport, editor); const status = el("p", "oe-org-status", "Loading organization chart..."); status.setAttribute("role", "status"); const retry = button("Retry loading", "plus", load, true); retry.hidden = true; const footer = el("footer", "oe-org-footer"), save = button("Save chart", "save", saveChart, true); save.classList.add("oe-org-primary"); footer.append(status, retry, save); dialog.append(header, toolbar, workspace, footer); modal.append(dialog); const tooltip = el("div", "oe-org-tooltip"); tooltip.hidden = true; tooltip.id = "oe-org-notes-tooltip"; tooltip.setAttribute("role", "tooltip"); modal.append(tooltip); const siblings = Array.from(host.children).map(element => [element, element.inert]); siblings.forEach(([element]) => { element.inert = true; }); host.append(modal); const entry = { modal, trigger, siblings, nodes: [], baseline: "[]", revision: 0, ready: false, saving: false, zoom: 1, autoFit: true, width: 1, height: 1, selected: null, edit: null, confirmation: null, contacts: [], details: new Map() }; entry.hasChanges = () => JSON.stringify(entry.nodes) !== entry.baseline || editorDirty(); entry.confirm = confirm; active = entry; function message(text, error = false) { status.textContent = text; status.setAttribute("role", error ? "alert" : "status"); status.classList.toggle("oe-org-error", error); } function updateControls() { const blocked = !entry.ready || entry.saving; for (const control of [add, expand, collapse, zoomOut, zoomIn, fitButton]) control.disabled = blocked || (control !== add && !entry.nodes.length); save.disabled = blocked || !entry.hasChanges(); dismiss.disabled = entry.saving; toolbar.inert = workspace.inert = entry.saving; } function changed() { message("Unsaved changes"); updateControls(); } function editorValue() { if (!entry.edit) return null; const { fields, person } = entry.edit; const contact = entry.contacts.find(item => item.PartyId === fields.name.value); return { ...person, ...(contact ? { contactPartyId: contact.PartyId, PartyNumber: contact.PartyNumber || (contact.PartyId === person.contactPartyId ? person.PartyNumber : undefined) } : {}), name: contact ? contact.PartyName : fields.name.value === "__saved" ? person.name : "", jobTitle: contact ? (typeof contact.JobTitle === "string" ? contact.JobTitle.trim() : "") : fields.name.value === "__saved" ? person.jobTitle || "" : "", notes: fields.notes.value, parentId: fields.parent.value === "__root" ? null : fields.parent.value }; } function editorDirty() { return !!entry.edit && JSON.stringify(editorValue()) !== entry.edit.baseline; } function guardEditor(action) { if (entry.saving) return; if (editorDirty()) confirm("Discard person edits?", "This person has unapplied changes.", "Discard edits", () => { entry.edit?.nameCombo.destroy(); entry.edit = null; editor.hidden = true; action(); }); else action(); } function confirm(titleText, bodyText, label, action) { if (entry.confirmation) return; const overlay = el("div", "oe-org-confirm"), panel = el("section", "oe-org-confirm-panel"); panel.setAttribute("role", "alertdialog"); panel.setAttribute("aria-modal", "true"); panel.setAttribute("aria-labelledby", "oe-org-confirm-title"); panel.setAttribute("aria-describedby", "oe-org-confirm-body"); const heading = el("h3", "", titleText); heading.id = "oe-org-confirm-title"; const body = el("p", "", bodyText); body.id = "oe-org-confirm-body"; const actions = el("div", "oe-org-form-actions"), previous = document.activeElement; function finish() { entry.confirmation = null; overlay.remove(); dialog.inert = false; previous?.focus(); } const cancel = button("Keep editing", "close", finish, true), proceed = button(label, "check", () => { finish(); action(); }, true); actions.append(cancel, proceed); panel.append(heading, body, actions); overlay.append(panel); modal.append(overlay); entry.confirmation = overlay; dialog.inert = true; tooltip.hidden = true; overlay.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); finish(); } if (event.key === "Tab") { event.preventDefault(); (document.activeElement === cancel ? proceed : cancel).focus(); } }); cancel.focus(); } function discardEditor() { entry.edit?.nameCombo.destroy(); entry.edit = null; editor.hidden = true; entry.selected = null; render(); updateControls(); } function deletePerson(person) { confirm("Delete person?", "Delete " + person.name + "? Direct reports will move to this person's manager.", "Delete person", () => { entry.nodes = model.remove(entry.nodes, person.id); entry.edit?.nameCombo.destroy(); entry.edit = null; entry.selected = null; editor.hidden = true; changed(); render(); add.focus(); }); } function showContactOpportunities(person, trigger) { if (entry.confirmation) return; person = entry.nodes.find(node => node.id === person.id) || person; const overlay = el("div", "oe-org-confirm"), panel = el("section", "opportunities-extension-action-form-dialog oe-org-dialog oe-contact-opportunities"); panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "true"); panel.setAttribute("aria-labelledby", "oe-contact-opportunities-title"); const header = el("header", "oe-org-editor-header"), title = el("h3", "", "Opportunities | " + person.name); title.id = "oe-contact-opportunities-title"; let closed = false, offset = 0, partyNumber = person.PartyNumber; const current = () => !closed && active === entry; function finish() { closed = true; overlay.remove(); entry.confirmation = null; entry.contactCleanup = null; dialog.inert = false; trigger.focus(); } header.append(title, button("Close opportunities", "close", finish)); const status = el("p", "oe-org-status"), surface = el("div", "oe-contact-opportunities-table"), table = el("table"), head = el("thead"), row = el("tr"), body = el("tbody"); status.setAttribute("role", "status"); table.setAttribute("data-table-layout", "standard"); const columns = [ { key: "OptyNumber", label: "OptyNumber" }, { key: "WinProb", label: "Win Probability", type: "number" }, { key: "Name", label: "Name" }, { key: "CurrencyCode", label: "CurrencyCode" }, { key: "Revenue", label: "Amount", type: "number" }, { key: "EffectiveDate", label: "Close date", type: "date" }, { key: "StatusCode", label: "StatusCode" } ]; const sorting = tableSorting.create("contact-opportunities", columns); let allItems = [], loading = false; for (const column of columns) { const th = el("th"); th.scope = "col"; if (column.key === "Revenue") th.setAttribute("data-align", "money"); sorting.header(th, column.key, column.label, renderRows); row.append(th); } head.append(row); table.append(head, body); surface.append(table); const footer = el("footer", "oe-org-form-actions"); const retry = button("Retry", "plus", () => load(), true); footer.append(retry); panel.append(header, status, surface, footer); overlay.append(panel); modal.append(overlay); entry.confirmation = overlay; entry.contactCleanup = finish; dialog.inert = true; tooltip.hidden = true; overlay.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); finish(); } if (event.key === "Tab") { const controls = [...panel.querySelectorAll("button:not(:disabled), a")].filter(item => item.getClientRects().length); const first = controls[0], last = controls.at(-1); if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } }); function renderRows() { body.replaceChildren(); table.setAttribute("aria-busy", String(loading)); for (const button of head.querySelectorAll("button")) button.disabled = loading; if (loading) { for (let index = 0; index < 5; index++) { const row = el("tr"); row.setAttribute("aria-hidden", "true"); for (const column of columns) { const cell = el("td"), bar = el("span", "oe-contact-skeleton"); if (column.key === "Revenue") cell.setAttribute("data-align", "money"); bar.style.width = column.key === "Name" ? "90%" : column.key === "WinProb" || column.key === "StatusCode" ? "45%" : "70%"; cell.append(bar); row.append(cell); } body.append(row); } return; } for (const item of sorting.select(allItems)) { const row = el("tr"); for (const key of ["OptyNumber", "WinProb", "Name", "CurrencyCode", "Revenue", "EffectiveDate", "StatusCode"]) { const cell = el("td"); if (key === "Revenue") cell.setAttribute("data-align", "money"); let value = item[key] ?? "—"; if (key === "Name" && item.OptyNumber) { const link = el("a", "", String(value)); link.href = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid=" + encodeURIComponent(item.OptyNumber); link.target = "_blank"; link.rel = "noopener noreferrer"; cell.append(link); } else { if (key === "WinProb" && item[key] != null) value += "%"; if (key === "Revenue" && item[key] != null && Number.isFinite(Number(value))) value = Number(value).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); if (key === "EffectiveDate" && /^\d{4}-\d{2}-\d{2}/.test(String(value))) value = String(value).slice(0, 10).split("-").reverse().join("/"); cell.textContent = String(value); } row.append(cell); } body.append(row); } } async function load() { offset = 0; retry.hidden = true; status.classList.remove("oe-org-error"); status.textContent = ""; status.setAttribute("aria-label", "Loading opportunities"); const totalSkeleton = el("span", "oe-contact-skeleton oe-contact-skeleton-total"); totalSkeleton.setAttribute("aria-hidden", "true"); status.append(totalSkeleton); let loaded = 0; allItems = []; loading = true; renderRows(); try { while (current()) { const data = await requestOpportunitiesByContact(partyNumber, offset, { contactPartyId: person.contactPartyId, contactName: person.name, isActive: current, async resolvePartyNumber() { // Read the saved person, never replace a persisted identifier with a search result. const chart = await request("load", { principal, customerId }); if (!current()) return; const saved = chart.nodes.find(node => node.id === person.id && node.contactPartyId === person.contactPartyId); partyNumber = saved?.PartyNumber; if (!partyNumber) throw new Error("This person has no PartyNumber saved in the organization chart. Open Person details to retrieve and save it before viewing opportunities."); const currentPerson = entry.nodes.find(node => node.id === person.id); if (currentPerson) entry.nodes = model.upsert(entry.nodes, { ...currentPerson, PartyNumber: partyNumber }); return partyNumber; } }); if (!current()) return; allItems.push(...data.items); loaded += data.items.length; if (!data.hasMore) { status.textContent = loaded ? `${loaded} opportunities` : "No opportunities found."; break; } const count = Number(data.count ?? data.items.length), responseOffset = Number(data.offset ?? offset); const nextOffset = responseOffset + count; if (!data.items.length || !Number.isSafeInteger(nextOffset) || count <= 0 || nextOffset <= offset) throw new Error("The API returned hasMore without advancing the page. Please retry."); offset = nextOffset; status.setAttribute("aria-label", `Loading opportunities: ${loaded} loaded`); } } catch (error) { if (current()) { status.textContent = error.message; status.classList.add("oe-org-error"); retry.hidden = false; } } finally { if (current()) { loading = false; status.removeAttribute("aria-label"); renderRows(); } } } header.querySelector("button").focus(); load(); } function createContact(trigger) { if (entry.confirmation) return; const overlay = el("div", "oe-org-confirm"), panel = el("section", "opportunities-extension-action-form-dialog oe-org-dialog oe-contact-dialog"); panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "true"); panel.setAttribute("aria-labelledby", "oe-contact-title"); const header = el("header", "oe-org-editor-header"), title = el("h3", "", "Create Contact"); title.id = "oe-contact-title"; const form = el("form", "oe-contact-form"), combos = []; form.noValidate = true; let validationShown = false; let saving = false; function finish() { if (saving) return; combos.forEach(combo => combo.destroy()); overlay.remove(); entry.confirmation = null; entry.contactCleanup = null; dialog.inert = false; trigger.focus(); } header.append(title, button("Close Create Contact", "close", finish)); for (const [label, type, autocomplete] of [["First Name", "text", "given-name"], ["Last Name", "text", "family-name"], ["Job Title", "text", "organization-title"], ["Email Address", "email", "email"]]) { const field = el("label", "oe-org-field", label), input = el("input"); input.type = type; input.autocomplete = autocomplete; input.name = autocomplete; if (["given-name", "family-name", "email"].includes(autocomplete)) { input.required = true; field.append(document.createTextNode(" (required)")); } else field.append(document.createTextNode(" (optional)")); input.setAttribute("aria-label", label); field.append(input); form.append(field); } function consent(parent, name, label, verb) { const group = el("fieldset", "oe-contact-primary"); group.append(el("legend", "", label)); for (const [value, text] of [["none", "None"], ["deny", "Do Not " + verb], ["allow", "Ok to " + verb]]) { const field = el("label"), radio = el("input"); radio.type = "radio"; radio.name = name; radio.value = value; radio.checked = value === "none"; field.append(radio, document.createTextNode(text)); group.append(field); } parent.append(group); } consent(form, "emailConsent", "Email preference", "Email"); const names = new Intl.DisplayNames(["en"], { type: "region" }); const countries = orgChartCallingCodes.map(([code, dial]) => ({ code, dial, name: names.of(code) })).sort((a, b) => a.name.localeCompare(b.name, "en")); for (const label of ["Work Phone", "Mobile Phone"]) { const group = el("fieldset", "oe-contact-phone"), legend = el("legend", "", label); const countryField = el("label", "oe-org-field", "Country code"), select = el("select"); const placeholder = el("option", "", "Select country code"); placeholder.value = ""; select.append(placeholder); for (const country of countries) { const option = el("option", "", country.code + " · " + country.name + " (+" + country.dial + ")"); option.value = country.code; option.dataset.callingCode = country.dial; select.append(option); } select.name = label === "Work Phone" ? "workCountry" : "mobileCountry"; select.id = "oe-contact-" + select.name; countryField.append(select); const numberField = el("label", "oe-org-field", "Phone number"), input = el("input"); input.type = "tel"; input.autocomplete = "tel-national"; input.name = label === "Work Phone" ? "workPhone" : "mobilePhone"; input.setAttribute("aria-label", label); numberField.append(input); group.append(legend, countryField, numberField); form.append(group); if (label === "Work Phone") { const extension = el("label", "oe-org-field", "Extension (optional)"), input = el("input"); input.name = "workExtension"; input.type = "tel"; extension.append(input); group.append(extension); } consent(group, label === "Work Phone" ? "workConsent" : "mobileConsent", "Contact preference", "Call"); function validatePhone() { input.setCustomValidity(select.value && !input.value.trim() ? "Enter the phone number." : ""); combo.input.setCustomValidity(input.value.trim() && !select.value ? "Select a country code." : ""); } const combo = searchableSelect(select, countryField, validatePhone, { floating: true, listWidth: 390, listMaxHeight: 260 }); input.addEventListener("input", validatePhone); combo.input.setAttribute("aria-label", label + " country code"); combos.push(combo); } const result = el("p", "oe-org-status"); result.setAttribute("role", "status"); result.id = "oe-contact-validation"; function validateContact(focus = false) { const invalid = []; for (const input of form.querySelectorAll("input:not([type=radio])")) { const valid = input.validity.valid && (!input.required || !!input.value.trim()); input.setAttribute("aria-invalid", String(!valid)); if (!valid) { input.setAttribute("aria-describedby", result.id); invalid.push(input); } else input.removeAttribute("aria-describedby"); } result.classList.toggle("oe-org-error", invalid.length > 0); result.setAttribute("role", invalid.length ? "alert" : "status"); result.textContent = invalid.length ? "Complete the highlighted required fields with valid values." : ""; if (focus) invalid[0]?.focus(); return !invalid.length; } form.addEventListener("input", () => { if (validationShown && !saving) validateContact(); }); form.addEventListener("change", () => { if (validationShown && !saving) validateContact(); }); const actions = el("footer", "oe-org-form-actions"), saveContact = button("Save", "save", () => form.requestSubmit(), true); saveContact.classList.add("oe-org-primary"); actions.append(button("Cancel", "close", finish, true), saveContact); form.addEventListener("submit", async event => { event.preventDefault(); if (saving) return; validationShown = true; if (!validateContact(true)) return; const value = name => form.elements.namedItem(name)?.value || ""; const country = name => form.elements.namedItem(name).selectedOptions[0]?.dataset.callingCode || ""; const values = { firstName: value("given-name"), lastName: value("family-name"), jobTitle: value("organization-title"), email: value("email"), emailConsent: value("emailConsent"), workConsent: value("workConsent"), mobileConsent: value("mobileConsent"), workPhone: value("workPhone"), mobilePhone: value("mobilePhone"), workCountry: country("workCountry"), mobileCountry: country("mobileCountry"), workExtension: value("workExtension") }; saving = true; saveContact.disabled = true; form.inert = true; result.classList.remove("oe-org-error"); result.textContent = "Saving contact…"; try { const data = await requestPersistContact(customer, values); saving = false; if (active !== entry) return; finish(); notify("Contact created successfully."); if (entry.edit && data?.PartyId != null) { const contact = { PartyId: String(data.PartyId), PartyNumber: data.PartyNumber, PartyName: data.PartyName || [values.firstName, values.lastName].join(" "), JobTitle: data.JobTitle ?? values.jobTitle, PrimaryEmail: { EmailAddress: data.EmailAddress ?? values.email } }; entry.contacts.push(contact); const option = el("option", "", contact.PartyName); option.value = contact.PartyId; entry.edit.fields.name.append(option); entry.edit.fields.name.value = contact.PartyId; entry.edit.nameCombo.sync(); showContactDetails(entry.edit, contact); updateControls(); } } catch (error) { if (active === entry) { result.textContent = error.message || "Unable to create contact."; result.classList.add("oe-org-error"); notify(result.textContent, true); } } finally { saving = false; saveContact.disabled = false; form.inert = false; } }); panel.append(header, form, result, actions); overlay.append(panel); modal.append(overlay); entry.confirmation = overlay; entry.contactCleanup = finish; dialog.inert = true; tooltip.hidden = true; overlay.addEventListener("keydown", event => { if (event.key === "Escape" && !event.defaultPrevented) { event.preventDefault(); event.stopPropagation(); finish(); } if (event.key === "Tab") { const controls = [...panel.querySelectorAll("button, input")].filter(item => !item.disabled && item.getClientRects().length); const first = controls[0], last = controls.at(-1); if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } }); form.querySelector("input").focus(); } function editPerson(person, parentId = null) { entry.edit?.nameCombo.destroy(); tooltip.hidden = true; const existing = person || { id: environment.crypto.randomUUID(), parentId, name: "", notes: "", collapsed: false }; entry.selected = person?.id || null; editor.replaceChildren(); editor.hidden = false; const editorHeader = el("div", "oe-org-editor-header"); editorHeader.append(el("h3", "", person ? "Person details" : "Add person"), button("Close person details", "close", () => guardEditor(discardEditor))); const form = el("form", "oe-org-form"), fields = {}; const nameField = el("label", "oe-org-field", "Name *"), nameSelect = el("select"); nameSelect.id = "oe-org-name"; nameSelect.required = true; fields.name = nameSelect; nameField.append(nameSelect); form.append(nameField); const placeholder = el("option", "", "Type to search contacts"); placeholder.value = ""; nameSelect.append(placeholder); if (existing.name) { const saved = el("option", "", existing.name); saved.value = "__saved"; nameSelect.append(saved); nameSelect.value = "__saved"; } const nameCombo = searchableSelect(nameSelect, nameField, () => { nameCombo.input.setCustomValidity(""); entry.edit.detailsVersion++; const contact = entry.contacts.find(item => item.PartyId === fields.name.value); showContactDetails(entry.edit, contact); updateControls(); }, { floating: true, listMaxHeight: 280, searchDelay: 350, async search(keywords, isCurrent) { const contacts = await requestQueryAccountContact(customer.PartyId, keywords, () => active === entry && isCurrent()); if (!isCurrent() || active !== entry) return []; entry.contacts = contacts; return contacts.map(contact => ({ value: contact.PartyId, label: contact.PartyName, description: contact.JobTitle || "" })); } }); nameCombo.input.setAttribute("aria-label", "Name"); const create = button("Create Contact", "contact", event => { event.preventDefault(); createContact(create); }); create.setAttribute("aria-haspopup", "dialog"); const wrapper = nameCombo.input.parentElement; wrapper.classList.add("oe-contact-name"); wrapper.append(create); for (const [key, label, max] of [["role", "Job Title", 160], ["emailAddress", "Email address", 320], ["notes", "Notes", 4000]]) { const field = el("label", "oe-org-field", label + (key === "notes" ? " (optional)" : " (from contact)")); const input = el(key === "notes" ? "textarea" : "input"); input.id = "oe-org-" + key; input.value = (key === "role" ? existing.jobTitle : existing[key]) || ""; input.maxLength = max; input.setAttribute("aria-label", label); if (key === "notes") input.rows = 7; else { input.readOnly = true; input.placeholder = "Not provided"; } field.append(input); fields[key] = input; form.append(field); input.addEventListener("input", () => { input.setCustomValidity(""); updateControls(); }); } const detailStatus = el("p", "oe-org-status"); detailStatus.setAttribute("role", "status"); const detailRetry = button("Retry contact details", "plus", () => loadPersonDetails(entry.edit), true); detailRetry.hidden = true; form.append(detailStatus, detailRetry); const reports = el("label", "oe-org-field", "Reports to"), select = el("select"); select.id = "oe-org-parent"; const top = el("option", "", "No manager (top level)"); top.value = "__root"; select.append(top); const excluded = person ? model.descendants(entry.nodes, person.id) : new Set(); for (const candidate of entry.nodes.filter(item => !excluded.has(item.id))) { const title = candidate.jobTitle; const option = el("option", "", candidate.name + (title ? " - " + title : "")); option.value = candidate.id; select.append(option); } select.value = existing.parentId || "__root"; fields.parent = select; reports.append(select); form.append(reports); const combo = searchableSelect(select, reports, updateControls, { floating: true }); combo.input.setAttribute("aria-label", "Reports to"); const actions = el("div", "oe-org-form-actions"); const apply = button(person ? "Apply changes" : "Add person", "check", () => {}, true); apply.type = "submit"; apply.classList.add("oe-org-primary"); actions.append(button("Cancel", "close", discardEditor, true), apply); if (person) { const remove = button("Delete person", "trash", () => deletePerson(person), true); remove.classList.add("oe-org-danger"); form.append(remove); } form.append(actions); editor.append(editorHeader, form); entry.edit = { person: existing, fields, form, combo, nameCombo, baseline: "", detailsVersion: 0, detailStatus, detailRetry }; entry.edit.baseline = JSON.stringify(editorValue()); form.addEventListener("submit", event => { event.preventDefault(); if (applyEditor()) { render(); focusPerson(entry.selected); } }); render(); updateControls(); if (!nameCombo.input.disabled) nameCombo.input.focus(); else fields.role.focus(); if (person) loadPersonDetails(entry.edit); } function showContactDetails(edit, contact) { edit.fields.role.value = typeof contact?.JobTitle === "string" ? contact.JobTitle : ""; edit.fields.emailAddress.value = typeof contact?.PrimaryEmail?.EmailAddress === "string" ? contact.PrimaryEmail.EmailAddress : ""; for (const key of ["role", "emailAddress"]) edit.fields[key].placeholder = "Not provided"; edit.detailStatus.textContent = ""; edit.detailRetry.hidden = true; if (contact) entry.details.set(String(contact.PartyId), contact); render(); } async function persistPartyNumber(person, value) { const number = String(value).trim(); if (!/^[A-Za-z0-9]{1,100}$/.test(number)) return; const current = entry.nodes.find(node => node.id === person.id && node.contactPartyId === person.contactPartyId); if (!current) return; entry.nodes = model.upsert(entry.nodes, { ...current, PartyNumber: number }); if (entry.edit?.person.id === person.id) { entry.edit.person = { ...entry.edit.person, PartyNumber: number }; entry.edit.baseline = JSON.stringify({ ...JSON.parse(entry.edit.baseline), PartyNumber: number }); } const savedNodes = JSON.parse(entry.baseline); const saved = savedNodes.find(node => node.id === person.id && node.contactPartyId === person.contactPartyId); if (!saved || saved.PartyNumber === number || entry.saving) { updateControls(); return; } saved.PartyNumber = number; entry.saving = true; updateControls(); try { const chart = await request("save", { principal, customerId, nodes: savedNodes, revision: entry.revision }); if (active !== entry) return; entry.revision = chart.revision; entry.baseline = JSON.stringify(chart.nodes); } finally { if (active === entry) { entry.saving = false; updateControls(); } } } async function loadPersonDetails(edit) { if (!edit) return; const version = ++edit.detailsVersion; const isCurrent = () => active === entry && entry.edit === edit && edit.detailsVersion === version; entry.details.delete(edit.person.contactPartyId); showContactDetails(edit, null); if (!edit.person.contactPartyId) { edit.detailStatus.textContent = "Select a contact to load Job Title and email address."; return; } edit.detailStatus.textContent = "Loading contact details..."; for (const key of ["role", "emailAddress"]) edit.fields[key].placeholder = "Loading..."; try { let contacts = await requestQueryAccountContact(customer.PartyId, edit.person.name, isCurrent); if (isCurrent() && !edit.person.PartyNumber && !contacts.find(item => String(item.PartyId) === edit.person.contactPartyId)?.PartyNumber) { contacts = await requestQueryAccountContact(customer.PartyId, edit.person.name, isCurrent, edit.person.contactPartyId); } if (!isCurrent()) return; const contact = contacts.find(item => String(item.PartyId) === edit.person.contactPartyId); if (contact) { const current = entry.nodes.find(node => node.id === edit.person.id); const jobTitle = typeof contact.JobTitle === "string" ? contact.JobTitle.trim() : ""; if (current && (current.name !== contact.PartyName.trim() || current.jobTitle !== jobTitle)) { entry.nodes = model.upsert(entry.nodes, { ...current, name: contact.PartyName, jobTitle }); edit.person = entry.nodes.find(node => node.id === current.id); const baseline = JSON.parse(edit.baseline); edit.baseline = JSON.stringify({ ...baseline, name: edit.person.name, jobTitle }); edit.fields.name.querySelector('option[value="__saved"]').textContent = edit.person.name; edit.nameCombo.sync(); changed(); } } showContactDetails(edit, contact); if (contact?.PartyNumber) await persistPartyNumber(edit.person, contact.PartyNumber); if (!contact) edit.detailStatus.textContent = "Contact details are unavailable. Select a contact to update this person."; } catch (error) { if (!isCurrent()) return; showContactDetails(edit, null); edit.detailStatus.textContent = error.message || "Unable to load contact details."; edit.detailRetry.hidden = false; } } function applyEditor() { if (!entry.edit) return true; const { form, fields } = entry.edit; if (!editorValue().name) { message("Select a contact in Name.", true); entry.edit.nameCombo.input.focus(); return false; } if (!form.reportValidity()) return false; try { const person = editorValue(); person.collapsed = entry.nodes.find(node => node.id === person.id)?.collapsed ?? person.collapsed; entry.nodes = model.upsert(entry.nodes, person); if (person.parentId) entry.nodes = entry.nodes.map(node => node.id === person.parentId ? { ...node, collapsed: false } : node); entry.edit.nameCombo.destroy(); entry.edit = null; editor.hidden = true; entry.selected = person.id; changed(); return true; } catch (error) { message(error.message, true); return false; } } function focusPerson(id) { canvas.querySelector('[data-person-id="' + id + '"] .oe-org-person')?.focus(); } function showNotes(person, anchor) { const card = anchor.closest(".oe-org-node"); if (!person.notes.trim() || entry.saving || active !== entry || !card?.isConnected) return; tooltip.textContent = person.notes; tooltip.hidden = false; const box = card.getBoundingClientRect(), rect = tooltip.getBoundingClientRect(); const width = document.documentElement.clientWidth, height = document.documentElement.clientHeight; tooltip.style.left = Math.max(12, Math.min(box.left, width - rect.width - 12)) + "px"; tooltip.style.top = Math.max(12, box.bottom + rect.height + 8 > height ? box.top - rect.height - 8 : box.bottom + 8) + "px"; } function render() { tooltip.hidden = true; canvas.replaceChildren(); const visible = entry.ready && entry.nodes.length; empty.hidden = !entry.ready || !!visible; sizing.hidden = !visible; count.textContent = entry.nodes.length + (entry.nodes.length === 1 ? " person" : " people"); if (!visible) return; const d3 = loadLayout(), root = d3.hierarchy(model.hierarchy(entry.nodes)); d3.tree().nodeSize([nodeWidth + 28, nodeHeight + 54]).separation(() => 1)(root); const nodes = root.descendants().filter(node => node.depth > 0), left = Math.min(...nodes.map(node => node.x)) - nodeWidth / 2 - 28; entry.width = Math.max(...nodes.map(node => node.x)) - left + nodeWidth / 2 + 28; entry.height = Math.max(...nodes.map(node => (node.depth - 1) * (nodeHeight + 54))) + nodeHeight + 56; const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.classList.add("oe-org-lines"); svg.setAttribute("width", entry.width); svg.setAttribute("height", entry.height); svg.setAttribute("aria-hidden", "true"); canvas.append(svg); for (const node of nodes) { const x = node.x - left - nodeWidth / 2, y = (node.depth - 1) * (nodeHeight + 54) + 28; if (node.parent.depth > 0) { const px = node.parent.x - left, py = y - 54, path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", "M" + px + "," + py + " V" + (py + 27) + " H" + (x + nodeWidth / 2) + " V" + y); svg.append(path); } const person = node.data, card = el("article", "oe-org-node"); card.setAttribute("data-person-id", person.id); card.setAttribute("data-selected", String(entry.selected === person.id)); card.style.left = x + "px"; card.style.top = y + "px"; const main = button("Edit " + person.name, "chart", () => guardEditor(() => editPerson(entry.nodes.find(item => item.id === person.id)))); main.className = "oe-org-person"; main.removeAttribute("title"); main.replaceChildren(el("strong", "", person.name), el("span", "", person.jobTitle || "Job Title not provided")); card.addEventListener("mouseenter", () => showNotes(person, main)); card.addEventListener("mouseleave", () => { tooltip.hidden = true; }); main.addEventListener("focus", () => showNotes(person, main)); main.addEventListener("blur", () => { tooltip.hidden = true; }); if (person.notes.trim()) main.setAttribute("aria-describedby", tooltip.id); const actions = el("div", "oe-org-node-actions"); actions.append(button("Add report to " + person.name, "plus", () => guardEditor(() => editPerson(null, person.id)))); const remove = button("Delete " + person.name, "trash", () => guardEditor(() => deletePerson(person))); remove.classList.add("oe-org-danger"); actions.append(remove); const opportunities = button("View opportunities for " + person.name, "opportunities", () => showContactOpportunities(person, opportunities)); opportunities.setAttribute("aria-haspopup", "dialog"); actions.append(opportunities); if (person.notes.trim()) { const marker = el("span", "oe-org-note-marker"); marker.append(icon("note")); marker.title = "Has notes"; actions.append(marker); } const reports = entry.nodes.filter(item => item.parentId === person.id).length; if (reports) { const toggle = button((person.collapsed ? "Expand" : "Collapse") + " reports for " + person.name, person.collapsed ? "down" : "up", () => guardEditor(() => { entry.nodes = entry.nodes.map(item => item.id === person.id ? { ...item, collapsed: !item.collapsed } : item); changed(); render(); focusPerson(person.id); })); toggle.setAttribute("aria-expanded", String(!person.collapsed)); toggle.append(el("span", "", String(reports))); actions.append(toggle); } card.append(main, actions); canvas.append(card); } if (entry.autoFit) fit(); else setZoom(entry.zoom, true); } function setZoom(value, preserveFit = false) { if (!preserveFit) entry.autoFit = false; entry.zoom = Math.max(.25, Math.min(1.5, value)); canvas.style.width = entry.width + "px"; canvas.style.height = entry.height + "px"; canvas.style.transform = "scale(" + entry.zoom + ")"; sizing.style.width = entry.width * entry.zoom + "px"; sizing.style.height = entry.height * entry.zoom + "px"; zoomValue.textContent = Math.round(entry.zoom * 100) + "%"; } function fit() { if (!entry.nodes.length) return; entry.autoFit = true; setZoom(Math.min(1, (viewport.clientWidth - 24) / entry.width, (viewport.clientHeight - 24) / entry.height), true); viewport.scrollTo({ left: 0, top: 0 }); } function foldAll(collapsed) { guardEditor(() => { entry.nodes = entry.nodes.map(person => ({ ...person, collapsed })); changed(); render(); fit(); }); } async function load() { retry.hidden = true; entry.ready = false; updateControls(); modal.setAttribute("aria-busy", "true"); empty.hidden = true; canvas.replaceChildren(el("div", "oe-org-skeleton")); sizing.hidden = false; message("Loading organization chart..."); try { if (!customerId || !principal) throw new Error("The current user and customer must be available before opening an organization chart."); const chart = await request("load", { principal, customerId }); if (active !== entry) return; entry.nodes = model.normalize(chart.nodes); entry.baseline = JSON.stringify(entry.nodes); entry.revision = chart.revision; entry.ready = true; message(chart.updatedAt ? "Saved locally: " + new Date(chart.updatedAt).toLocaleString("en-US") : "Not saved yet"); render(); fit(); } catch (error) { if (active === entry) { canvas.replaceChildren(); message(error.message || "Unable to load organization chart.", true); retry.hidden = false; } } finally { if (active === entry) { modal.setAttribute("aria-busy", "false"); updateControls(); } } } async function saveChart() { if (!entry.ready || entry.saving) return; if (!applyEditor()) return; render(); entry.saving = true; updateControls(); message("Saving locally..."); try { const chart = await request("save", { principal, customerId, nodes: entry.nodes, revision: entry.revision }); if (active !== entry) return; entry.revision = chart.revision; entry.baseline = JSON.stringify(entry.nodes); message("Saved locally: " + new Date(chart.updatedAt).toLocaleString("en-US")); notify("Organization chart saved locally."); } catch (error) { if (active === entry) { message(error.message || "Unable to save organization chart.", true); notify("Organization chart could not be saved.", true); } } finally { if (active === entry) { entry.saving = false; updateControls(); } } } let drag = null; viewport.addEventListener("pointerdown", event => { if (event.button !== 0 || event.target.closest("button, input, textarea, .oe-org-node")) return; drag = { x: event.clientX, y: event.clientY, left: viewport.scrollLeft, top: viewport.scrollTop }; viewport.setPointerCapture(event.pointerId); }); viewport.addEventListener("pointermove", event => { if (drag) { viewport.scrollLeft = drag.left - event.clientX + drag.x; viewport.scrollTop = drag.top - event.clientY + drag.y; } }); viewport.addEventListener("pointerup", () => { drag = null; }); viewport.addEventListener("pointercancel", () => { drag = null; }); viewport.addEventListener("scroll", () => { tooltip.hidden = true; }); if (environment.window?.ResizeObserver) { entry.observer = new environment.window.ResizeObserver(() => { tooltip.hidden = true; if (entry.autoFit) fit(); }); entry.observer.observe(viewport); } modal.addEventListener("keydown", event => { if (entry.confirmation) return; if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); tooltip.hidden = true; close(); } if (event.key === "Tab") { const controls = [...modal.querySelectorAll("button, input, textarea, [tabindex='0']")].filter(item => !item.disabled && !item.closest("[hidden], [inert]") && item.getClientRects().length); const first = controls[0], last = controls.at(-1); if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); } } }); dismiss.focus(); load(); } return { createButton, open, close }; } // Source: src/services/fusion/account-contact.js function createFusionAccountContact({ environment, getToken, logRequestStart, logRequestSuccess, logRequestFailure, readResponseBodyForDebug }) { async function requestQueryAccountContact(accountId, keywords, isActive = () => true, contactPartyId = null) { if (!/^\d+$/.test(String(accountId)) || !Number.isSafeInteger(Number(accountId))) throw new Error("The customer's PartyId is unavailable."); if (contactPartyId != null && !/^\d+$/.test(String(contactPartyId))) throw new Error("The contact's PartyId is unavailable."); if ((!String(keywords || "").trim() && contactPartyId == null) || !isActive()) return []; const token = await getToken(); if (!isActive()) return []; const url = "https://eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/latest/custom-actions/queries"; const fields = ["PartyName", "PrimaryEmail.EmailAddress", "JobTitle", "PrimaryAccount.PartyName", "PartyId", "PartyNumber"]; const payload = { aggregationResults: false, skipInValidFields: true, skipHiddenFromUIFields: true, language: "en", onlyData: true, keywords, entity: "Contact", fields, keywordsFields: fields, q: { op: "$and", criteria: [{ op: "$or", innerQuery: true, criteria: [{ attribute: "PrimaryAccount.PartyId", op: "$eq", value: Number(accountId) }] }], innerQuery: true }, sort: [{ attribute: "_score", direction: "descending" }], limit: 15, offset: 0 }; if (contactPartyId != null) { payload.keywords = ""; payload.q.criteria.push({ attribute: "PartyId", op: "$eq", value: String(contactPartyId) }); } const headers = { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${token}`, Preference: "transient", Origin: "https://eeho.fa.us2.oraclecloud.com" }; const entry = logRequestStart({ name: "requestQueryAccountContact", method: "POST", url, headers, body: JSON.stringify(payload, null, 2) }); try { const response = await environment.fetch.call(environment.window, url, { method: "POST", credentials: "include", headers, body: JSON.stringify(payload) }); logRequestSuccess(entry, response, await readResponseBodyForDebug(response)); if (!response.ok) throw new Error(`requestQueryAccountContact failed with status ${response.status}.`); const data = await response.json(); if (!data || !Array.isArray(data.items)) throw new Error("requestQueryAccountContact returned an invalid response."); if (!isActive()) return []; const contacts = new Map(); for (const item of data.items) if (item?.PartyId != null && typeof item.PartyName === "string" && item.PartyName.trim()) { contacts.set(String(item.PartyId), { ...item, PartyId: String(item.PartyId) }); } return [...contacts.values()]; } catch (error) { logRequestFailure(entry, error.message); throw error; } } async function requestPersistContact(account, values) { if (!/^\d+$/.test(String(account?.PartyId)) || !String(account?.PartyNumber || "").trim()) throw new Error("The customer's PartyId or PartyNumber is unavailable."); const phone = (number, country) => { if (!number && !country) return ""; if (!number || !country) throw new Error("Select a country code and enter a phone number for each phone provided."); const digits = number.replace(/[\s().-]/g, ""); if (!/^\d+$/.test(digits)) throw new Error("Enter the phone number using digits only, without the country code."); return country + "-" + digits; }; const payload = { FirstName: values.firstName.trim(), LastName: values.lastName.trim(), AccountPartyId: String(account.PartyId), AccountPartyNumber: String(account.PartyNumber), JobTitle: values.jobTitle.trim(), Type: "ZCA_CONTACT", EmailAddress: values.email.trim(), RawWorkPhoneNumber: phone(values.workPhone.trim(), values.workCountry), WorkPhoneCountryCode: values.workCountry, WorkPhoneExtension: values.workExtension.trim(), MobileCountryCode: values.mobileCountry, RawMobileNumber: phone(values.mobilePhone.trim(), values.mobileCountry) }; if (!payload.FirstName || !payload.LastName || !payload.EmailAddress) throw new Error("First Name, Last Name and Email Address are required."); for (const key of Object.keys(payload)) if (payload[key] === "") delete payload[key]; for (const [choice, allow, deny] of [ [values.emailConsent, "DoContactEmailFlag", "DoNotContactEmailFlag"], [values.workConsent, "DoCallWorkPhoneFlag", "DoNotCallWorkPhoneFlag"], [values.mobileConsent, "DoCallMobilePhoneFlag", "DoNotCallMobileFlag"] ]) { if (choice === "allow" || choice === "deny") { payload[allow] = choice === "allow"; payload[deny] = choice === "deny"; } } const body = JSON.stringify(payload); const token = await getToken(); const url = "https://eeho.fa.us2.oraclecloud.com/crmRestApi/rest/rv:e75ce5f9-a69f-4c6a-8591-6b06a538829e/en/11.13.18.05:9/contacts?onlyData=true"; const headers = { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${token}`, Origin: "https://eeho.fa.us2.oraclecloud.com" }; const entry = logRequestStart({ name: "requestPersistContact", method: "POST", url, headers, body: JSON.stringify(payload, null, 2) }); try { const response = await environment.fetch.call(environment.window, url, { method: "POST", credentials: "include", headers, body }); logRequestSuccess(entry, response, await readResponseBodyForDebug(response)); if (!response.ok) throw new Error(`requestPersistContact failed with status ${response.status}.`); const text = await response.text(); try { return text ? JSON.parse(text) : null; } catch { return null; } } catch (error) { logRequestFailure(entry, error.message); throw error; } } async function requestOpportunitiesByContact(partyNumber, offset = 0, context = {}) { const baseUrl = "https://eeho.fa.us2.oraclecloud.com/crmRestApi/rest/rv:e75ce5f9-a69f-4c6a-8591-6b06a538829e/en/11.13.18.05:9/opportunities"; let entry; try { if (!String(partyNumber || "").trim() && context.resolvePartyNumber) partyNumber = await context.resolvePartyNumber(); partyNumber = String(partyNumber || "").trim(); if (!/^[A-Za-z0-9]{1,100}$/.test(partyNumber)) throw new Error("The person's PartyNumber is missing or invalid. The opportunities HTTP request was not sent."); if (context.isActive && !context.isActive()) throw new Error("Opportunities lookup cancelled because its dialog was closed. HTTP request was not sent."); const token = await getToken(); const fields = "SalesStageId,KeyContactId,EffectiveDate,WinProb,CurrencyCode,Revenue,SalesStage,Name,ManageRestrictedProfileDataFlag,OptyId,OptyNumber,ManageRevenueDataFlag,StatusCode"; const query = encodeURIComponent("(OpportunityContact.ContactPartyNumber='" + partyNumber + "')").replace(/[()']/g, character => "%" + character.charCodeAt(0).toString(16).toUpperCase()); const url = baseUrl + "?onlyData=true&q=" + query + "&totalResults=true&fields=" + encodeURIComponent(fields) + "&orderBy=LastUpdateDate%3Adesc&limit=5&offset=" + offset; const headers = { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${token}`, Preference: "transient", Origin: "https://eeho.fa.us2.oraclecloud.com" }; entry = logRequestStart({ name: "requestOpportunitiesByContact", method: "GET", url, headers, metadata: { contactPartyId: context.contactPartyId || "", contactName: context.contactName || "", partyNumber, offset, httpRequestSent: true } }); const response = await environment.fetch.call(environment.window, url, { method: "GET", credentials: "include", headers }); logRequestSuccess(entry, response, await readResponseBodyForDebug(response)); if (!response.ok) throw new Error(`requestOpportunitiesByContact failed with status ${response.status}.`); const data = await response.json(); if (!Array.isArray(data?.items)) throw new Error("requestOpportunitiesByContact returned an invalid response."); return data; } catch (error) { if (!entry) entry = logRequestStart({ name: "resolveContactForOpportunities", method: "LOCAL", url: "(HTTP request not sent)", metadata: { contactPartyId: context.contactPartyId || "", contactName: context.contactName || "", offset, httpRequestSent: false, targetRequest: "requestOpportunitiesByContact" } }); logRequestFailure(entry, error.message); throw error; } } return { requestQueryAccountContact, requestPersistContact, requestOpportunitiesByContact }; } // Source: src/features/preferences/table-sorting.js function createTableSorting({ environment }) { const memory = new Map(); const storageKey = id => `opportunities-extension-table-sort-v1-${id}`; function read(id, columns) { let saved = memory.get(id); if (!saved) { try { saved = JSON.parse(environment.localStorage?.getItem(storageKey(id)) || "null"); } catch { /* Storage is optional. */ } } return saved && columns.some(column => column.key === saved.key && column.sortable !== false) && ["ascending", "descending"].includes(saved.direction) ? { key: saved.key, direction: saved.direction } : { key: "", direction: "ascending" }; } function save(id, state) { memory.set(id, { ...state }); try { environment.localStorage?.setItem(storageKey(id), JSON.stringify(state)); } catch { /* Retain the page-session preference. */ } } function value(raw, type) { if (raw == null || String(raw).trim() === "") return null; if (type === "number") return Number.isFinite(Number(raw)) ? Number(raw) : null; if (type === "date") { const timestamp = Date.parse(raw); return Number.isFinite(timestamp) ? timestamp : null; } return String(raw); } function create(id, columns) { let state; const headers = new Map(); const get = () => state || (state = read(id, columns)); function toggle(key) { if (!columns.some(column => column.key === key && column.sortable !== false)) return; const previous = get(); state = { key, direction: previous.key === key && previous.direction === "ascending" ? "descending" : "ascending" }; save(id, state); headers.forEach(update); } function select(items) { const selected = get(), column = columns.find(column => column.key === selected.key); if (!column) return [...items]; const direction = selected.direction === "descending" ? -1 : 1; return items.map((item, index) => ({ item, index, value: value(column.value ? column.value(item) : item[column.key], column.type) })) .sort((a, b) => { if (a.value === null || b.value === null) return a.value === b.value ? a.index - b.index : a.value === null ? 1 : -1; const result = typeof a.value === "number" ? a.value - b.value : a.value.localeCompare(b.value, undefined, { numeric: true, sensitivity: "base" }); return result * direction || a.index - b.index; }).map(record => record.item); } function update({ th, button, key, label }) { const selected = get(); const direction = selected.key === key ? selected.direction : "none"; th.setAttribute("aria-sort", direction); button.setAttribute("data-sort-direction", direction); button.setAttribute("aria-label", `Sort by ${label}${direction === "none" ? "" : `, ${direction}`}`); } function header(th, key, label, onChange, disabled = false) { const button = environment.document.createElement("button"); button.type = "button"; button.className = "opportunities-extension-sort-button"; button.disabled = disabled; button.title = `Sort by ${label}`; button.setAttribute("data-sort-key", key); const text = environment.document.createElement("span"); text.className = "opportunities-extension-sort-label"; text.textContent = label; button.append(text); const record = { th, button, key, label }; headers.set(key, record); update(record); th.append(button); button.addEventListener("click", () => { toggle(key); onChange?.(key); }); return button; } return { get, toggle, select, header }; } return { read, save, create }; } // Source: src/shared/time-dates.js function createTimeDates() { const months = "JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC".split(" "); function parse(value) { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); if (!match) return null; const date = new Date(`${value}T00:00:00.000Z`); return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value ? date : null; } function fromApi(value) { const match = /^(\d{2})-([A-Z]{3})-(\d{4})$/.exec(String(value)); if (!match) return null; const month = months.indexOf(match[2]) + 1; return parse(`${match[3]}-${String(month).padStart(2, "0")}-${match[1]}`); } function toApi(value) { const date = parse(value); if (!date) throw new Error("Select a valid reference date."); return `${String(date.getUTCDate()).padStart(2, "0")}-${months[date.getUTCMonth()]}-${date.getUTCFullYear()}`; } function shift(value, days) { const date = parse(value); if (!date) return value; date.setUTCDate(date.getUTCDate() + days); return date.toISOString().slice(0, 10); } function today() { const date = new Date(); return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } return { parse, fromApi, toApi, shift, today }; } // Source: src/features/manage-time/view.js function createManageTime({ MODAL_ID, environment, tableSorting, searchableSelect, dates, requestWeek, requestSave, requestDelete, notify = () => {}, requestEntries = async () => [], requestServices = async () => [], requestExpand = async () => [], requestTaskTypes = async () => [] }) { const { document } = environment; let active = null; let ready = false; let launcher = null; const referenceDateKey = "opportunities-extension-manage-time-reference-date"; let lastReferenceDate = ""; function savedReferenceDate() { try { const saved = environment.localStorage?.getItem(referenceDateKey); if (dates.parse(saved)) lastReferenceDate = saved; } catch { /* Keep the in-memory selection when storage is unavailable. */ } return dates.parse(lastReferenceDate) ? lastReferenceDate : dates.today(); } function saveReferenceDate(value) { if (!dates.parse(value)) return; lastReferenceDate = value; try { environment.localStorage?.setItem(referenceDateKey, value); } catch { /* Keep the selection for this page even if persistence is blocked. */ } } function setReady(value) { ready = !!value; if (launcher) { launcher.disabled = !ready; launcher.title = ready ? "Manage Time" : "Waiting for SR session ready"; } } function node(tag, className = "", text) { const el = document.createElement(tag); el.className = className; if (text !== undefined) el.textContent = text; return el; } function icon(name) { const paths = { clock: "M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0ZM12 6v6l4 2", left: "m15 18-6-6 6-6", right: "m9 18 6-6-6-6", close: "m6 6 12 12M18 6 6 18", up: "m6 15 6-6 6 6", add: "M12 5v14M5 12h14", copy: "M9 9h12v12H9ZM5 15H3V3h12v2", notes: "M4 3h16v18H4ZM8 7h8M8 11h8M8 15h5", trash: "M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7", save: "M5 3h12l4 4v14H3V3ZM7 3v6h10V3M7 21v-8h10v8" }; const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", paths[name]); 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; } function button(label, symbol, onClick, text = false) { const el = node("button"); el.type = "button"; el.title = label; el.setAttribute("aria-label", label); el.append(icon(symbol)); if (text) el.append(node("span", "", label)); if (onClick) el.addEventListener("click", onClick); return el; } function createButton() { const trigger = button("Manage Time", "clock", () => open(trigger)); trigger.className = "opportunities-extension-icon-button"; launcher = trigger; setReady(ready); return trigger; } 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 open(trigger) { if (active?.saving) return; if (!ready) return; const host = document.getElementById(MODAL_ID); if (!host || host.hidden) return; close(false); const modal = node("section", "opportunities-extension-action-form-modal oe-time-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "oe-time-title"); const dialog = node("div", "opportunities-extension-action-form-dialog oe-time-dialog"); const header = node("header", "opportunities-extension-account-plans-header oe-time-header"); const title = node("h2", "", "Manage Time"); title.id = "oe-time-title"; const closeButton = button("Close Manage Time", "close", () => close()); closeButton.className = "opportunities-extension-icon-button"; header.append(title, closeButton); const body = node("div", "oe-time-body"); const toolbar = node("div", "oe-time-toolbar"); const navigation = node("div", "oe-time-navigation"); const date = node("input"); date.type = "date"; date.value = savedReferenceDate(); date.required = true; date.setAttribute("aria-label", "Reference date"); const ending = node("div", "oe-time-ending"); const endingValue = node("strong", "", "--"); ending.append(node("span", "", "Week Ending"), endingValue); const changeDate = () => { saveReferenceDate(date.value); load(); }; const changeWeek = days => { if (dates.parse(date.value)) { date.value = dates.shift(date.value, days); changeDate(); } }; navigation.append(button("Previous week", "left", () => changeWeek(-7)), date, button("Next week", "right", () => changeWeek(7))); date.addEventListener("change", changeDate); toolbar.append(navigation, ending); const summary = node("div", "oe-time-summary"), sums = {}; for (const label of ["Service", "Non Service", "Time Off", "Total Hours"]) { const item = node("div"); sums[label] = node("strong", "", "0h"); item.append(node("span", "", `${label}: `), sums[label]); summary.append(item); } const status = node("div", "oe-time-status"); status.setAttribute("role", "status"); const retry = button("Retry", "clock", load, true); retry.hidden = true; const serviceStatus = node("div", "oe-time-status"); serviceStatus.setAttribute("role", "status"); serviceStatus.hidden = true; const serviceRetry = button("Retry Service Requests", "clock", () => loadServices(entry.initialWeekId, true), true); serviceRetry.hidden = true; const taskStatus = node("div", "oe-time-status"); taskStatus.setAttribute("role", "status"); const taskRetry = button("Retry Task Types", "clock", loadTaskTypes, true); taskRetry.hidden = true; const scroll = node("div", "oe-time-grid"), table = node("table"); table.setAttribute("data-table-layout", "standard"); table.setAttribute("aria-label", "Weekly time entries"); scroll.append(table); const footer = node("footer", "opportunities-extension-action-form-footer oe-time-footer"); const add = button("Add", "add", () => addRow("Service"), true); const addNonService = button("Add (Non service)", "add", () => addRow("Non Service"), true); const save = button("Save", "save", saveEntries, true); save.disabled = true; const saveStatus = node("div", "oe-time-status"); saveStatus.hidden = true; saveStatus.setAttribute("role", "status"); footer.append(add, addNonService, save); body.append(toolbar, summary, status, retry, serviceStatus, serviceRetry, taskStatus, taskRetry, scroll, saveStatus, footer); dialog.append(header, body); modal.append(dialog); host.append(modal); const entry = { modal, trigger, version: 0, days: [], rows: [], drafts: new Map(), tbody: null, dayTotals: [], services: [], servicesStarted: false, servicesReady: false, expanded: new Map() }; active = entry; entry.deletions = new Map(); entry.taskTypes = []; entry.taskTypesReady = false; let combos = []; modal.addEventListener("scroll", event => { if (event.target === body || event.target === scroll) combos.forEach(combo => combo.sync()); }, true); modal.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); } if (event.key !== "Tab") return; const elements = [...dialog.querySelectorAll("button:not(:disabled), input:not(:disabled), textarea:not(:disabled)")].filter(el => !el.hidden && el.getClientRects().length); const first = elements[0], last = elements[elements.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); } }); function blankRow(type) { return { type, hours: {}, invalidHours: {}, notes: "", srNumber: "", activity: "", taskType: "", notesOpen: false }; } const hourFields = ["sundayTaskHours", "mondayTaskHours", "tuesdayTaskHours", "wednesdayTaskHours", "thursdayTaskHours", "fridayTaskHours", "saturdayTaskHours"]; const sorting = tableSorting.create("manage-time", [ { key: "service", value: row => row.type === "Non Service" ? "Non Service" : `${entry.services.find(sr => sr.srNumber === row.srNumber)?.srTitle || ""} ${row.srNumber}` }, { key: "activity" }, { key: "taskType", value: row => entry.taskTypes.find(item => item.lookUpCode_c === row.taskType)?.lookUpMeaning_c || row.taskType }, { key: "notes" }, ...hourFields.map((key, weekday) => ({ key, type: "number", value: row => { const day = entry.days.find(item => dates.fromApi(item.Date).getUTCDay() === weekday); return day ? row.hours[day.Date] : null; } })) ]); function rowPayload(row) { const payload = { srNumber: row.type === "Non Service" ? "non-service-sr" : row.srNumber, activityCode: row.activity, taskType: row.taskType, deliveryType: row.deliveryType || "", notes: row.notes, weekId: entry.days[0]?.WeekId, tag: row.type === "Non Service" ? "NON_SERVICE" : "SERVICE" }; for (const day of entry.days) payload[hourFields[dates.fromApi(day.Date).getUTCDay()]] = Number(row.hours[day.Date]) || 0; return payload; } function signature(row) { return JSON.stringify(rowPayload(row)); } function touched(row) { return row.id || row.persisted || row.srNumber || row.activity || row.taskType || row.notes.trim() || Object.values(row.hours).some(value => value !== "") || Object.values(row.invalidHours).some(Boolean); } function validRow(row) { return !!row.taskType && (row.type === "Non Service" || (!!row.srNumber && !!row.activity)) && !Object.values(row.invalidHours).some(Boolean) && Object.values(row.hours).every(value => Number.isFinite(Number(value)) && Number(value) >= 0 && Number(value) <= 24) && Object.values(row.hours).some(value => Number(value) > 0); } function changedRows() { return entry.rows.filter(row => touched(row) && signature(row) !== row.baseline); } function pendingDeletes() { const key = entry.days.map(day => day.Date).join("|"); if (!entry.deletions.has(key)) entry.deletions.set(key, new Set()); return entry.deletions.get(key); } function updateSave() { const rows = changedRows(); const deletions = pendingDeletes(); save.disabled = (rows.length > 0 && !requestSave) || (deletions.size > 0 && !requestDelete) || !entry.tbody || entry.saving || (!rows.length && !deletions.size) || entry.rows.some(row => touched(row) && !validRow(row)) || rows.some(row => row.persisted && !row.id); save.title = save.disabled ? "Complete the required fields and enter hours to save changes." : "Save time entries"; } async function saveEntries() { updateSave(); if (save.disabled) return; const rows = changedRows(), knownIds = new Set(entry.rows.filter(row => row.id).map(row => String(row.id))); const controls = []; function lock(node) { if (["BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(String(node.tagName || node.tag).toUpperCase())) { controls.push([node, node.disabled]); node.disabled = true; } Array.from(node.children).forEach(lock); } entry.saving = true; lock(modal); saveStatus.hidden = false; saveStatus.setAttribute("role", "status"); saveStatus.textContent = "Saving time entries..."; let savedCount = 0, deletedCount = 0; try { const deletions = pendingDeletes(); for (const id of [...deletions]) { await requestDelete(id); deletions.delete(id); deletedCount++; } for (const method of ["PUT", "POST"]) { const group = rows.filter(row => method === "PUT" ? !!row.id : !row.id); if (!group.length) continue; const result = await requestSave(method, group.map(row => ({ ...rowPayload(row), ...(row.id ? { id: row.id } : {}) }))); group.forEach(row => { row.persisted = true; row.baseline = signature(row); }); savedCount += group.length; if (method === "POST") { let records = Array.isArray(result) ? result : Array.isArray(result?.items) ? result.items : []; function assignIds() { const candidates = rowsFromEntries(records, entry.days); for (const row of group.filter(row => !row.id)) { const match = candidates.find(candidate => candidate.id && !knownIds.has(String(candidate.id)) && signature(candidate) === row.baseline); if (match) { row.id = match.id; knownIds.add(String(match.id)); } } } assignIds(); if (group.some(row => !row.id)) { records = await requestEntries(entry.days[0].WeekId); assignIds(); } if (group.some(row => !row.id)) throw new Error("Entries saved, but their IDs could not be confirmed. Reopen Manage Time before editing these entries."); } } saveStatus.textContent = [savedCount ? `${savedCount} time ${savedCount === 1 ? "entry" : "entries"} saved.` : "", deletedCount ? `${deletedCount} time ${deletedCount === 1 ? "entry" : "entries"} deleted.` : ""].filter(Boolean).join(" "); notify(saveStatus.textContent); saveStatus.hidden = true; } catch (error) { saveStatus.setAttribute("role", "alert"); saveStatus.textContent = `${savedCount ? `${savedCount} saved. ` : ""}${deletedCount ? `${deletedCount} deleted. ` : ""}${error.message || "Unable to save time entries."}`; notify(saveStatus.textContent, true); } finally { entry.saving = false; controls.forEach(([control, disabled]) => { control.disabled = disabled; }); if (active === entry) { renderRows(); updateSave(); } } } function total(value) { return Number(Number(value).toFixed(2)).toString(); } function updateTotals() { const groups = { Service: 0, "Non Service": 0, "Time Off": 0, "Total Hours": 0 }; entry.days.forEach((day, i) => { let sum = 0; entry.rows.forEach(row => { const hours = Number(row.hours[day.Date]) || 0; sum += hours; groups[row.type] += hours; groups["Total Hours"] += hours; }); entry.dayTotals[i].textContent = total(sum); }); Object.entries(groups).forEach(([label, value]) => { sums[label].textContent = `${total(value)}h`; }); updateSave(); } function addRow(type, source) { if (!entry.tbody || add.disabled) return; entry.rows.push(source ? { ...source, id: undefined, persisted: false, baseline: undefined, duplicateHighlightAt: undefined, ...(source.type === "Service" ? { srNumber: "", activity: "" } : {}), invalidHours: { ...source.invalidHours }, hours: { ...source.hours } } : blankRow(type)); renderRows(); } function renderRows() { if (entry.saving) return; const focusedId = document.activeElement?.id; combos = []; entry.tbody.replaceChildren(); sorting.select(entry.rows).forEach(row => { const index = entry.rows.indexOf(row); const tr = node("tr"); if (row.duplicateHighlightAt && Date.now() - row.duplicateHighlightAt < 5500) { tr.className = "oe-time-duplicate-row"; tr.setAttribute("style", `animation-delay: -${Math.max(0, Date.now() - row.duplicateHighlightAt)}ms`); tr.addEventListener("animationend", () => { tr.className = ""; row.duplicateHighlightAt = undefined; }); } for (const label of [row.type === "Service" ? "Service Request" : "Non Service", "Activity", "Task Type"]) { const td = node("td"), select = node("select"); select.id = `oe-time-${index}-${label.toLowerCase().replaceAll(" ", "-")}`; let change = () => {}; select.setAttribute("aria-label", `${label}, row ${index + 1}`); const placeholder = node("option", "", label); placeholder.value = ""; select.append(placeholder); if (label === "Service Request") { for (const sr of entry.services) { const option = node("option", "", sr.srTitle ? `${sr.srTitle} (${sr.srNumber})` : sr.srNumber); option.value = sr.srNumber; option.title = option.textContent; select.append(option); } if (row.srNumber && !entry.services.some(sr => sr.srNumber === row.srNumber)) { const option = node("option", "", row.srNumber); option.value = row.srNumber; select.append(option); } select.value = row.srNumber; select.disabled = !entry.servicesReady; select.title = entry.servicesReady ? "Service Request" : "Loading Service Requests"; change = () => { const duplicate = select.value && entry.rows.find(other => other !== row && other.type === "Service" && other.srNumber === select.value); if (duplicate) { duplicate.duplicateHighlightAt = Date.now(); select.value = row.srNumber; notify("This Service Request is already selected in another row.", true); renderRows(); return; } row.srNumber = select.value; row.activity = ""; renderRows(); }; } else if (label === "Activity") { const source = entry.expanded.get(row.type === "Service" ? row.srNumber : "non-service-sr"); const raw = String(source?.SRActivities_c || ""); const activities = [...new Set(raw.split(raw.includes(";") ? ";" : ",").map(value => value.trim()).filter(Boolean))]; if (row.activity && !activities.includes(row.activity)) activities.push(row.activity); for (const activity of activities) { const option = node("option", "", activity); option.value = activity; select.append(option); } select.value = row.activity; select.disabled = !activities.length; select.title = activities.length ? "Activity" : "No activities available for this Service Request"; change = () => { row.activity = select.value; updateSave(); }; } else if (label === "Task Type") { const tag = row.type === "Service" ? "SERVICE" : "NON_SERVICE"; const taskTypes = entry.taskTypes.filter(item => item.tags_c === tag); for (const item of taskTypes) { const option = node("option", "", item.lookUpMeaning_c); option.value = item.lookUpCode_c; select.append(option); } if (row.taskType && !taskTypes.some(item => item.lookUpCode_c === row.taskType)) { const option = node("option", "", row.taskType); option.value = row.taskType; select.append(option); } select.value = row.taskType; select.disabled = !entry.taskTypesReady || (!taskTypes.length && !row.taskType); select.title = !entry.taskTypesReady ? "Loading Task Types" : taskTypes.length ? "Task Type" : "No Task Types available for this row type"; change = () => { row.taskType = select.value; updateSave(); }; } else { select.disabled = true; select.title = "Options are not available yet."; } td.append(select); tr.append(td); select.addEventListener("change", change); const combo = searchableSelect(select, td, change, { floating: true, listWidth: label === "Service Request" ? 640 : 0, listMaxHeight: label === "Service Request" ? 360 : 180, tooltipContent: label === "Service Request" ? serviceRequestTooltip : undefined }); combo.input.setAttribute("aria-label", `${label}, row ${index + 1}`); combos.push(combo); } for (const day of entry.days) { const td = node("td"), input = node("input"); input.type = "number"; input.min = "0"; input.max = "24"; input.step = "1"; input.value = row.hours[day.Date] ?? ""; input.setAttribute("aria-invalid", String(!!row.invalidHours[day.Date])); input.setAttribute("aria-label", `Hours ${day.Date}, row ${index + 1}`); input.addEventListener("input", () => { const valid = !input.validity?.badInput && (input.value === "" || (Number.isFinite(input.valueAsNumber) && input.valueAsNumber >= 0 && input.valueAsNumber <= 24)); if (!valid) { input.value = row.hours[day.Date] ?? ""; notify("Enter hours between 0 and 24.", true); return; } row.invalidHours[day.Date] = !valid; input.setAttribute("aria-invalid", String(!valid)); row.hours[day.Date] = input.value; updateTotals(); }); td.append(input); tr.append(td); } const notesCell = node("td"), notesRow = node("tr", "oe-time-notes"); notesRow.hidden = !row.notesOpen; const noteCell = node("td"); noteCell.colSpan = entry.days.length + 6; const notesBox = node("div", "oe-time-notes-box"); const remaining = node("span", "oe-time-notes-count", `${2000 - row.notes.length} characters left`); const notes = node("textarea"); notes.rows = 6; notes.maxLength = 2000; notes.value = row.notes; notes.setAttribute("aria-label", `Notes, row ${index + 1}`); notes.addEventListener("input", () => { row.notes = notes.value.slice(0, 2000); remaining.textContent = `${2000 - row.notes.length} characters left`; updateSave(); }); const collapse = button(`Collapse notes, row ${index + 1}`, "up", () => { row.notesOpen = false; notesRow.hidden = true; notesButton.setAttribute("aria-expanded", "false"); notesButton.focus(); }); collapse.className = "oe-time-notes-collapse"; notesBox.append(remaining, notes, collapse); noteCell.append(notesBox); notesRow.append(noteCell); const notesButton = button(`Notes, row ${index + 1}`, "notes", () => { row.notesOpen = !row.notesOpen; notesRow.hidden = !row.notesOpen; notesButton.setAttribute("aria-expanded", String(row.notesOpen)); if (!notesRow.hidden) notes.focus(); }); notesButton.setAttribute("aria-expanded", String(row.notesOpen)); notesCell.append(notesButton); tr.append(notesCell); const copy = node("td"); copy.append(button(`Copy row ${index + 1}`, "copy", () => addRow(row.type, row))); tr.append(copy); const remove = node("td"); remove.append(button(`Delete row ${index + 1}`, "trash", () => { if (entry.saving) return; if (row.persisted && !row.id) { saveStatus.hidden = false; saveStatus.setAttribute("role", "alert"); saveStatus.textContent = "Reopen Manage Time to obtain the saved entry ID before deleting it."; return; } if (row.id) pendingDeletes().add(String(row.id)); entry.rows.splice(index, 1); renderRows(); saveStatus.hidden = false; saveStatus.setAttribute("role", "status"); saveStatus.textContent = pendingDeletes().size ? `${pendingDeletes().size} pending ${pendingDeletes().size === 1 ? "deletion" : "deletions"}. Click Save to apply.` : "Unsaved row removed."; })); tr.append(remove); entry.tbody.append(tr, notesRow); }); updateTotals(); combos.find(combo => combo.input.id === focusedId)?.input.focus(); } function serviceRequestTooltip(srNumber) { const sr = entry.expanded.get(srNumber); if (!sr) return null; const details = node("dl", "oe-time-sr-details"); for (const [key, label] of [ ["SrNumber", "SR number"], ["AccountPartyUniqueName", "Account"], ["OptyName_c", "Opportunity"], ["OptyNumber_c", "Opty number"], ["OptyOwner_c", "Opty owner"], ["OptyStatus_c", "Opty status"], ["ServiceNew_c", "Service"], ["ReportedByPartyUniqueName", "Requested by"], ["CountryText_c", "Country"], ["StatusCdMeaning", "Status"], ["AssigneePersonName", "Service lead"], ["ProblemDescription", "Description"] ]) { const field = node("div", key === "ProblemDescription" ? "oe-time-sr-description" : ""); field.append(node("dt", "", label), node("dd", "", String(sr[key] ?? "-"))); details.append(field); } return details; } async function loadTaskTypes() { taskStatus.className = "oe-time-status oe-time-loading-status"; taskStatus.hidden = false; taskStatus.textContent = "Loading Task Types..."; taskStatus.setAttribute("role", "status"); taskRetry.hidden = true; try { const items = await requestTaskTypes(); if (active !== entry) return; entry.taskTypes = items.filter(item => typeof item?.lookUpCode_c === "string" && item.lookUpCode_c.trim() && typeof item.lookUpMeaning_c === "string" && item.lookUpMeaning_c.trim()); entry.taskTypesReady = true; taskStatus.className = "oe-time-status"; taskStatus.textContent = "No Task Types available."; taskStatus.hidden = entry.taskTypes.length > 0; if (entry.tbody) renderRows(); } catch (error) { if (active !== entry) return; taskStatus.className = "oe-time-status"; taskStatus.textContent = error.message || "Unable to load Task Types."; taskStatus.setAttribute("role", "alert"); taskRetry.hidden = false; } } async function loadServices(weekId, retrying = false) { if (entry.servicesStarted && !retrying) return; entry.servicesStarted = true; entry.initialWeekId = weekId; serviceRetry.hidden = true; serviceStatus.hidden = false; serviceStatus.className = "oe-time-status oe-time-loading-status"; serviceStatus.setAttribute("role", "status"); serviceStatus.textContent = "Loading Service Requests..."; try { const services = entry.servicesReady ? entry.services : await requestServices(weekId); if (active !== entry) return; entry.services = services; entry.servicesReady = true; serviceStatus.textContent = services.length ? "" : "No active Service Requests."; serviceStatus.hidden = services.length > 0; if (entry.tbody) renderRows(); serviceStatus.hidden = false; serviceStatus.textContent = "Loading activities..."; const expanded = await requestExpand(services.map(sr => sr.srNumber)); if (active !== entry) return; entry.expanded = new Map(expanded.map(sr => [sr.SrNumber, sr])); serviceStatus.hidden = true; if (entry.tbody) renderRows(); } catch (error) { if (active !== entry) return; serviceStatus.className = "oe-time-status"; serviceStatus.textContent = error.message || "Unable to load Service Requests."; serviceStatus.setAttribute("role", "alert"); serviceRetry.hidden = false; } } function rowsFromEntries(records, days) { return records.map(record => { const row = blankRow(record.tag === "NON_SERVICE" || record.srNumber === "non-service-sr" ? "Non Service" : "Service"); row.id = record.id; row.deliveryType = record.deliveryType || ""; row.srNumber = record.srNumber || ""; row.activity = record.activityCode || ""; row.taskType = record.taskType || ""; row.notes = typeof record.notes === "string" ? record.notes : ""; for (const day of days) { const value = record[hourFields[dates.fromApi(day.Date).getUTCDay()]]; row.hours[day.Date] = value == null ? "" : String(value); } row.baseline = signature(row); return row; }); } function renderWeek(items, records) { entry.days = items; const key = items.map(day => day.Date).join("|"); if (!entry.drafts.has(key)) entry.drafts.set(key, records.length ? rowsFromEntries(records, items) : Array.from({ length: 3 }, () => blankRow("Service"))); entry.rows = entry.drafts.get(key); const head = node("thead"), row = node("tr"); for (const [key, label] of [["service", "Service Request"], ["activity", "Activity"], ["taskType", "Task Type"]]) { const th = node("th"); sorting.header(th, key, label, renderRows); row.append(th); } entry.dayTotals = []; for (const day of items) { const parsed = dates.fromApi(day.Date), th = node("th", "oe-time-day"); const dayButton = sorting.header(th, hourFields[parsed.getUTCDay()], ["Su", "M", "T", "W", "Th", "F", "Sa"][parsed.getUTCDay()], renderRows); const dayLabel = dayButton.children[0]; dayLabel.className += " oe-time-day-label"; const count = node("span", "oe-time-day-total", "0"); entry.dayTotals.push(count); dayLabel.append(count); th.append(node("span", "", parsed.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }))); row.append(th); } const notesHeading = node("th"); sorting.header(notesHeading, "notes", "Notes", renderRows); row.append(notesHeading); for (const label of ["Copy", ""]) row.append(node("th", "", label)); [...row.children].forEach(th => th.setAttribute("scope", "col")); head.append(row); entry.tbody = node("tbody"); table.replaceChildren(head, entry.tbody); endingValue.textContent = dates.fromApi(items[0].EndDate).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" }); renderRows(); } function renderSkeleton() { const head = node("thead"), heading = node("tr"), skeletonBody = node("tbody"); for (let column = 0; column < 13; column++) { const cell = node("th"); cell.append(node("span", "oe-time-skeleton oe-time-skeleton-heading")); heading.append(cell); } head.append(heading); for (let row = 0; row < 3; row++) { const tr = node("tr", "oe-time-skeleton-row"); for (let column = 0; column < 13; column++) { const cell = node("td"); cell.append(node("span", `oe-time-skeleton ${column < 3 ? "oe-time-skeleton-select" : "oe-time-skeleton-control"}`)); tr.append(cell); } skeletonBody.append(tr); } table.setAttribute("aria-hidden", "true"); table.replaceChildren(head, skeletonBody); } function finishSkeleton() { table.setAttribute("aria-hidden", "false"); endingValue.className = ""; Object.values(sums).forEach(value => { value.className = ""; }); status.className = "oe-time-status"; } async function load() { if (entry.saving) return; finishSkeleton(); const version = ++entry.version; entry.tbody = null; table.replaceChildren(); endingValue.textContent = "--"; save.disabled = true; saveStatus.hidden = true; Object.values(sums).forEach(value => { value.textContent = "--"; }); add.disabled = addNonService.disabled = true; retry.hidden = true; status.hidden = false; status.setAttribute("role", "status"); scroll.setAttribute("aria-busy", "false"); if (!dates.parse(date.value)) { status.textContent = "Select a valid reference date."; return; } status.className = "oe-time-status oe-time-loading-status"; endingValue.className = "oe-time-skeleton oe-time-skeleton-summary"; Object.values(sums).forEach(value => { value.className = "oe-time-skeleton oe-time-skeleton-summary"; }); renderSkeleton(); status.textContent = "Loading week details..."; scroll.setAttribute("aria-busy", "true"); try { const items = await requestWeek(date.value); if (active !== entry || entry.version !== version) return; status.textContent = "Loading time entries..."; const key = items.map(day => day.Date).join("|"); const records = entry.drafts.has(key) ? [] : await requestEntries(items[0].WeekId); if (active !== entry || entry.version !== version) return; finishSkeleton(); renderWeek(items, records); add.disabled = addNonService.disabled = false; status.hidden = true; loadServices(items[0].WeekId); } catch (error) { if (active !== entry || entry.version !== version) return; finishSkeleton(); table.replaceChildren(); status.textContent = error.message || "Unable to load week details."; status.setAttribute("role", "alert"); retry.hidden = false; } finally { if (active === entry && entry.version === version) scroll.setAttribute("aria-busy", "false"); } } date.focus(); load(); loadTaskTypes(); } return { createButton, open, close, setReady }; } // Source: src/shared/messages.js /** Stable wire names. All payloads remain plain, serializable objects. */ function createMessageTypes() { return Object.freeze({ addShortcutBookmark: "opportunitiesExtension.addShortcutBookmark", getBookmarkFolders: "opportunitiesExtension.getBookmarkFolders", getShortcutBookmarkStatus: "opportunitiesExtension.getShortcutBookmarkStatus", loadOrganizationChart: "opportunitiesExtension.loadOrganizationChart", saveOrganizationChart: "opportunitiesExtension.saveOrganizationChart", requestWeekDetails: "opportunitiesExtension.requestWeekDetails", requestTimeEntries: "opportunitiesExtension.requestTimeEntries", requestTimeEntriesSave: "opportunitiesExtension.requestTimeEntriesSave", requestTimeEntriesDelete: "opportunitiesExtension.requestTimeEntriesDelete", requestLookupTaskType: "opportunitiesExtension.requestLookupTaskType", requestServiceRequestExpand: "opportunitiesExtension.requestServiceRequestExpand", requestResourceUser: "opportunitiesExtension.requestResourceUser", requestActiveServiceRequests: "opportunitiesExtension.requestActiveServiceRequests", 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", requestWorkloads: "opportunitiesExtension.requestWorkloads", requestWorkloadForecastUpdate: "opportunitiesExtension.requestWorkloadForecastUpdate", requestActionTeamMembers: "opportunitiesExtension.requestActionTeamMembers", requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup", requestMgmtAction: "opportunitiesExtension.requestMgmtAction", requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest", requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar", requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest", requestPostInternalServiceRequest: "opportunitiesExtension.requestPostInternalServiceRequest", prepareSrSession: "opportunitiesExtension.prepareSrSession", requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage", requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform", requestLookupTypePlatform: "opportunitiesExtension.requestLookupTypePlatform", requestInternalSrServices: "opportunitiesExtension.requestInternalSrServices", requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness", requestCurrentUser: "opportunitiesExtension.requestCurrentUser", invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache", requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend", requestConsumptionPlanDetail: "opportunitiesExtension.requestConsumptionPlanDetail", requestRatecardByPlan: "opportunitiesExtension.requestRatecardByPlan" }); } // Source: src/shared/debug-body.js function createDebugBodyPreview(text) { return { value: text || "(empty response body)", truncated: false }; } // 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)" ]; 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/features/opportunities/query-state.js function createOpportunitiesQueryState() { return { periodRequestVersion: 0, dateRange: null }; } // 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 { selectedProductTypes: new Set(["Booking", "Workload"]), 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/preferences/request-cache.js function createFormRequestCache({ getHours, getUserId }) { const entries = new Map(); const pending = new Map(); let generation = 0; let owner = ""; const queries = new Set([ "requestWorkloads", "requestWeekDetails", "requestTimeEntries", "requestActiveServiceRequests", "requestServiceRequestExpand", "requestLookupTaskType", "requestActionTeamMembers", "requestActionFindLookup", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestLookupTypePlatform", "requestInternalSrServices" ]); const writes = new Set(["requestTimeEntriesDelete", "requestTimeEntriesSave", "requestMgmtAction", "requestPostInternalServiceRequest", "requestPostServiceRequest", "requestWorkloadForecastUpdate"]); function invalidate() { generation++; entries.clear(); pending.clear(); } function maxAge() { const hours = Number(getHours()); return (Number.isFinite(hours) && hours > 0 ? Math.min(hours, 168) : 24) * 3600000; } function copy(value) { return JSON.parse(JSON.stringify(value)); } function stable(value) { if (Array.isArray(value)) return value.map(stable); if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])); return value; } async function send(message, transport) { const userId = String(getUserId() || "").trim().toLowerCase(); if (userId !== owner) { invalidate(); owner = userId; } const operation = String(message.type || "").split(".").pop(); const duration = maxAge(); const request = queries.has(operation) || writes.has(operation) || operation === "requestResourceUser" ? { ...message, cacheMaxAgeMs: duration } : message; if (operation === "invalidateProductsCache") invalidate(); // Only explicitly known read operations are reusable. Writes and token generation always execute. if (!queries.has(operation) || !userId) { const result = await transport(request); if (result?.ok && writes.has(operation) && !(operation === "requestPostServiceRequest" && message.phase === "prepare")) invalidate(); return result; } // Workloads are scoped to the user/account, not the rotating WWB token or the selected Reference. const key = JSON.stringify(stable(operation === "requestWorkloads" ? { type: message.type, customerId: String(message.customerId), principal: String(message.principal || userId).trim().toLowerCase() } : message)); const hit = entries.get(key); if (hit && Date.now() >= hit.fetchedAt && Date.now() - hit.fetchedAt < duration) { return { ...copy(hit.result), debugRequests: [], fromCache: true, cachedAt: hit.fetchedAt }; } entries.delete(key); if (pending.has(key)) { const result = await pending.get(key); return { ...copy(result), debugRequests: [] }; } const version = generation; const promise = transport(request).then(result => { const workload = operation === "requestWorkloads"; const fetchedAt = workload && Number.isFinite(result?.cachedAt) ? result.cachedAt : Date.now(); const cacheable = !workload || (Array.isArray(result?.workloads) && (!result.fromCache || Number.isFinite(result.cachedAt))); if (result?.ok && cacheable && version === generation) { // Keep this per-page cache bounded and avoid persisting credentials or customer data to disk. if (entries.size >= 500) entries.delete(entries.keys().next().value); // Reusing a persistent workload entry must not restart its original TTL. entries.set(key, { result: copy({ ...result, debugRequests: [] }), fetchedAt }); } return result; }); pending.set(key, promise); try { return await promise; } finally { if (pending.get(key) === promise) pending.delete(key); } } return { send, invalidate }; } // Source: src/features/products/content-state.js function createProductsContentState() { return { currentProductsRequestId: "", productsRenderQueued: false, opptyProductsByNumber: new Map(), workloadCustomerIds: [], workloadsByCustomer: new Map(), expandedProductOptyNumbers: new Set(), productsProgress: { state: "idle", completed: 0, total: 0, failed: 0, cacheHits: 0 }, productsLastCachedAt: 0 }; } // 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(environment = {}) { let storage; let tabId; try { storage = environment.localStorage; const session = environment.window?.sessionStorage; tabId = session?.getItem("opportunities-extension-debug-tab"); if (!tabId) { tabId = environment.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`; session?.setItem("opportunities-extension-debug-tab", tabId); } } catch { storage = null; } const prefix = `opportunities-extension-debug:${tabId || "memory"}:`; let timer = null; const pending = new Map(); const state = { requestLog: [], debugSecretsVisible: false, storageError: "", generation: 0, persist, flush, reload, reset }; function persist(entry) { if (!state.requestLog.includes(entry)) return; pending.set(entry.id, entry); try { if (timer === null && environment.setTimeout) timer = environment.setTimeout(flush, 100); else if (!environment.setTimeout) flush(); } catch { // Debug persistence must never interrupt the request being recorded. timer = null; flush(); } } function flush() { if (timer !== null) environment.clearTimeout?.(timer); timer = null; if (!storage) { pending.clear(); return; } try { // Write only changed records, never stringify all response bodies per request. for (const [id, entry] of pending) storage.setItem(`${prefix}${id}`, JSON.stringify(entry)); storage.setItem(`${prefix}index`, JSON.stringify({ version: 1, count: state.requestLog.length })); state.storageError = ""; } catch { state.storageError = "Local storage is unavailable or full. Requests remain available in memory for this session."; } pending.clear(); } function reload() { flush(); if (!storage || state.storageError) return; try { const index = JSON.parse(storage.getItem(`${prefix}index`) || "null"); if (!index || index.version !== 1 || index.count !== state.requestLog.length) return; const records = state.requestLog.map(entry => { const saved = JSON.parse(storage.getItem(`${prefix}${entry.id}`) || "null"); if (!saved || saved.id !== entry.id) throw new Error("Incomplete debug record"); return { ...saved, startedAt: new Date(saved.startedAt), completedAt: saved.completedAt ? new Date(saved.completedAt) : null }; }); // Preserve the handles held by requests which are still in flight. records.forEach((record, i) => Object.assign(state.requestLog[i], record)); } catch { state.storageError = "Saved debug data could not be read. Showing this session's requests from memory."; } } function reset() { if (timer !== null) environment.clearTimeout?.(timer); timer = null; pending.clear(); state.requestLog = []; state.debugSecretsVisible = false; state.storageError = ""; state.generation++; try { if (!storage) return; const keys = []; for (let i = 0; i < storage.length; i++) { const key = storage.key(i); if (key?.startsWith(prefix)) keys.push(key); } keys.forEach(key => storage.removeItem(key)); storage.setItem(`${prefix}index`, JSON.stringify({ version: 1, count: 0 })); } catch { state.storageError = "Local storage is unavailable. Requests will be kept in memory for this session."; } } return state; } // 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", "PartyName"]), display: (item) => getNestedValue(item, ["CustomerAccount", "PartyName"]) }, { 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: "Prob.", 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/print.js function createOpportunitiesPrint({ environment, getSnapshot }) { function createButton() { const doc = environment.document; const button = doc.createElement("button"); button.type = "button"; button.className = "opportunities-extension-icon-button"; button.title = "Print opportunities"; button.setAttribute("aria-label", "Print opportunities"); const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("fill", "none"); svg.setAttribute("stroke", "currentColor"); svg.setAttribute("stroke-width", "1.6"); svg.setAttribute("aria-hidden", "true"); const path = doc.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", "M6 9V3h12v6M6 17H3V9h18v8h-3M6 14h12v7H6zM17 11h1"); svg.append(path); button.append(svg); button.addEventListener("click", print); return button; } function print() { const snapshot = getSnapshot(); if (!["ready", "empty"].includes(snapshot.status)) { environment.window.alert("Wait for opportunities to finish loading before printing."); return; } const popup = environment.window.open("", "_blank", "width=1200,height=850"); if (!popup) { environment.window.alert("Allow pop-ups to print opportunities."); return; } popup.opener = null; const doc = popup.document; doc.title = "Opportunities Extension — Report"; doc.documentElement.lang = "en"; const node = (tag, text, parent) => { const el = doc.createElement(tag); if (text != null) el.textContent = String(text); if (parent) parent.append(el); return el; }; node("style", ` @page { size: A4 landscape; margin: 15mm; } * { box-sizing: border-box; } body { margin: 32px; font: 11px/1.4 Arial, sans-serif; color: #222; background: white; } h1 { margin: 0 0 4px; font-size: 21px; } h2 { font-size: 13px; margin: 16px 0 6px; } .meta { color: #555; margin: 0 0 12px; } .filters { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px 16px; border: 1px solid #ccc; padding: 10px; } .filters div { overflow-wrap: anywhere; } .filters strong { margin-right: 5px; } .totals { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; margin: 12px 0; } .totals section { border: 1px solid #bbb; border-top: 3px solid #426a75; padding: 8px; break-inside: avoid; } .totals strong { display: block; font-size: 16px; } .totals small { display: block; } table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 9px; } th, td { padding: 6px 4px; border-bottom: 1px solid #ccc; text-align: left; vertical-align: top; overflow-wrap: anywhere; } th { background: #eee; font-weight: bold; } tbody tr:nth-child(even) { background: #f7f7f7; } thead { display: table-header-group; } tr { break-inside: avoid; } th:nth-child(1), th:nth-child(3) { width: 22%; } th:nth-child(2) { width: 7%; } th:nth-child(4) { width: 8%; } th:nth-child(5) { width: 9%; } th:nth-child(6) { width: 7%; } th:nth-child(7), th:nth-child(8) { width: 5%; } th:nth-child(9) { width: 7%; } th:nth-child(10) { width: 8%; } .money { text-align: right; font-variant-numeric: tabular-nums; } .toolbar { display: flex; gap: 8px; margin-bottom: 16px; } button { padding: 8px 14px; cursor: pointer; } @media print { body { margin: 0; } .toolbar { display: none; } th { print-color-adjust: exact; } } `, doc.head); const toolbar = node("div", null, doc.body); toolbar.className = "toolbar"; node("button", "Print / Save PDF", toolbar).addEventListener("click", () => popup.print()); node("button", "Close", toolbar).addEventListener("click", () => popup.close()); node("h1", "Opportunities Extension", doc.body); node("p", `Generated ${new Date().toLocaleString("en-US")} • ${snapshot.rows.length} opportunities`, doc.body).className = "meta"; const filters = node("div", null, doc.body); filters.className = "filters"; for (const [label, value] of snapshot.filters) { const field = node("div", null, filters); node("strong", `${label}:`, field); node("span", value || "None", field); } const totals = node("div", null, doc.body); totals.className = "totals"; for (const card of snapshot.totals) { const section = node("section", null, totals); node("span", card.label, section); node("strong", card.value, section); if (card.detail) node("small", card.detail, section); } node("h2", "Opportunities", doc.body); const table = node("table", null, doc.body); const header = node("tr", null, node("thead", null, table)); for (const column of snapshot.columns) { const th = node("th", column.label, header); th.scope = "col"; if (column.key === "revenue") th.className = "money"; } const body = node("tbody", null, table); for (const values of snapshot.rows) { const row = node("tr", null, body); values.forEach((value, index) => { const td = node("td", value, row); if (snapshot.columns[index].key === "revenue") td.className = "money"; }); } if (!snapshot.rows.length) node("p", "No opportunities match the selected filters.", doc.body); popup.focus(); // Print a frozen snapshot, without changing the extension's scroll or selected view. environment.setTimeout(() => { if (!popup.closed) popup.print(); }, 150); } return { createButton, print }; } // 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; let shortcutOpened = false; function openShortcutIfRequested() { if (shortcutOpened || !isAllowedPage()) return; if (window.top && window.top !== window) return; let pending = false; try { const timestamp = Number(window.sessionStorage.getItem("opportunitiesExtension.shortcutIntent")); pending = timestamp > 0 && Date.now() >= timestamp && Date.now() - timestamp < 10 * 60 * 1000; window.sessionStorage.removeItem("opportunitiesExtension.shortcutIntent"); } catch { /* Storage may be disabled. */ } if (!pending && window.location.hash !== "#opportunities-extension") return; shortcutOpened = true; openOpportunitiesModal(); } 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() { // Opening a bookmarked shortcut must not depend on Sales being expanded // or its tile insertion point being present in Fusion's lazy navigation. openShortcutIfRequested(); 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({ openBookmarkPicker, getShortcutBookmarkStatus, 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, closeOrganizationChart, closeActionForm, closeCustomerFilterPanel, closeOwnerFilterPanel, closePreferencesModal, createSettingsIcon, createRefreshAllButton, createManageTimeButton, createActivityFilterControls, mountOpportunityViews, debugState, ensureExtensionStyles, getCustomerOptions, getOwnerOptions, getSavedOpportunityTypeValues, getSavedOpptyProductsCacheHours, getSavedTheme, openPreferencesModal, opportunitiesFilterState, opportunitiesQueryState, preferencesState, renderCustomerFilter, renderCustomerFilterList, renderOpportunitiesTable, renderOwnerFilter, renderOwnerFilterList, renderStageFilterButtons, renderStatusFilterButtons, requestOpportunitiesForPeriod, closeCrmSrPopover, requestTokenRelayOnce, ensureCurrentUser, prepareSrSession, resetCustomerAndOwnerFiltersForPeriod, resetOpportunitiesTable, restoreFilterPreferences, saveFilterPreferences, toggleCustomerFilterPanel, toggleDebugPanel, toggleOwnerFilterPanel, environment }) { const { document, window, setTimeout, clearTimeout, matchMedia } = environment; let authCompletion = null; let bookmarkHintTimer = null; let bookmarkHintVersion = 0; function hideBookmarkHint() { bookmarkHintVersion++; if (bookmarkHintTimer !== null) clearTimeout(bookmarkHintTimer); bookmarkHintTimer = null; const hint = document.getElementById("opportunities-extension-bookmark-hint"); if (hint) hint.hidden = true; } async function refreshBookmarkHint() { hideBookmarkHint(); const version = bookmarkHintVersion; try { const result = await getShortcutBookmarkStatus?.(); const modal = document.getElementById(MODAL_ID); const hint = document.getElementById("opportunities-extension-bookmark-hint"); if (version !== bookmarkHintVersion || !modal || modal.hidden || !hint || !result?.ok || result.bookmarked) return; hint.hidden = false; bookmarkHintTimer = setTimeout(hideBookmarkHint, 20000); } catch { /* A failed bookmarks check must not interrupt opportunities. */ } } function resetAuthCompletion() { if (!authCompletion) return; clearTimeout(authCompletion.waitTimer); clearTimeout(authCompletion.hideTimer); authCompletion.badge.setAttribute("data-fading", "false"); authCompletion = null; } function openOpportunitiesModal() { ensureExtensionStyles(); const oldDebug = document.getElementById(DEBUG_PANEL_ID); if (oldDebug) { oldDebug.hidden = true; oldDebug.textContent = ""; } debugState.debugSecretsVisible = false; const existingModal = document.getElementById(MODAL_ID); if (existingModal) { existingModal.hidden = false; refreshBookmarkHint(); prepareSrSession?.(); existingModal.querySelector("select").focus(); document.documentElement.classList.add("opportunities-extension-scroll-lock"); ensureEscapeKeyHandler(); ensureCurrentUser().then(() => { if (existingModal.hidden) return; return requestTokenRelayOnce(); }) .then(() => renderOpportunitiesTable()).catch(() => {}); return; } const overlay = document.createElement("div"); overlay.setAttribute("lang", "en"); 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; const bookmarkHint = document.createElement("span"); bookmarkHint.id = "opportunities-extension-bookmark-hint"; bookmarkHint.className = "opportunities-extension-products-cache-timestamp"; bookmarkHint.setAttribute("role", "status"); bookmarkHint.textContent = "Save to favorites: Ctrl+Shift+F"; bookmarkHint.hidden = true; subtitleRow.append(productsCacheTimestamp, bookmarkHint, authBadge, productsProgressBadge); 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", "Close"); 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", "Open preferences"); 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), createManageTimeButton(), 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(); renderOwnerFilter(); 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(); renderCustomerFilter(); 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("data-table-layout", "standard"); opportunitiesTable.setAttribute("aria-label", "Opportunities"); tableSurface.append(opportunitiesTable); mountOpportunityViews(body, 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); refreshBookmarkHint(); document.documentElement.classList.add("opportunities-extension-scroll-lock"); ensureEscapeKeyHandler(); renderStageFilterButtons(); renderStatusFilterButtons(); renderCustomerFilter(); renderOwnerFilter(); renderOpportunitiesTable(); requestOpportunitiesForPeriod(select.value); prepareSrSession?.(); select.focus(); } function updateAuthBadge(status) { const badge = document.querySelector(`#${MODAL_ID} .opportunities-extension-auth-badge`); if (!badge) { return; } const badgeStatus = status === AUTH_STATUS.authenticated || status === AUTH_STATUS.unauthenticated ? status : AUTH_STATUS.pending; if (authCompletion && (authCompletion.badge !== badge || badgeStatus !== AUTH_STATUS.authenticated)) resetAuthCompletion(); badge.hidden = Boolean(authCompletion?.hidden); if (badgeStatus === AUTH_STATUS.authenticated && !authCompletion) { const current = { badge, hidden: false, waitTimer: null, hideTimer: null }; authCompletion = current; badge.setAttribute("data-fading", "false"); current.waitTimer = setTimeout(() => { if (authCompletion !== current) return; badge.setAttribute("data-fading", "true"); const reducedMotion = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches; current.hideTimer = setTimeout(() => { if (authCompletion !== current) return; current.hidden = true; badge.hidden = true; }, reducedMotion ? 0 : 300); }, 5000); } badge.setAttribute("data-auth-status", badgeStatus); badge.className = `opportunities-extension-auth-badge opportunities-extension-auth-badge-${badgeStatus}`; badge.textContent = badgeStatus === AUTH_STATUS.authenticated ? "Authenticated" : badgeStatus === AUTH_STATUS.unauthenticated ? "Unauthenticated" : "Authenticating"; } function closeOpportunitiesModal() { if (closeOrganizationChart()) return; hideBookmarkHint(); 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; } const shortcutKey = event.key.toLowerCase(); if (event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey && (shortcutKey === "d" || shortcutKey === "f")) { event.preventDefault(); event.stopPropagation(); if (event.repeat) return; if (shortcutKey === "d") toggleDebugPanel(); else openBookmarkPicker(); return; } if (event.key === "Escape") { if (closeOrganizationChart()) { event.preventDefault(); return; } 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, hideBookmarkHint, refreshBookmarkHint }; } // 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; } function createViewIcon(view) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("aria-hidden", "true"); svg.setAttribute("focusable", "false"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); const calendarFrame = "M8 2v4m8-4v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"; path.setAttribute("d", view === "today" ? `${calendarFrame}M9 16l2 2 4-4` : view === "week" ? `${calendarFrame}M6 14h12v4H6Z M10 14v4m4-4v4` : view === "calendar" ? "M8 2v4m8-4v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2ZM7 14h2m6 0h2m-10 4h2m6 0h2" : "M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1ZM3 9h18M9 9v12M3 15h18"); 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; } function createAlertIcon() { /* Lucide triangle-alert (https://lucide.dev/icons/triangle-alert), 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. * * Derived from Feather alert-triangle, MIT License. * Copyright (c) 2013-present Cole Bemis * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); for (const [name, value] of Object.entries({ viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" })) svg.setAttribute(name, value); for (const d of ["m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3", "M12 9v4", "M12 17h.01"]) { const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", d); svg.append(path); } return svg; } return { createThemeIcon, createSettingsIcon, createCloseIcon, createRefreshIcon, createViewIcon, createAlertIcon }; } // 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, enableDialogDrag, redwoodStyles, environment }) { const { document } = environment; function ensureExtensionStyles() { enableDialogDrag?.(); 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/content/dialog-drag.js function createDialogDrag({ environment }) { const { document, window } = environment; const panels = ".opportunities-extension-action-form-dialog, .opportunities-extension-preferences-dialog, .opportunities-extension-account-plans-dialog, .oe-org-confirm-panel"; const positions = new WeakMap(); let installed = false, drag = null; function move(panel, dx, dy) { const rect = panel.getBoundingClientRect(); const viewport = window.visualViewport; const left = viewport?.offsetLeft || 0, top = viewport?.offsetTop || 0; const width = viewport?.width || document.documentElement.clientWidth; const height = viewport?.height || document.documentElement.clientHeight; const x = Math.max(left - rect.width * 0.7, Math.min(rect.left + dx, left + width - rect.width * 0.3)); const y = Math.max(top - rect.height * 0.7, Math.min(rect.top + dy, top + height - rect.height * 0.3)); const previous = positions.get(panel) || { x: 0, y: 0 }; const next = { x: previous.x + x - rect.left, y: previous.y + y - rect.top }; positions.set(panel, next); panel.style.position = "relative"; panel.style.left = `${next.x}px`; panel.style.top = `${next.y}px`; panel.setAttribute("data-dialog-moved", "true"); } function stop() { if (!drag) return; const previous = drag; drag = null; previous.panel.removeAttribute("data-dialog-dragging"); if (previous.title.hasPointerCapture?.(previous.id)) previous.title.releasePointerCapture(previous.id); } function install() { if (installed || window.__opportunitiesDialogDragInstalled || !document.addEventListener || !window.addEventListener) return; installed = true; window.__opportunitiesDialogDragInstalled = true; document.addEventListener("pointerdown", event => { if (event.button !== 0 || event.isPrimary === false) return; const title = event.target.closest?.("header, h2, h3"); const panel = title?.closest(panels); if (!panel || !panel.closest(".opportunities-extension-modal") || panel.closest("[inert], [hidden]") || event.target.closest("button, a, input, select, textarea, [role='button'], [contenteditable='true']")) return; // Accept the whole title bar, but not headers inside the dialog's content. const heading = panel.querySelector("h2, h3"); if (!heading || (heading !== title && !title.contains(heading))) return; const rect = panel.getBoundingClientRect(); if (rect.width >= document.documentElement.clientWidth - 2 && rect.height >= document.documentElement.clientHeight - 2) return; stop(); event.preventDefault(); drag = { title, panel, id: event.pointerId, x: event.clientX, y: event.clientY }; panel.setAttribute("data-dialog-dragging", "true"); title.setPointerCapture?.(event.pointerId); }); document.addEventListener("pointermove", event => { if (!drag || event.pointerId !== drag.id) return; if (!drag.panel.isConnected || drag.panel.closest("[hidden], [inert]")) { stop(); return; } event.preventDefault(); move(drag.panel, event.clientX - drag.x, event.clientY - drag.y); drag.x = event.clientX; drag.y = event.clientY; }); for (const name of ["pointerup", "pointercancel", "lostpointercapture"]) { document.addEventListener(name, event => { if (drag?.id === event.pointerId) stop(); }); } window.addEventListener("blur", stop); function resize() { stop(); for (const panel of document.querySelectorAll('[data-dialog-moved="true"]')) { if (panel.getClientRects().length) move(panel, 0, 0); } } window.addEventListener("resize", resize); window.visualViewport?.addEventListener("resize", resize); } return { install }; } // 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.tokenRelayRequest = null; 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, environment }) { const { setTimeout } = environment; let pending = null; let allowed = false; let userId = ""; function ensure() { if (!pending) { pending = (async () => { let retryDelay = 1000; // Keep a single pending identity request until the response is usable. for (;;) { try { const response = await sendRuntimeMessage({ type: messageTypes.requestCurrentUser }); appendBackgroundRequestLog(response?.debugRequests); const user = response?.user; if (response?.ok === true && typeof user?.id === "string" && user.id.trim() && typeof user.name === "string" && user.name.trim() && Array.isArray(user.roles) && user.roles.every(role => typeof role === "string" && role.trim())) { userId = user.id; allowed = !user.roles.includes("SALES_REP"); return allowed; } } catch (error) { appendBackgroundRequestLog(error?.debugRequests); } await new Promise(resolve => setTimeout(resolve, retryDelay)); retryDelay = Math.min(retryDelay * 2, 10000); } })(); } return pending; } return { ensure, allowsCrmActivities: () => allowed, getUserId: () => userId }; } // 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); const controller = environment.AbortController ? new environment.AbortController() : null; let timeoutId; const timeout = new Promise((_, reject) => { if (!environment.setTimeout) return; timeoutId = environment.setTimeout(() => { reject(new Error("Token relay timed out after 30 seconds. Please try again.")); controller?.abort(); }, 30000); }); try { const response = await Promise.race([fetch(url, { method: "GET", credentials: "include", ...(controller ? { signal: controller.signal } : {}), headers: { "x-xsrf-token": xsrfTokenSource.token } }), timeout]); const responseBody = await Promise.race([readResponseBodyForDebug(response), timeout]); 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 Promise.race([response.json(), timeout]); } catch (error) { if (controller?.signal.aborted) throw 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, responseBody); 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; } finally { if (timeoutId !== undefined) environment.clearTimeout?.(timeoutId); } } } function createTokenRelayRequestEntry(xsrfTokenSource, refreshResult, name = "requestTokenRelay", url = TOKEN_RELAY_URL) { return logRequestStart({ name, method: "GET", url, headers: { "x-xsrf-token": 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, requestCache, environment }) { const { browser, chrome } = environment; function sendRuntimeMessage(message) { return requestCache ? requestCache.send(message, sendUncached) : sendUncached(message); } function sendUncached(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, loadServiceRequests = async () => {}, resetCrmServiceRequests, resetWorkloadActions, resetWorkloads, requestOpptyProductsInBackground, requestTokenRelayOnce, ensureCurrentUser, setOpportunitiesTableState }) { async function requestOpportunitiesForPeriod(period) { const dateRange = getFiscalPeriodRange(period, new Date()); opportunitiesQueryState.dateRange = dateRange; const requestVersion = ++opportunitiesQueryState.periodRequestVersion; resetWorkloadActions(); resetWorkloads(); 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: "" }); // Start SR only after all opportunity pages have been displayed. // Its incremental updates run independently of the opportunity request. try { Promise.resolve(loadServiceRequests(token, requestVersion)).catch(() => {}); } catch { /* Keep the displayed opportunities available. */ } requestOpptyProductsInBackground(allItems, token); } } catch (error) { if (isCurrentPeriodRequest(requestVersion)) { setOpportunitiesTableState({ items: [], status: "error", message: "Unable to load opportunities." }); } throw error; } } function resetOpportunitiesTable() { resetWorkloadActions(); resetWorkloads(); 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, resetDebugRequests = () => {}, environment }) { const { fetch } = environment; async function requestOpportunitiesSavedSearch(token) { const url = OPPORTUNITIES_QUERY_URL + "?access=userAdmin&applications=461e5791-9f15-4cd1-b5cd-19e8480cbce4&entity=Opportunity&language=en&limit=-1&returnIdentifierOnlyFields=true"; const headers = { Accept: "application/json", Authorization: `Bearer ${token}` }; const entry = logRequestStart({ name: "requestOpportunitiesSavedSearch", method: "GET", url, headers }); try { const response = await fetch.call(environment.window, url, { method: "GET", credentials: "include", headers }); logRequestSuccess(entry, response, await readResponseBodyForDebug(response)); if (!response.ok) throw new Error(`requestOpportunitiesSavedSearch failed with status ${response.status}.`); const data = await response.json(); if (!Array.isArray(data?.items)) throw new Error("requestOpportunitiesSavedSearch returned an invalid response."); const defaults = data.items.filter(item => item.isDefault === true); if (defaults.length !== 1 || typeof defaults[0].savedSearchNumber !== "string" || !defaults[0].savedSearchNumber.trim()) throw new Error("A unique default Opportunity saved search with savedSearchNumber was not returned."); return defaults[0].savedSearchNumber.trim(); } catch (error) { logRequestFailure(entry, error.message); throw error; } } async function requestOpportunitiesPage(token, period, dateRange, offset, accumulatedItems, requestVersion, page, savedSearchNumber) { if (!isCurrentPeriodRequest(requestVersion)) { return null; } if (!savedSearchNumber) savedSearchNumber = await requestOpportunitiesSavedSearch(token); if (!isCurrentPeriodRequest(requestVersion)) return null; const payload = createOpportunitiesQueryPayload(dateRange, offset, savedSearchNumber); const requestBody = JSON.stringify(payload); // One history per query, preserving all pages of the same result set. if (page === 1) { resetDebugRequests(); if (!isCurrentPeriodRequest(requestVersion)) return null; } const requestEntry = logRequestStart({ name: "requestOpportunities", method: "POST", url: OPPORTUNITIES_QUERY_URL, headers: { Accept: "application/json", Authorization: `Bearer ${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.call(environment.window, 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, savedSearchNumber ); } catch (error) { logRequestFailure(requestEntry, error.message || "Request failed."); throw error; } } function createOpportunitiesQueryPayload(dateRange, offset, savedSearchNumber) { if (typeof savedSearchNumber !== "string" || !savedSearchNumber.trim()) throw new Error("Missing default Opportunity savedSearchNumber."); return { aggregationResults: false, applications: ["ORACLE-ISS-APP"], onlyData: true, entity: "Opportunity", q: { op: "$and", criteria: [ { op: "$and", criteria: [{ op: "$or", criteria: Array.from(getOpportunityTypeValues(), (value) => ({ op: "$eq", attribute: "RecordSet", value })) }] }, { op: "$wi", attribute: "EffectiveDate", value1: new Date(dateRange.startCloseDate).toISOString().replace(/Z$/, "+0000"), value2: new Date(dateRange.endCloseDate).toISOString().replace(/Z$/, "+0000") } ] }, 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.PartyName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber", "CustomerAccount.Region_c", "CustomerAccount.PrimaryAddress.CountryCode" ], sort: null, language: "en", skipInValidFields: true, skipHiddenFromUIFields: true, copiedFrom: "queries/" + savedSearchNumber, limit: OPPORTUNITIES_PAGE_LIMIT, offset }; } return { requestOpportunitiesPage, createOpportunitiesQueryPayload, requestOpportunitiesSavedSearch }; } // 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"; async function requestSRSearchResources(token) { const searchUrl = `${url}?applications=461e5791-9f15-4cd1-b5cd-19e8480cbce4&entity=CRMServiceRequest&language=en&limit=-1&onlyData=true`; const headers = { Accept: "application/json", Authorization: `Bearer ${token}` }; const entry = logRequestStart({ name: "requestSRSearchResources", method: "GET", url: searchUrl, headers }); try { const response = await fetch(searchUrl, { method: "GET", credentials: "include", headers }); logRequestSuccess(entry, response, await readResponseBodyForDebug(response)); if (response.status !== 200) throw new Error(`requestSRSearchResources failed with status ${response.status}.`); const data = await response.json(); const number = data?.items?.[0]?.savedSearchNumber; if (!Array.isArray(data?.items) || typeof number !== "string" || !number.trim()) { throw new Error("requestSRSearchResources did not return savedSearchNumber in its first item."); } return number.trim(); } catch (error) { logRequestFailure(entry, error.message); throw error; } } function createPayload(offset, savedSearchNumber) { if (typeof savedSearchNumber !== "string" || !savedSearchNumber.trim()) throw new Error("Missing SR savedSearchNumber."); return { aggregationResults: false, applications: ["ORACLE-ISS-APP"], onlyData: true, entity: "CRMServiceRequest", copiedFrom: `queries/${savedSearchNumber.trim()}`, 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", "CustomerDeadline_c", "PrimaryPillarNew_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 = () => {}) { if (!isActive()) return null; const savedSearchNumber = await requestSRSearchResources(token); const items = []; let offset = 0; let attempt = 1; while (isActive()) { const payload = createPayload(offset, savedSearchNumber); 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 }, 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, items.slice()); 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, requestSRSearchResources }; } // Source: src/features/opportunities/crm-service-requests.js function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, formatDate, environment, onStateChange = () => {} }) { let status = "idle"; let byOpportunity = new Map(); let pending = null; let generation = 0; let loadedItems = []; function reset() { closeSrForm(); closePopover(); generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); loadedItems = []; reportProgress("idle"); } function load(token, version, cacheOptions = null) { if (!allowsCrmActivities() || !isCurrentPeriodRequest(version)) return; if (status === "ready") { return Promise.resolve(cacheOptions ? activityCache.write("sr", "all", cacheOptions.cachedAt, loadedItems) : undefined).then(() => true); } if (!pending) { const request = run(token, version, generation, cacheOptions); pending = request; request.finally(() => { if (pending === request) pending = null; }); } return pending; } async function run(token, version, currentGeneration, cacheOptions) { const isActive = () => currentGeneration === generation; status = "loading"; onStateChange(); 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, partialItems) => { if (!isActive()) return; reportProgress("running", count, total); if (Array.isArray(partialItems)) { updateItems(partialItems); onStateChange(); } }); if (items && isActive() && cacheOptions) await activityCache.write("sr", "all", cacheOptions.cachedAt, items); } if (!items || !isActive()) return false; updateItems(items); status = "ready"; if (!cached) reportProgress("complete", items.length, items.length); onStateChange(); return true; } catch { if (isActive()) { status = "error"; reportProgress("error"); onStateChange(); } return false; } } function updateItems(items) { 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; } 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 === "loading" && items.length ? "matched" : status === "ready" ? items.length ? "matched" : "missing" : status, reportedHours: hours.length === items.length ? hours.reduce((total, value) => total + value, 0) : null, hasUnderFourHours: (status === "ready" || status === "loading") && hours.some(value => value < 4) }; } function createBadge(optyNumber) { const badgeStatus = status === "loading" && byOpportunity.get(String(optyNumber))?.length ? "ready" : status; if (!allowsCrmActivities() || !optyNumber || badgeStatus === "idle" || badgeStatus === "loading") return null; const items = byOpportunity.get(String(optyNumber)) || []; const badge = environment.document.createElement(items.length && badgeStatus === "ready" ? "button" : "span"); badge.className = "opportunities-extension-crm-sr-badge"; badge.setAttribute("data-state", badgeStatus === "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/A"; setBadgeContent(badge, "sr", badgeStatus === "error" ? "?" : items.length ? `${reportedHours}${reportedHours === "N/A" ? "" : "h"}` : "0h"); const display = value => value === null || value === undefined || value === "" ? "N/A" : String(value); const descriptions = items.map(item => [ `Title: ${display(item.Title)}`, `Opportunity: ${display(item.OpportunityName_Id_c?.OptyNumber)}`, `Reported Hours: ${display(item.ReportedHours_c)}`, `Customer Deadline: ${display(formatDate(item.CustomerDeadline_c))}`, `Owner: ${[item.Owner?.PersonFirstName, item.Owner?.PersonLastName].filter(Boolean).join(" ") || "N/A"}`, `Email: ${display(item.Owner?.PrimaryEmail?.EmailAddress)}`, `Do Not Call: ${display(item.DoNotCallFlag)}`, `Do Not Email: ${display(item.DoNotEmailFlag)}` ].join("\n")); const description = badgeStatus === "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 && badgeStatus === "ready") bindPopover(badge, items, descriptions); if (!items.length && badgeStatus === "ready") badge.append(createSrButton(String(optyNumber))); badge.tabIndex = 0; return badge; } return { reset, load, createBadge, getSummary }; } // Source: src/features/opportunities/product-activities.js function createProductActivities({ productsContentState, opportunitiesQueryState, loadCrm, loadActions, blockActions, render }) { let cycle = null; function begin(requestId, token, opportunityNumbers = null) { cycle = { requestId, token, numbers: opportunityNumbers && new Set(opportunityNumbers.map(String)), version: opportunitiesQueryState.periodRequestVersion, completed: false }; } function complete(requestId) { const current = cycle; if (!current || current.completed || current.requestId !== requestId || productsContentState.currentProductsRequestId !== requestId) return; current.completed = true; const entries = [...productsContentState.opptyProductsByNumber].filter(([number]) => !current.numbers || current.numbers.has(String(number))); const products = entries.map(([, result]) => result).filter(result => !result.error); if (!products.length) return; const cachedAt = Math.max(...products.map(result => Number(result.cachedAt) || 0)); const isActive = () => cycle === current && productsContentState.currentProductsRequestId === requestId && opportunitiesQueryState.periodRequestVersion === current.version; // Product cache hits do not guarantee that the corresponding activity cache exists. return Promise.resolve(loadCrm(current.token, current.version, { cachedAt, cacheOnly: false })).then(async success => { if (!isActive()) return; if (success === false) { products.forEach(blockActions); render(); return; } const pending = []; const seen = new Set(); for (const result of products) { const items = result.items.filter(item => { const id = String(item?.RevnId ?? ""); if (seen.has(id)) return false; seen.add(id); return true; }); pending.push(loadActions({ ...result, items }, true, { cachedAt: result.cachedAt, cacheOnly: false })); } await Promise.allSettled(pending); if (!isActive()) return; if (cachedAt > 0) productsContentState.productsLastCachedAt = cachedAt; render(); }).catch(() => { if (isActive()) { products.forEach(blockActions); render(); } }); } return { begin, complete }; } // Source: src/features/opportunities/refresh-all.js function createRefreshAll({ messageTypes, sendRuntimeMessage, activityCache, resetRelatedData, opportunitiesQueryState, productsContentState, consumptionPlansState, consumptionTrendState, 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(); 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, retrySession = () => {} }) { const { document, setTimeout, clearTimeout, matchMedia } = environment; const entries = new Map(); function update(key, state, completed = 0, total = null) { const host = document?.querySelector?.(`#${MODAL_ID} .opportunities-extension-subtitle-row`); if (!host) return; let entry = entries.get(key); if (!entry) { const badge = document.createElement("span"); badge.className = "opportunities-extension-products-progress"; badge.setAttribute("role", "status"); badge.setAttribute("aria-live", "polite"); host.append(badge); entry = { badge, timer: null, hideTimer: null, state: "idle" }; entries.set(key, entry); } if (state === "complete" && entry.state === "complete") return; clearTimeout(entry.timer); clearTimeout(entry.hideTimer); entry.state = state; const label = key === "session" ? "SR session" : key === "sr" ? "Service Requests" : "Actions"; entry.badge.hidden = state === "idle"; entry.badge.setAttribute("data-state", state); entry.badge.setAttribute("data-fading", "false"); entry.badge.setAttribute("aria-busy", String(state === "running")); const count = total === null ? String(completed) : `${completed}/${total}`; entry.badge.textContent = state === "error" ? `${label} unavailable` : state === "complete-with-errors" ? `${label} ${count} (errors)` : state === "complete" ? `${label} ready ${completed}` : `${label} ${count}`; if (key === "session") entry.badge.textContent = state === "complete" ? "SR session ready" : state === "error" ? "SR session unavailable" : "SR session loading"; entry.badge.title = key === "session" ? "Service Request session initialization" : key === "sr" ? "CRM service request records loaded" : "Product workloads checked for Actions in the selected Reference, regardless of display filters"; if (key === "session" && state === "error") { const retry = document.createElement("button"); retry.type = "button"; retry.className = "opportunities-extension-session-retry"; retry.textContent = "Retry"; retry.setAttribute("aria-label", "Retry Service Request session initialization"); retry.addEventListener("click", () => { retry.disabled = true; retrySession(); }); entry.badge.append(retry); } 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("matched") && selected.has("missing")) { selected.clear(); selected.add(value); } else 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, formatDate, openInternalSrForm, 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/A"; 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 add = document.createElement("button"); add.type = "button"; add.className = "opportunities-extension-copy-button opportunities-extension-internal-sr-add"; add.title = "Create Internal Service Request"; add.setAttribute("aria-label", add.title); add.setAttribute("aria-haspopup", "dialog"); 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", "M12 5v14M5 12h14"); path.setAttribute("stroke", "currentColor"); path.setAttribute("stroke-width", "2"); path.setAttribute("stroke-linecap", "round"); icon.append(path); add.append(icon); add.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); close(); openInternalSrForm(item, button); }); heading.append(add); 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); const loadDetails = async () => { if (active !== entry || entry.panel !== panel) return; children.textContent = "Loading SR details..."; children.setAttribute("aria-busy", "true"); children.setAttribute("role", "status"); try { const records = await requestDetails(item.SrId, item.SrNumber); 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/A"; 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/A" : String(key === "LastUpdateDate" || key === "OpenDate" ? formatDate(record[key]) : 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."; const retry = document.createElement("button"); retry.type = "button"; retry.textContent = "Retry"; retry.setAttribute("aria-label", "Retry loading Internal Service Requests"); retry.addEventListener("click", () => { retry.disabled = true; loadDetails(); }); children.append(retry); } finally { if (active === entry && entry.panel === panel) { children.setAttribute("aria-busy", "false"); entry.position(); } } }; detailLoads.push(loadDetails); panel.append(section); }); panel.addEventListener("pointerenter", () => clearTimeout(entry.timer)); panel.addEventListener("pointerleave", leave); panel.addEventListener("focusout", focusOut); host.append(panel); active = entry; detailLoads.forEach(load => load()); button.setAttribute("aria-expanded", "true"); entry.position(); document.addEventListener("pointerdown", entry.outside, true); document.addEventListener("keydown", entry.keydown, true); document.addEventListener("scroll", entry.scroll, true); window.addEventListener("resize", entry.position); } button.addEventListener("pointerenter", open); button.addEventListener("pointerleave", leave); button.addEventListener("focus", open); button.addEventListener("focusout", focusOut); button.addEventListener("click", event => { event.stopPropagation(); if (active === entry && entry.pinned) close(); else { open(); if (active === entry) { entry.pinned = true; entry.panel.querySelector("a, button").focus(); } } }); } return { bind, close }; } // Source: src/features/opportunities/crm-sr-form.js function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportunity, requestSave, notify, formatDate, requestPillars, requestLanguage, requestPlatform, requestServices, createCloseIcon, searchableSelect, environment }) { const { document } = environment; let active = null; function node(tag, className, text) { const element = document.createElement(tag); if (className) element.className = className; if (text !== undefined) element.textContent = text; return element; } function close(restoreFocus = true) { if (!active) return false; if (active.saving) return true; const previous = active; active = null; previous.modal.remove(); if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus(); return true; } function createButton(number) { const button = node("button", "opportunities-extension-add-action", "+"); button.type = "button"; button.title = "Add Service Request"; button.setAttribute("aria-label", `Add Service Request for ${number}`); button.setAttribute("aria-haspopup", "dialog"); button.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); open(number, button); }); return button; } function open(number, trigger) { if (active?.saving) return; const host = document.getElementById(MODAL_ID); if (!host || host.hidden || !allowsCrmActivities() || !isMissing(number)) return; close(false); const modal = node("section", "opportunities-extension-action-form-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "opportunities-extension-sr-form-title"); const dialog = node("div", "opportunities-extension-action-form-dialog opportunities-extension-sr-form-dialog"); const header = node("header", "opportunities-extension-account-plans-header"); const heading = node("div", "opportunities-extension-action-form-heading"); const title = node("h2", "", "Add Service Request"); title.id = "opportunities-extension-sr-form-title"; heading.append(title, node("p", "", String(number))); const opportunity = getOpportunity(number); const details = node("dl", "opportunities-extension-action-form-context"); for (const [label, value] of [ ["Customer", opportunity?.CustomerAccount?.PartyUniqueName], ["Name", opportunity?.Name], ["Close Date", formatDate(opportunity?.EffectiveDate)] ]) { details.append(node("dt", "", label), node("dd", "", value == null || value === "" ? "-" : String(value))); } heading.append(details); const dismiss = node("button", "opportunities-extension-icon-button"); dismiss.type = "button"; dismiss.setAttribute("aria-label", "Close Service Request form"); dismiss.append(createCloseIcon()); dismiss.addEventListener("click", () => close()); header.append(heading, dismiss); const form = node("form", "opportunities-extension-action-form"); form.noValidate = true; const fields = new Map(); const combos = new Map(); const loadingFields = new Set(["primaryPillar", "language", "platform", "services"]); const availableFields = new Set(); let serviceItems = []; const serviceRadios = []; let serviceGroup; const field = node("label", "opportunities-extension-action-form-field"); const select = node("select"); select.id = "opportunities-extension-sr-primary-pillar"; select.name = "primaryPillar"; select.required = true; field.htmlFor = select.id; field.append(node("span", "", "Primary pillar (required)"), select); const autocomplete = searchableSelect(select, field); fields.set("primaryPillar", select); combos.set(select, autocomplete); const status = node("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide"); status.setAttribute("role", "status"); const footer = node("footer", "opportunities-extension-action-form-footer"); const retry = node("button", "", "Retry"); retry.type = "button"; const cancel = node("button", "", "Cancel"); const save = node("button", "opportunities-extension-action-form-save", "Save"); save.type = "submit"; save.disabled = true; cancel.type = "button"; cancel.addEventListener("click", () => close()); footer.append(retry, save, cancel); form.append(field); const extraControls = []; const extraLoads = []; function textField(name, label, type) { const wrapper = node("label", "opportunities-extension-action-form-field" + (type === "textarea" ? " opportunities-extension-action-form-wide" : "")); const input = node(type === "textarea" ? "textarea" : "input"); if (type === "textarea") input.rows = 4; else input.type = type; input.id = `opportunities-extension-sr-${name}`; input.name = name; input.required = true; fields.set(name, input); wrapper.htmlFor = input.id; wrapper.append(node("span", "", `${label} (required)`), input); form.append(wrapper); extraControls.push(input); } textField("customerDeadline", "Customer deadline", "date"); function lookupField(name, label, request, radios = false) { const wrapper = node(radios ? "fieldset" : "div", "opportunities-extension-sr-lookup" + (radios ? " opportunities-extension-action-form-wide" : "")); const content = node("div", radios ? "opportunities-extension-sr-services" : ""); const message = node("p", "opportunities-extension-action-form-status"); const retryOption = node("button", "opportunities-extension-sr-lookup-retry", "Retry"); retryOption.type = "button"; retryOption.setAttribute("aria-label", `Retry ${label}`); retryOption.hidden = true; extraControls.push(retryOption); let input, combo; if (radios) { wrapper.append(node("legend", "", `${label} (required)`)); serviceGroup = wrapper; } else { const field = node("label", "opportunities-extension-action-form-field"); input = node("select"); input.id = `opportunities-extension-sr-${name}`; input.name = name; input.required = true; fields.set(name, input); field.htmlFor = input.id; field.append(node("span", "", `${label} (required)`), input); combo = searchableSelect(input, field); combos.set(input, combo); content.append(field); extraControls.push(combo.input); } wrapper.append(content, message, retryOption); form.append(wrapper); let loading = false; async function loadOptions() { if (loading || !current()) return; loading = true; loadingFields.add(name); availableFields.delete(name); updateSave(); retryOption.hidden = true; message.hidden = false; message.setAttribute("role", "status"); message.textContent = `Loading ${label.toLowerCase()}...`; wrapper.setAttribute("aria-busy", "true"); if (input) { input.disabled = true; input.textContent = ""; combo.sync(); } try { const items = await request(); if (!current()) return; if (items.length) availableFields.add(name); if (input) { const blank = node("option", "", items.length ? `Select ${label.toLowerCase()}` : "No options available"); blank.value = ""; input.append(blank); items.forEach(item => { const option = node("option", "", item.name); option.value = item.code; input.append(option); }); input.value = ""; input.disabled = !items.length; } else { content.textContent = ""; serviceItems = items; serviceRadios.length = 0; const seen = new Set(); items.forEach((item, index) => { if (seen.has(String(item.Id))) return; seen.add(String(item.Id)); const row = node("label", "opportunities-extension-sr-service-option"); const radio = node("input"); radio.type = "radio"; radio.name = "service"; radio.required = true; serviceRadios.push(radio); radio.value = String(item.Id); radio.id = `opportunities-extension-sr-service-${index}`; const copy = node("span"); const abstract = node("span", "opportunities-extension-sr-service-abstract", item.Abstract_c || ""); abstract.id = `${radio.id}-description`; radio.setAttribute("aria-describedby", abstract.id); row.htmlFor = radio.id; copy.append(node("span", "opportunities-extension-sr-service-name", item.RecordName), abstract); row.append(radio, copy); content.append(row); extraControls.push(radio); }); } message.textContent = items.length ? "" : "No options available."; message.hidden = !!items.length; } catch (error) { if (!current()) return; message.setAttribute("role", "alert"); message.textContent = error.message || `Unable to load ${label.toLowerCase()}.`; retryOption.hidden = false; } finally { loading = false; loadingFields.delete(name); if (current()) updateSave(); if (current()) { combo?.sync(); wrapper.setAttribute("aria-busy", "false"); } } } retryOption.addEventListener("click", loadOptions); extraLoads.push(loadOptions); } lookupField("language", "Language of delivery", requestLanguage); lookupField("platform", "Platform", requestPlatform); textField("description", "Description", "textarea"); lookupField("services", "Services", requestServices, true); form.append(status, footer); dialog.append(header, form); modal.append(dialog); host.append(modal); const view = { modal, trigger, loading: false }; active = view; const current = () => active === view && allowsCrmActivities(); function updateSave() { save.disabled = loadingFields.size > 0 || availableFields.size < 4 || Boolean(view.saving); } form.addEventListener("submit", async event => { event.preventDefault(); if (!current() || view.saving || save.disabled) return; const invalid = []; const value = name => String(fields.get(name)?.value || "").trim(); for (const [name, input] of fields) { let valid = !!value(name); if (combos.has(input)) valid = valid && Array.from(input.children).some(option => option.value === value(name)); if (name === "customerDeadline") { const date = value(name); valid = /^\d{4}-\d{2}-\d{2}$/.test(date) && Number.isFinite(Date.parse(`${date}T00:00:00Z`)) && new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) === date; } const control = combos.get(input)?.input || input; control.setAttribute("aria-invalid", String(!valid)); if (!valid) invalid.push(control); } const selected = serviceRadios.find(radio => radio.checked); const service = serviceItems.find(item => String(item.Id) === selected?.value); serviceGroup.setAttribute("aria-invalid", String(!service)); if (!service) invalid.push(serviceRadios[0]); if (invalid.length) { status.hidden = false; status.setAttribute("role", "alert"); status.textContent = "Complete the highlighted required fields with valid values."; invalid[0]?.focus(); return; } const payload = { AccountPartyId: opportunity?.CustomerAccount?.PartyId, OpportunityName_Id_c: opportunity?.OptyId, Title: `${service.RecordName}-${number}-${opportunity?.CustomerAccount?.PartyUniqueName || ""}`, RegionText_c: opportunity?.CustomerAccount?.Region_c, CountryText_c: opportunity?.CustomerAccount?.PrimaryAddress?.CountryCode, ServiceNew_Id_c: service.Id, ProblemDescription: value("description"), Language_c: value("language"), CustomerDeadline_c: value("customerDeadline"), TechPlatformLookUp_c: value("platform"), PrimaryPillarNew_c: value("primaryPillar") }; view.saving = true; const controls = [dismiss, retry, cancel, ...fields.values(), ...serviceRadios, ...extraControls]; const disabled = controls.map(control => [control, control.disabled]); controls.forEach(control => { control.disabled = true; }); combos.forEach(combo => combo.sync()); updateSave(); save.textContent = "Preparing..."; form.setAttribute("aria-busy", "true"); status.hidden = true; try { const prepared = await requestSave({ phase: "prepare", payload }); if (!current() || host.hidden) return; save.textContent = "Saving..."; await requestSave({ phase: "confirm", payload: prepared.payload }); view.saving = false; close(); notify("Service Request saved successfully."); } catch (error) { status.hidden = false; status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to save Service Request."; notify(status.textContent, true); } finally { view.saving = false; if (current()) { disabled.forEach(([control, wasDisabled]) => { control.disabled = wasDisabled; }); combos.forEach(combo => combo.sync()); updateSave(); save.textContent = "Save"; form.setAttribute("aria-busy", "false"); save.focus(); } } }); function option(value, text) { const item = node("option", "", text); item.value = value; select.append(item); } async function load() { if (view.loading || !current()) return; view.loading = true; loadingFields.add("primaryPillar"); availableFields.delete("primaryPillar"); updateSave(); retry.hidden = true; select.disabled = true; select.textContent = ""; option("", "Loading..."); autocomplete.sync(); form.setAttribute("data-state", "loading"); form.setAttribute("aria-busy", "true"); status.hidden = false; status.setAttribute("role", "status"); status.textContent = "Loading primary pillars..."; try { const items = await requestPillars(); if (!current()) return; select.textContent = ""; option("", items.length ? "Select primary pillar" : "No options available"); items.forEach(item => option(item.code, item.name)); select.value = ""; select.disabled = !items.length; if (items.length) availableFields.add("primaryPillar"); form.setAttribute("data-state", "ready"); status.hidden = true; } catch (error) { if (!current()) return; form.setAttribute("data-state", "error"); select.textContent = ""; option("", "Unavailable"); status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to load primary pillars."; retry.hidden = false; } finally { view.loading = false; loadingFields.delete("primaryPillar"); if (current()) updateSave(); if (current()) { autocomplete.sync(); form.setAttribute("aria-busy", "false"); } } } retry.addEventListener("click", load); modal.addEventListener("click", event => { if (event.target === modal) close(); }); modal.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); } if (event.key !== "Tab") return; const controls = [dismiss, autocomplete.input, ...extraControls, retry, save, cancel].filter(control => !control.disabled && !control.hidden); const index = controls.indexOf(document.activeElement); if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus(); } }); dismiss.focus(); load(); extraLoads.forEach(loadOptions => loadOptions()); } return { createButton, close, open }; } // Source: src/features/opportunities/internal-sr-form.js function createInternalSrForm({ MODAL_ID, searchableSelect, formatDate, getOpportunity, requestPlatform, requestServices, requestSave, notify, environment }) { const { document } = environment; let active = null; function node(tag, className, text) { const el = document.createElement(tag); if (className) el.className = className; if (text !== undefined) el.textContent = text; return el; } 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 open(parent, trigger) { if (active?.saving) return; const host = document.getElementById(MODAL_ID); if (!host || host.hidden) return; close(false); const modal = node("section", "opportunities-extension-action-form-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "internal-sr-form-title"); const dialog = node("div", "opportunities-extension-action-form-dialog opportunities-extension-internal-sr-dialog"); const header = node("header", "opportunities-extension-account-plans-header"); const heading = node("div", "opportunities-extension-action-form-heading"); const title = node("h2", "", "Create Internal Service Request"); title.id = "internal-sr-form-title"; const context = node("dl", "opportunities-extension-action-form-context"); const opportunity = getOpportunity(parent.OpportunityName_Id_c?.OptyNumber); for (const [label, value] of [["SR Number", parent.SrNumber], ["Opportunity", opportunity?.Name], ["Title", parent.Title]]) { context.append(node("dt", "", label), node("dd", "", value == null || value === "" ? "N/A" : String(value))); } heading.append(title, context); header.append(heading); const form = node("form", "opportunities-extension-action-form"); form.noValidate = true; const fields = new Map(); const combos = new Map(); const ready = new Set(); function field(name, label, type, wide = false) { const wrapper = node("label", "opportunities-extension-action-form-field" + (wide ? " opportunities-extension-action-form-wide" : "")); const input = node(type === "textarea" || type === "select" ? type : "input"); if (!["textarea", "select"].includes(type)) input.type = type; if (type === "textarea") input.rows = 4; input.id = `internal-sr-${name}`; input.name = name; fields.set(name, input); input.required = name !== "customerDeadline"; input.setAttribute("aria-required", String(input.required)); wrapper.htmlFor = input.id; wrapper.append(node("span", "", input.required ? `${label} (required)` : label), input); form.append(wrapper); return { input, wrapper }; } const titleInput = field("title", "Title", "text", true).input; const deadline = field("customerDeadline", "Customer Deadline", "text").input; deadline.readOnly = true; deadline.value = formatDate(parent.CustomerDeadline_c) || "N/A"; field("internalDeadline", "Internal Deadline", "date"); const platform = field("platform", "Platform", "select", true); field("description", "Description", "textarea", true); const services = field("services", "Services", "select", true); const footer = node("footer", "opportunities-extension-action-form-footer"); const cancel = node("button", "", "Cancel"); const save = node("button", "opportunities-extension-action-form-save", "Save"); save.type = "submit"; save.disabled = true; const feedback = node("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide"); feedback.setAttribute("role", "status"); feedback.hidden = true; cancel.type = "button"; cancel.addEventListener("click", () => close()); footer.append(save, cancel); form.append(feedback, footer); dialog.append(header, form); modal.append(dialog); host.append(modal); const entry = { modal, trigger, saving: false }; active = entry; const validDate = value => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(`${value}T00:00:00Z`)) && new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value; form.addEventListener("submit", async event => { event.preventDefault(); if (active !== entry || entry.saving || ready.size !== 2) return; let firstInvalid = null; for (const [name, input] of fields) { if (!input.required) continue; const value = String(input.value || "").trim(); const valid = !!value && (name !== "internalDeadline" || validDate(value)) && (!combos.has(name) || Array.from(input.children).some(option => option.value === value)); input.setAttribute("aria-invalid", String(!valid)); const visible = combos.get(name)?.input || input; visible.setAttribute("aria-invalid", String(!valid)); if (!valid && !firstInvalid) firstInvalid = visible; } feedback.hidden = false; if (firstInvalid) { feedback.setAttribute("role", "alert"); feedback.textContent = "Complete all required fields with valid values."; firstInvalid.focus(); return; } const value = name => String(fields.get(name).value || "").trim(); const dateParts = value("customerDeadline").match(/^(\d{2})\/(\d{2})\/(\d{4})$/); const payload = { Title: value("title"), ServiceNew_Id_c: value("services"), ProblemDescription: value("description"), CustomerDeadline_c: dateParts ? `${dateParts[3]}-${dateParts[2]}-${dateParts[1]}` : "", TechPlatformLookUp_c: value("platform"), ServiceRequest_Id_SRToInternalSR: String(parent.SrId ?? ""), SvcQuestionsCollection_c: [], InternalDeadline_c: value("internalDeadline") }; entry.saving = true; form.setAttribute("aria-busy", "true"); save.disabled = cancel.disabled = true; save.textContent = "Saving..."; for (const [name, input] of fields) { input.disabled = true; combos.get(name)?.sync(); } feedback.setAttribute("role", "status"); feedback.textContent = "Creating Internal Service Request..."; try { await requestSave(payload); entry.saving = false; close(); notify("Internal Service Request created successfully."); } catch (error) { if (active !== entry) return; feedback.setAttribute("role", "alert"); feedback.textContent = error.message || "Unable to create Internal Service Request."; notify(feedback.textContent, true); } finally { entry.saving = false; if (active === entry) { form.setAttribute("aria-busy", "false"); save.disabled = cancel.disabled = false; save.textContent = "Save"; for (const [name, input] of fields) { input.disabled = false; combos.get(name)?.sync(); } } } }); function load(target, request, service) { const { input, wrapper } = target; input.disabled = true; const combo = searchableSelect(input, wrapper); combos.set(input.name, combo); const status = node("p", "opportunities-extension-action-form-status", "Loading..."); status.setAttribute("role", "status"); wrapper.append(status); wrapper.setAttribute("aria-busy", "true"); Promise.resolve().then(request).then(items => { if (active !== entry) return; const placeholder = node("option", "", items.length ? "Select an option" : "No options available"); placeholder.value = ""; input.append(placeholder); const seen = new Set(); for (const item of items) { const value = String(service ? item.Id : item.code); if (seen.has(value)) continue; seen.add(value); const option = node("option", "", service ? item.RecordName : item.name); option.value = value; if (service && item.Description_c) option.setAttribute("data-description", item.Description_c); input.append(option); } input.value = ""; input.disabled = !items.length; if (items.length) ready.add(input.name); save.disabled = ready.size !== 2; status.textContent = items.length ? "" : "No options available."; status.hidden = !!items.length; combo.sync(); }).catch(error => { if (active !== entry) return; status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to load options. Close and reopen the form to retry."; }).finally(() => { if (active === entry) wrapper.setAttribute("aria-busy", "false"); }); } load(platform, requestPlatform, false); load(services, () => requestServices(parent.SrId), true); modal.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); } if (event.key === "Tab") { const focusable = Array.from(form.querySelectorAll("input, textarea, button")).filter(el => !el.disabled && !el.hidden); const first = focusable[0], last = focusable.at(-1); if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } }); titleInput.focus(); } return { open, close }; } // Source: src/features/opportunities/searchable-select.js function createSearchableSelect({ environment }) { const { document } = environment; function element(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function searchableSelect(select, field, onChange, { floating = false, tooltipContent, listWidth = 0, listMaxHeight = 180, search, searchDelay = 350 } = {}) { let searchTimer, searchVersion = 0, disposed = false; const schedule = environment.setTimeout || globalThis.setTimeout, cancel = environment.clearTimeout || globalThis.clearTimeout; function cancelSearch() { searchVersion++; if (searchTimer) cancel(searchTimer); searchTimer = null; input.removeAttribute("aria-busy"); } 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); const tooltip = tooltipContent ? element("div", "opportunities-extension-select-tooltip") : null; if (tooltip) { tooltip.id = `${select.id}-tooltip`; tooltip.setAttribute("role", "tooltip"); tooltip.hidden = true; box.append(tooltip); } let tooltipAnchor = null; function hideTooltip() { if (!tooltip) return; tooltip.hidden = true; tooltipAnchor?.removeAttribute?.("aria-describedby"); tooltipAnchor = null; } function showTooltip(value, anchor) { hideTooltip(); if (!tooltip || !value) return; const content = tooltipContent(value); if (!content) return; tooltip.replaceChildren(content); tooltip.hidden = false; tooltipAnchor = anchor; anchor.setAttribute("aria-describedby", tooltip.id); const rect = anchor.getBoundingClientRect?.(); const bounds = input.closest?.(".opportunities-extension-action-form-dialog")?.getBoundingClientRect(); if (!rect || !bounds) return; const size = tooltip.getBoundingClientRect(); const left = rect.right + 8 + size.width <= bounds.right ? rect.right + 8 : Math.max(bounds.left + 8, rect.left - size.width - 8); tooltip.style.left = `${left}px`; tooltip.style.top = `${Math.max(bounds.top + 8, Math.min(rect.top, bounds.bottom - size.height - 8))}px`; } function leaveTooltip(event) { if (!tooltip?.contains?.(event.relatedTarget)) hideTooltip(); } tooltip?.addEventListener("mouseleave", hideTooltip); list.addEventListener("scroll", hideTooltip); 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() { if (search) cancelSearch(); hideTooltip(); 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 = tooltipContent ? "" : input.value; } function highlight(withTooltip = true) { 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" }); if (withTooltip && matches[index]) showTooltip(matches[index].value, list.children[index]); } 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() { let bounds = input.closest?.(".opportunities-extension-action-form-dialog")?.getBoundingClientRect(); const anchor = input.getBoundingClientRect?.(); if (!bounds || !anchor) return; if (floating && listWidth && document.documentElement?.clientWidth) { bounds = { top: 0, left: 0, right: document.documentElement.clientWidth, bottom: document.documentElement.clientHeight }; } const below = Math.max(0, bounds.bottom - anchor.bottom - 8); const above = Math.max(0, anchor.top - bounds.top - 8); const opensAbove = below < listMaxHeight && above > below; list.setAttribute("data-placement", opensAbove ? "above" : "below"); list.style.maxHeight = `${Math.min(listMaxHeight, opensAbove ? above : below)}px`; if (floating) { const width = listWidth ? Math.min(Math.max(anchor.width, listWidth), bounds.right - bounds.left - 16) : anchor.width; list.style.position = "fixed"; list.style.left = `${listWidth ? Math.max(bounds.left + 8, Math.min(anchor.left, bounds.right - width - 8)) : anchor.left}px`; list.style.width = `${width}px`; list.style.right = "auto"; list.style.bottom = "auto"; list.style.top = `${opensAbove ? anchor.top - 4 : anchor.bottom + 4}px`; list.style.transform = opensAbove ? "translateY(-100%)" : "none"; } } function render(query = "", statusText = "") { if (input.disabled) return; hideTooltip(); matches = statusText ? [] : options().filter(item => search || normalize(`${item.textContent} ${item.getAttribute?.("data-description") || ""} ${item.value}`).includes(normalize(query))); index = matches.findIndex(item => item.value === select.value); list.textContent = ""; matches.forEach((item, i) => { const row = element("div", "opportunities-extension-action-option", item.textContent); const description = item.getAttribute?.("data-description"); if (description) row.append(element("small", "opportunities-extension-option-description", description)); 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); }); if (tooltip) { row.addEventListener("mouseenter", () => showTooltip(item.value, row)); row.addEventListener("mouseleave", leaveTooltip); } list.append(row); }); if (!matches.length) { const empty = element("div", "opportunities-extension-action-option", statusText || "No results"); empty.setAttribute("role", "status"); list.append(empty); } positionList(); list.hidden = false; input.setAttribute("aria-expanded", "true"); highlight(false); } input.addEventListener("click", () => { render(); input.select?.(); }); function searchInput(event) { if (!search) { render(input.value); return; } cancelSearch(); select.replaceChildren(select.children[0]); select.value = ""; onChange?.(); const query = input.value; render("", query.trim() ? "Searching..." : "Type to search."); if (!query.trim() || event?.isComposing) return; const version = searchVersion; const isCurrent = () => !disposed && version === searchVersion && input.isConnected && document.activeElement === input; input.setAttribute("aria-busy", "true"); searchTimer = schedule(async () => { if (!isCurrent()) return; try { const results = await search(query, isCurrent); if (!isCurrent()) return; const placeholder = select.children[0]; select.replaceChildren(placeholder); for (const result of results) { const option = element("option", "", result.label); option.value = result.value; if (result.description) option.setAttribute("data-description", result.description); select.append(option); } select.value = ""; render(); } catch (error) { if (isCurrent()) render("", (error.message || "Unable to search contacts.") + " Type to retry."); } finally { if (isCurrent()) input.removeAttribute("aria-busy"); } }, searchDelay); } input.addEventListener("input", searchInput); if (search) input.addEventListener("compositionend", searchInput); input.addEventListener("blur", sync); if (tooltip) { input.addEventListener("mouseenter", () => { if (list.hidden) showTooltip(select.value, input); }); input.addEventListener("mouseleave", leaveTooltip); } 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 || (tooltip && !tooltip.hidden))) { event.preventDefault(); event.stopPropagation(); sync(); } else if (event.key === "Tab") sync(); }); sync(); return { input, sync, destroy() { disposed = true; closeList(); } }; } return searchableSelect; } // Source: src/features/opportunities/internal-sr-session.js function createInternalSrSession({ query, environment, sessionUrl, sessionLabel = "Internal SR" }) { const pending = new Map(); let recovery = null; function isUnauthorized(result) { const status = result?.httpStatus ?? result?.debugRequests?.at(-1)?.httpStatus; return status != null ? Number(status) === 401 : /failed with status 401\.$/.test(result?.error || ""); } async function warmSession(id, srNumber) { const document = environment?.document; if (!document?.body) throw new Error(`${sessionLabel} session cannot be initialized on this page.`); const frame = document.createElement("iframe"); // Restore the original session initialization route without opening a tab. frame.src = sessionUrl || "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=SR0001421040&srId=302963804324543&view=showisr"; frame.title = `${sessionLabel} session initialization`; frame.tabIndex = -1; frame.setAttribute("aria-hidden", "true"); frame.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms"); frame.style.cssText = "position:fixed;width:1px;height:1px;left:-10000px;top:0;border:0;opacity:0;pointer-events:none;"; let blocked = false; const failed = () => { blocked = true; }; frame.addEventListener("error", failed); try { document.body.append(frame); // Keep the iframe alive while its application completes authentication. // A load event is not proof of a ready session. Limit and space API checks. for (const delay of [2000, 4000, 8000, 12000, 16000]) { await new Promise(resolve => environment.setTimeout(resolve, delay)); if (blocked) throw new Error(`${sessionLabel} session iframe could not be loaded. Retry after signing in.`); const result = await query(id, srNumber); if (result?.ok) return { id: String(id), result }; if (!isUnauthorized(result)) throw new Error(result?.error || `Unable to load ${sessionLabel} data.`); } throw new Error(`${sessionLabel} session is still unavailable. Retry loading the details after signing in.`); } finally { frame.removeEventListener("error", failed); try { frame.contentWindow?.stop(); } catch { /* SSO may redirect cross-origin. */ } frame.remove(); } } function request(id, srNumber) { const key = String(id); if (!pending.has(key)) { const operation = (async () => { let result = await query(id, srNumber); if (!result?.ok && isUnauthorized(result)) { if (!recovery) { const current = warmSession(id, srNumber); recovery = current; current.finally(() => { if (recovery === current) recovery = null; }).catch(() => {}); } const recovered = await recovery; result = recovered.id === key ? recovered.result : await query(id, srNumber); } if (!result?.ok) throw new Error(result?.error || `Unable to load ${sessionLabel} data.`); return result.items; })().finally(() => pending.delete(key)); pending.set(key, operation); } return pending.get(key); } return { request }; } // Source: src/features/opportunities/sr-session-preparation.js function createSrSessionPreparation({ ensureCurrentUser, allowsCrmActivities, sendRequest, reportProgress, appendLog, onReady = async () => {} }) { let pending = null; let failed = false; function ensure() { if (pending) return pending; pending = (async () => { failed = false; try { reportProgress("running"); // Navigation establishes SR cookies independently of the SPA identity. // Consume user-dependent data only once both preparations have finished. const [, result] = await Promise.all([ensureCurrentUser(), sendRequest()]); appendLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Session initialization failed."); if (!allowsCrmActivities()) { reportProgress("idle"); return; } reportProgress("complete"); await onReady(); } catch { failed = true; reportProgress("error"); } })(); return pending; } function retry() { if (failed) pending = null; return ensure(); } return { ensure, retry }; } // Source: src/features/opportunities/workload-actions.js function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivities, requestConsumerTokenServiceOnce, sendRuntimeMessage, appendBackgroundRequestLog, queueProductsTableRender, productsContentState, messageTypes, reportProgress, activityCache, createActionButton, closeActionForm, setBadgeContent, environment }) { let session = newSession(); function newSession() { return { enabled: true, records: new Map(), queue: [], running: 0, progress: new Set() }; } function updateProgress(current) { if (current !== session || !current.enabled || !current.progress.size) return; const records = [...current.progress]; const completed = records.filter(record => record.status !== "loading").length; const failed = records.filter(record => record.status === "error").length; reportProgress(completed < records.length ? "running" : failed ? "complete-with-errors" : "complete", completed, records.length); } function reset(enabled = true) { closeActionForm(false); session.enabled = false; for (const job of session.queue) job.record.resolveCompletion(); session.queue.length = 0; session = newSession(); session.enabled = enabled; reportProgress("idle"); } function idsFor(products) { return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))]; } function loadProducts(products, refresh = false, cacheOptions = null) { if (!session.enabled || !allowsCrmActivities()) return; const pending = []; if ([...session.progress].every(record => record.status !== "loading")) session.progress.clear(); for (const revnId of idsFor(products)) { if (!refresh && session.records.has(revnId)) { pending.push(session.records.get(revnId).completion); continue; } session.progress.delete(session.records.get(revnId)); const record = { status: "loading", actions: [], hasNext: false }; record.completion = new Promise(resolve => { record.resolveCompletion = resolve; }); pending.push(record.completion); session.records.set(revnId, record); if (!cacheOptions) session.progress.add(record); session.queue.push({ revnId, record, cacheOptions }); } updateProgress(session); pump(session); return Promise.all(pending); } function reopen() { reset(); for (const products of productsContentState.opptyProductsByNumber.values()) loadProducts(products); queueProductsTableRender(); } function blockProducts(products) { if (!allowsCrmActivities()) return; for (const id of idsFor(products)) { session.records.set(id, { status: "error", actions: [], hasNext: false, error: "CRM service requests unavailable." }); } updateProgress(session); } function pump(current) { while (current.enabled && current === session && current.running < 4 && current.queue.length) { const job = current.queue.shift(); current.running += 1; run(current, job).finally(() => { current.running -= 1; pump(current); }); } } async function run(current, { revnId, record, cacheOptions }) { try { const cached = cacheOptions ? await activityCache.read("actions", revnId, cacheOptions.cachedAt) : null; if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return; if (cached) { record.actions = cached.items; record.hasNext = cached.hasNext; record.status = "ready"; return; } if (cacheOptions?.cacheOnly) { record.status = "error"; record.error = "No matching activity cache for this product update."; return; } current.progress.add(record); updateProgress(current); let result; for (let attempt = 1; attempt <= 5; attempt += 1) { try { if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return; const authorizations = await requestConsumerTokenServiceOnce(); if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return; result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions }); if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return; appendBackgroundRequestLog(result?.debugRequests); if (!result?.ok) throw new Error(result?.error || "Unable to load workload actions."); break; } catch (error) { if (attempt === 5) throw error; } } record.actions = result.actions; record.hasNext = result.hasNext; record.status = "ready"; if (cacheOptions) await activityCache.write("actions", revnId, cacheOptions.cachedAt, result.actions, result.hasNext); } catch (error) { if (!current.enabled || current !== session) return; record.status = "error"; record.error = error.message || "Unable to load workload actions."; } finally { record.resolveCompletion(); if (current.enabled && current === session) { updateProgress(current); queueProductsTableRender(); } } } function getSummary(optyNumber) { if (!session.enabled || !allowsCrmActivities() || !optyNumber) return null; const products = productsContentState.opptyProductsByNumber.get(String(optyNumber)); if (!products) return null; const ids = idsFor(products); const records = ids.map(id => session.records.get(id)); const actions = records.flatMap(record => record?.actions || []); const pending = records.some(record => !record || record.status === "loading"); const uncertain = products.error || (products.items || []).some(item => !String(item?.RevnId ?? "").trim()) || records.some(record => record?.status === "error" || record?.hasNext); const principal = authSessionState.principal; const owned = Boolean(principal?.trim()) && actions.some(action => typeof action.owner === "string" && action.owner.trim().toLowerCase() === principal.trim().toLowerCase()); const state = owned ? "matched" : pending ? "loading" : uncertain || !principal ? "error" : "missing"; return { state, actions, records, principal, pending, uncertain }; } async function applySavedAction(optyNumber, revnId, response, submittedAction = {}) { const products = productsContentState.opptyProductsByNumber.get(String(optyNumber)); const id = String(revnId); if (!session.enabled || !allowsCrmActivities() || !idsFor(products).includes(id)) return false; const returned = Array.isArray(response) ? response : Array.isArray(response?.actions) ? response.actions : [response?.action || response]; const saved = returned.filter(action => action && typeof action === "object" && action.id != null && (action.revenueId == null || String(action.revenueId) === id)); if (!saved.length) return false; const previous = session.records.get(id); const actions = [...(previous?.actions || [])]; for (const action of saved) { // The POST can return a display name in owner, unlike the workload GET. const ownerEmail = typeof action.ownerEmail === "string" && action.ownerEmail.trim() ? action.ownerEmail.trim() : submittedAction.ownerEmail || action.owner || ""; const normalized = { ...action, ownerEmail, owner: ownerEmail }; const index = actions.findIndex(item => String(item.id) === String(action.id)); if (index === -1) actions.push(normalized); else actions[index] = { ...actions[index], ...normalized }; } const record = { status: "ready", actions, hasNext: previous?.hasNext || false, completion: Promise.resolve(), resolveCompletion() {} }; session.records.set(id, record); previous?.resolveCompletion?.(); session.progress.delete(previous); queueProductsTableRender(); if (products.cachedAt) await activityCache.write("actions", id, products.cachedAt, actions, record.hasNext); return true; } function createBadge(optyNumber) { const summary = getSummary(optyNumber); if (!summary) return null; const { state, actions, records, principal, pending, uncertain } = summary; const badge = environment.document.createElement("span"); badge.className = "opportunities-extension-crm-sr-badge opportunities-extension-wl-actions-badge"; badge.setAttribute("data-state", state); setBadgeContent(badge, "actions", state === "matched" || state === "missing" ? actions.length : state === "loading" ? "..." : "?"); badge.title = actions.length ? actions.map(action => [ `Action: ${action.name ?? "N/A"}`, `ID: ${action.id ?? "N/A"}`, `Status: ${action.status ?? "N/A"}`, `Owner: ${action.owner || "N/A"}`, `Email: ${action.ownerEmail || "N/A"}`, `Workload: ${action.workloadName || "N/A"}`, `Description: ${action.workloadDescription || "N/A"}`, `Start: ${action.startDate || "N/A"}`, `End: ${action.endDate || "N/A"}` ].join("\n")).join("\n\n") : state === "missing" ? "No actions found for this opportunity's products." : state === "loading" ? "Loading workload actions..." : "Unable to confirm workload actions. Check product RevnId and Debug Requests."; if (records.some(record => record?.hasNext)) badge.title += "\nAdditional actions exist; pagination is not enabled."; const errors = [...new Set(records.filter(record => record?.error).map(record => record.error))]; if (errors.length) badge.title += `\n${errors.join("\n")}`; if (!principal) badge.title += "\nTokenrelay principal is unavailable; ownership cannot be verified."; else if (actions.length && state === "missing") badge.title += "\nNone of the returned actions belong to the signed-in principal."; if (actions.length && (pending || uncertain)) badge.title += "\nSome workloads are still loading or could not be fully checked."; badge.tabIndex = 0; badge.setAttribute("aria-label", badge.title); if (state === "missing") badge.append(createActionButton(String(optyNumber))); return badge; } return { reset, reopen, loadProducts, blockProducts, createBadge, getSummary, applySavedAction }; } // Source: src/features/opportunities/workload-action-form.js function createWorkloadActionForm({ MODAL_ID, allowsCrmActivities, isMissingAction, getOpportunity, getProducts, requestTeamMembers, requestLookup, getCurrentUserId, saveAction, onActionSaved, notify, formatDate, createCloseIcon, searchableSelect, environment }) { const { document } = environment; let active = null; function element(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function close(restoreFocus = true) { if (!active) return false; if (active.saving) return true; const previous = active; active = null; previous.modal.remove(); if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus(); return true; } function createButton(optyNumber) { const button = element("button", "opportunities-extension-add-action", "+"); button.type = "button"; button.title = "Add Action"; button.setAttribute("aria-label", `Add Action for ${optyNumber}`); button.setAttribute("aria-haspopup", "dialog"); button.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); open(optyNumber, button); }); return button; } function open(optyNumber, trigger) { if (!allowsCrmActivities() || !isMissingAction(optyNumber)) return; const host = document.getElementById(MODAL_ID); if (!host || host.hidden) return; close(false); const modal = element("section", "opportunities-extension-action-form-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "opportunities-extension-action-form-title"); const dialog = element("div", "opportunities-extension-action-form-dialog"); const header = element("header", "opportunities-extension-account-plans-header"); const heading = element("div", "opportunities-extension-action-form-heading"); const title = element("h2", "", "Add Action"); title.id = "opportunities-extension-action-form-title"; heading.append(title, element("p", "", String(optyNumber))); const opportunity = getOpportunity(optyNumber); const details = element("dl", "opportunities-extension-action-form-context"); for (const [label, value] of [ ["Customer", opportunity?.CustomerAccount?.PartyUniqueName], ["Name", opportunity?.Name], ["Close Date", formatDate(opportunity?.EffectiveDate)] ]) { details.append(element("dt", "", label), element("dd", "", value == null || value === "" ? "-" : String(value))); } heading.append(details); const dismiss = element("button", "opportunities-extension-icon-button"); dismiss.type = "button"; dismiss.title = "Close"; dismiss.setAttribute("aria-label", "Close Action form"); dismiss.append(createCloseIcon()); dismiss.addEventListener("click", () => close()); header.append(heading, dismiss); const form = element("form", "opportunities-extension-action-form"); form.noValidate = true; const workloads = [...new Map(getProducts(optyNumber).filter(product => /^\d+$/.test(String(product?.RevnId ?? ""))) .map(product => [String(product.RevnId), product])).entries()]; const workloadField = element("label", "opportunities-extension-action-form-field opportunities-extension-action-form-wide"); const workload = element("select"); workload.id = "opportunities-extension-action-form-workload"; workloadField.htmlFor = workload.id; function option(select, value, label) { const item = element("option", "", label); item.value = value; select.append(item); } option(workload, "", "Select workload"); for (const [id, product] of workloads) option(workload, id, `${product.WorkloadName_c || product.ProdGroupName || "Workload"} (${id})`); workload.value = workloads.length === 1 ? workloads[0][0] : ""; workloadField.hidden = workloads.length <= 1; workloadField.append(element("span", "", "Workload"), workload); const status = element("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide"); status.setAttribute("role", "status"); form.append(workloadField, status); const searchable = new Map(); searchable.set(workload, searchableSelect(workload, workloadField, () => loadOptions())); const fields = []; const selects = []; const lookupKeys = { role: "WWB_ACTION_ROLE", action: "WWB_ACTION_NAME", team: "WWB_ACTION_TEAM", status: "WWB_ACTION_STATUS", complexity: "WWB_ACTION_COMPLEXITY" }; for (const [name, label, type] of [ ["owner", "Owner", "select"], ["role", "Role", "select"], ["action", "Action", "select"], ["team", "Team", "select"], ["startDate", "Start date", "date"], ["endDate", "End Date", "date"], ["status", "Status", "select"], ["complexity", "Complexity", "select"], ["notes", "Notes", "textarea"] ]) { const field = element("label", "opportunities-extension-action-form-field"); const input = element(type === "select" || type === "textarea" ? type : "input"); if (type === "select") selects.push({ input, label, name }); else if (type === "textarea") input.rows = 5; else input.type = type; input.disabled = true; input.required = name !== "notes"; input.setAttribute("aria-required", String(input.required)); input.name = name; input.id = `opportunities-extension-action-form-${name}`; field.htmlFor = input.id; if (["notes", "team", "action"].includes(name)) field.className += " opportunities-extension-action-form-wide"; field.append(element("span", "", `${label} (${input.required ? "required" : "optional"})`), input); if (type === "select") searchable.set(input, searchableSelect(input, field)); form.append(field); fields.push(input); } const footer = element("footer", "opportunities-extension-action-form-footer"); const save = element("button", "opportunities-extension-action-form-save", "Save"); save.type = "submit"; save.disabled = true; save.title = "Save Action"; const retry = element("button", "", "Retry"); retry.type = "button"; retry.hidden = true; const cancel = element("button", "", "Cancel"); cancel.type = "button"; cancel.addEventListener("click", () => close()); footer.append(retry, save, cancel); form.append(footer); dialog.append(header, form); modal.append(dialog); host.append(modal); const view = { modal, trigger, generation: 0, lookup: null, loadingId: null }; active = view; const isOpen = () => active === view && allowsCrmActivities(); let ready = false; function validate() { const invalid = []; const value = name => String(fields.find(field => field.name === name)?.value || "").trim(); for (const field of fields) { let valid = !field.required || !!value(field.name); if (field.required && searchable.has(field)) valid = Array.from(field.children).some(option => option.value && option.value === field.value); if (field.type === "date") { const date = value(field.name); valid = /^\d{4}-\d{2}-\d{2}$/.test(date) && Number.isFinite(Date.parse(`${date}T00:00:00.000Z`)); if (valid) valid = new Date(`${date}T00:00:00.000Z`).toISOString().slice(0, 10) === date; if (field.name === "endDate" && value("startDate") > date) valid = false; } const control = searchable.get(field)?.input || field; control.setAttribute("aria-invalid", String(!valid)); if (!valid) invalid.push(control); } return { invalid, value }; } form.addEventListener("submit", async event => { event.preventDefault(); if (!ready || !isOpen() || view.saving) return; const { invalid, value } = validate(); if (invalid.length) { status.hidden = false; status.setAttribute("role", "alert"); status.textContent = "Complete the highlighted required fields. End date must be on or after start date."; invalid[0].focus(); return; } const userId = getCurrentUserId(); if (!userId) { notify("Unable to save: current user is unavailable.", true); return; } const payload = { parentType: "WORKLOAD", parentId: workload.value, action: { ownerEmail: value("owner"), role: value("role"), name: value("action"), team: value("team"), tags: "", startDate: `${value("startDate")}T00:00:00.000Z`, endDate: `${value("endDate")}T00:00:00.000Z`, status: value("status"), complexity: value("complexity"), notes: value("notes") ? [value("notes")] : [], objective: "", createdBy: userId, updatedBy: userId } }; view.saving = true; save.disabled = cancel.disabled = dismiss.disabled = workload.disabled = true; fields.forEach(field => { field.disabled = true; }); searchable.forEach(control => control.sync()); save.textContent = "Saving..."; form.setAttribute("aria-busy", "true"); status.hidden = true; try { const result = await saveAction(payload, isOpen); if (!result) return; view.saving = false; close(); try { await onActionSaved(optyNumber, payload.parentId, result.result, payload.action); } catch { notify("Action saved, but the local badge could not be updated.", true); return; } notify("Action saved successfully."); } catch (error) { status.hidden = false; status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to save Action."; notify(status.textContent, true); } finally { view.saving = false; if (active === view) { save.disabled = cancel.disabled = dismiss.disabled = workload.disabled = false; fields.forEach(field => { field.disabled = false; }); searchable.forEach(control => control.sync()); save.textContent = "Save"; form.setAttribute("aria-busy", "false"); } } }); const settled = promise => promise.then(result => ({ result }), error => ({ error })); async function loadOptions(refresh = false) { const id = workload.value; if (!isOpen() || (view.loadingId === id && !refresh)) return; const generation = ++view.generation; const isCurrent = () => isOpen() && view.generation === generation; ready = false; save.disabled = true; retry.hidden = true; status.hidden = false; status.setAttribute("role", "status"); fields.forEach(input => { input.disabled = true; }); selects.forEach(({ input }) => { input.textContent = ""; option(input, "", "Loading..."); }); const syncSearch = () => selects.forEach(({ input }) => searchable.get(input).sync()); syncSearch(); if (!workloads.length) { form.setAttribute("data-state", "error"); form.setAttribute("aria-busy", "false"); status.setAttribute("role", "alert"); status.textContent = "No product with a valid RevnId is available for this opportunity."; selects.forEach(({ input }) => { input.textContent = ""; option(input, "", "Unavailable"); }); syncSearch(); return; } if (!view.lookup || refresh) view.lookup = settled(requestLookup(isOpen)); if (!workloads.some(([value]) => value === id)) { view.loadingId = null; form.setAttribute("data-state", "waiting"); form.setAttribute("aria-busy", "false"); status.textContent = "Select a workload to load Action options."; return; } view.loadingId = id; form.setAttribute("data-state", "loading"); form.setAttribute("aria-busy", "true"); status.textContent = "Loading Action options..."; try { const [lookup, team] = await Promise.all([view.lookup, settled(requestTeamMembers(id, isCurrent))]); if (!isCurrent()) return; if (lookup.error || team.error) throw lookup.error || team.error; const values = selects.map(({ name }) => name === "owner" ? team.result.members.map(({ email }) => ({ code: email, name: email })) : lookup.result.lookups[lookupKeys[name]]); const defaults = { owner: getCurrentUserId(), role: "Cloud Architect", action: "Success Plan & Consumption review", status: "Complete" }; const normalize = value => String(value ?? "").trim().toLowerCase(); for (const [index, { input, label, name }] of selects.entries()) { input.textContent = ""; option(input, "", values[index].length ? `Select ${label.toLowerCase()}` : "No options available"); for (const item of values[index]) option(input, item.code, item.name); const match = defaults[name] && values[index].find(item => normalize(name === "owner" ? item.code : item.name) === normalize(defaults[name])); input.value = match ? match.code : ""; } fields.forEach(input => { input.disabled = false; }); selects.forEach(({ input }, index) => { input.disabled = !values[index].length; }); form.setAttribute("data-state", "ready"); ready = true; save.disabled = false; status.hidden = true; syncSearch(); if (document.activeElement === dismiss) { const first = fields.find(field => !field.disabled); (searchable.get(first)?.input || first)?.focus(); } } catch (error) { if (!isCurrent()) return; form.setAttribute("data-state", "error"); status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to load Action options."; selects.forEach(({ input }) => { input.textContent = ""; option(input, "", "Unavailable"); }); syncSearch(); retry.hidden = false; } finally { if (isCurrent()) { view.loadingId = null; form.setAttribute("aria-busy", "false"); } } } retry.addEventListener("click", () => { if (!view.loadingId) loadOptions(true); }); workload.addEventListener("change", () => loadOptions()); modal.addEventListener("click", event => { if (event.target === modal) close(); }); modal.addEventListener("keydown", event => { if (event.key !== "Tab") return; const controls = [dismiss, ...(!workloadField.hidden ? [workload] : []), ...fields, retry, save, cancel] .map(control => searchable.get(control)?.input || control) .filter(control => !control.disabled && !control.hidden); const index = controls.indexOf(document.activeElement); if (!controls.length) { event.preventDefault(); return; } if (index === -1 || (!event.shiftKey && index === controls.length - 1) || (event.shiftKey && index === 0)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus(); } }); if (!workloadField.hidden) searchable.get(workload).input.focus(); else dismiss.focus(); loadOptions(); } return { createButton, open, close }; } // Source: src/features/opportunities/workload-action-options.js function createWorkloadActionOptions({ requestConsumerTokenServiceOnce, sendRuntimeMessage, appendBackgroundRequestLog, messageTypes }) { async function request(type, workloadId, isCurrent, payload) { const authorizations = await requestConsumerTokenServiceOnce(); if (!isCurrent()) return null; const authorization = authorizations?.workloadActions; if (!authorization?.value) throw new Error("WWB-TS uiaasHeader.value is unavailable."); const result = await sendRuntimeMessage({ type, ...(workloadId === undefined ? {} : { workloadId }), ...(payload ? { payload } : {}), authorization }); appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to load Action options."); return result; } return { save: (payload, isCurrent) => request(messageTypes.requestMgmtAction, undefined, isCurrent, payload), requestTeamMembers: (id, isCurrent) => request(messageTypes.requestActionTeamMembers, id, isCurrent), requestLookup: isCurrent => request(messageTypes.requestActionFindLookup, undefined, isCurrent) }; } // Source: src/features/opportunities/fiscal-periods.js function createOpportunitiesFiscalPeriods() { // Oracle fiscal years start on June 1; each range ends at the next period's start. const FISCAL_START_MONTH = 5; function getFiscalPeriodRange(period, currentDate) { const currentYear = currentDate.getFullYear(); const currentMonth = currentDate.getMonth(); const fiscalStartYear = currentMonth >= FISCAL_START_MONTH ? currentYear : currentYear - 1; const currentQuarterIndex = Math.floor(((currentMonth - FISCAL_START_MONTH + 12) % 12) / 3); if (period === "Current Fiscal Year") { return createDateRange(fiscalStartYear, FISCAL_START_MONTH, fiscalStartYear + 1, FISCAL_START_MONTH); } 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 startMonth = FISCAL_START_MONTH + (quarterOffset * 3); return createDateRange(fiscalStartYear, startMonth, fiscalStartYear, startMonth + (numberOfQuarters * 3)); } function createDateRange(startYear, startMonth, endYear, endMonth) { return { startCloseDate: formatOracleDate(new Date(startYear, startMonth, 1)), endCloseDate: formatOracleDate(new Date(endYear, endMonth, 1)) }; } 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({ tableSorting, ACCOUNT_DETAIL_URL, OPPORTUNITIES_COLUMNS, OPPORTUNITIES_TABLE_ID, OPPORTUNITY_DETAIL_URL, createCloudConsumptionButton, createInstallBaseButton, createServiceRequestsButton, createOrganizationChartButton, createCopyOpportunityButton, createDetailLink, createProductsCountButton, createProductsRampBadge, createProjectionIndicator, createCrmServiceRequestBadge, closeCrmSrPopover, createWorkloadActionsBadge, createProductsDetailRow, createStageBadge, createStatusBadge, displayOpportunityValue, getNestedValue, getSortedOpportunities, opportunitiesTableState, productsContentState, renderCustomerFilter, renderOwnerFilter, renderActivityFilters, renderStageDashboard, renderCalendar, 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: tableSorting.read("overview", OPPORTUNITIES_COLUMNS).key, sortDirection: tableSorting.read("overview", OPPORTUNITIES_COLUMNS).direction }; 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 = opportunitiesTableState.opportunitiesTableState.status === "loading" ? "Loading opportunities..." : "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.key === "revenue") header.setAttribute("data-align", "money"); 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.disabled = opportunitiesTableState.opportunitiesTableState.status === "loading"; 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); renderCalendar?.(items, opportunitiesTableState.opportunitiesTableState); if (opportunitiesTableState.opportunitiesTableState.status === "loading") { for (let index = 0; index < 8; index++) { const row = document.createElement("tr"); row.className = "opportunities-extension-table-skeleton-row"; row.setAttribute("aria-hidden", "true"); OPPORTUNITIES_COLUMNS.forEach(column => { const cell = document.createElement("td"); cell.setAttribute("data-column", column.key); if (column.key === "revenue") cell.setAttribute("data-align", "money"); const bar = document.createElement("span"); bar.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-table-skeleton-text"; cell.append(bar); if (column.key === "name" || column.key === "customer") { const actions = document.createElement("span"); actions.className = "opportunities-extension-cell-actions"; for (let action = 0; action < 4; action++) { const badge = document.createElement("span"); badge.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-table-skeleton-badge"; actions.append(badge); } cell.append(actions); } row.append(cell); }); tableBody.append(row); } } else if (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 === "revenue") cell.setAttribute("data-align", "money"); 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, item); if (rampBadge) productActions.append(rampBadge); } const projectionIndicator = createProjectionIndicator(productsResult, item, productsContentState.expandedProductOptyNumbers.has(String(item.OptyNumber))); if (projectionIndicator) productActions.append(projectionIndicator); 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), createOrganizationChartButton(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) { if (!OPPORTUNITIES_COLUMNS.some(column => column.key === key && column.sortable !== false)) return; const isSameColumn = opportunitiesTableState.opportunitiesTableState.sortKey === key; opportunitiesTableState.opportunitiesTableState.sortKey = key; opportunitiesTableState.opportunitiesTableState.sortDirection = isSameColumn && opportunitiesTableState.opportunitiesTableState.sortDirection === "ascending" ? "descending" : "ascending"; tableSorting.save("overview", { key, direction: opportunitiesTableState.opportunitiesTableState.sortDirection }); renderOpportunitiesTable(); } return { setOpportunitiesTableState, renderOpportunitiesTable, sortOpportunitiesBy }; } // Source: src/features/opportunities/calendar.js function createOpportunityCalendar({ environment, loadLibrary, getState, getItems, getVersion, getRange, formatRevenue, formatDate, getOwnerName, createDetailLink, createCopyButton, createCloseIcon, createStageBadge, createStatusBadge, opportunityUrl, modalId, closeSrPopover, createViewIcon, productTypeSelection, onProductTypeChange, createPrintButton }) { const { document } = environment; let elements = null; let calendar = null; let library = null; let selectedView = "overview"; let anchoredVersion = null; let details = null; let lastEvents = ""; let dayCounts = new Map(); let dayEvents = new Map(); const dayBadges = new Map(); let resizeObserver = null; let layoutFrame = null; let positionToday = false; function scheduleLayout() { const win = environment.window; if (!win?.requestAnimationFrame || layoutFrame !== null) return; layoutFrame = win.requestAnimationFrame(() => { layoutFrame = null; if (calendar && selectedView === "calendar") { calendar.updateSize(); if (positionToday && !elements.calendarHost.hidden) { const scroller = elements.calendarHost.querySelector(".fc-multimonth"); if (!scroller?.clientHeight) return; const today = dateKey(new Date()); const range = getRange(); const cell = range && today >= range.startCloseDate.slice(0, 10) && today < range.endCloseDate.slice(0, 10) ? elements.calendarHost.querySelector(`[data-date="${today}"]:not(.fc-day-other)`) : null; if (cell) { const first = new Date(`${today}T12:00:00`); first.setDate(first.getDate() - first.getDay() - 7); const last = new Date(first); last.setDate(last.getDate() + 20); const start = dateKey(first), end = dateKey(last); // A week crossing a month boundary has two rendered rows. Include both. const rows = [...scroller.querySelectorAll(".fc-daygrid-body tr")].filter(row => [...row.querySelectorAll("[data-date]:not(.fc-day-other)")].some(day => { const date = day.getAttribute("data-date"); return date >= start && date <= end; })); const headings = [...new Set(rows.map(row => row.closest(".fc-multimonth-month")?.querySelector(".fc-multimonth-header")))].filter(Boolean); const headerSpace = headings.reduce((sum, header) => sum + header.getBoundingClientRect().height, 0); const height = Math.max(48, Math.floor((scroller.clientHeight - headerSpace - 20) / Math.max(1, rows.length))); elements.calendarHost.style.setProperty("--calendar-week-height", `${height}px`); const target = rows[0] || cell; const inset = (headings[0]?.getBoundingClientRect().height || 0) + 8; scroller.scrollTop += target.getBoundingClientRect().top - scroller.getBoundingClientRect().top - inset; } else scroller.scrollTop = 0; positionToday = false; } } }); } function dateKey(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; } function fiscalWeekLabel(date) { // FullCalendar's week hook supplies a UTC date marker, not a local instant. const start = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); start.setUTCDate(start.getUTCDate() - start.getUTCDay()); const end = new Date(start.getTime() + 6 * 86400000); const year = end.getUTCMonth() >= 5 ? end.getUTCFullYear() : end.getUTCFullYear() - 1; const fiscalStart = new Date(Date.UTC(year, 5, 1)); const firstSunday = fiscalStart.getTime() - fiscalStart.getUTCDay() * 86400000; const representative = start < fiscalStart ? fiscalStart : start; const quarter = Math.floor(((representative.getUTCMonth() + 7) % 12) / 3) + 1; const week = Math.round((start.getTime() - firstSunday) / (7 * 86400000)) + 1; return `FY${String(year + 1).slice(-2)}-Q${quarter}-W${week}`; } function updateDayBadge(badge, key) { const count = dayCounts.get(key) || 0; badge.hidden = count === 0; badge.textContent = String(count); badge.title = `${count} ${count === 1 ? "opportunity" : "opportunities"}`; badge.setAttribute("aria-label", badge.title); badge.setAttribute("data-empty", String(count === 0)); } function renderDayCards(container, key) { container.textContent = ""; for (const event of dayEvents.get(key) || []) { const item = event.extendedProps.item; const button = node("button", `opportunities-calendar-day-card fc-daygrid-event ${event.classNames.join(" ")}`); button.type = "button"; const card = node("div", "opportunities-calendar-event-content"); const summary = `${item.OptyNumber || "-"} | ${item.ForecastGroup_c || "N/A"} | ${formatRevenue(item)}`; card.append(node("strong", "", event.title), node("span", "", summary)); button.title = `${event.title}\n${summary}`; button.append(card); button.addEventListener("click", () => showDetails(item, button)); container.append(button); } } function node(tag, className, text) { const el = document.createElement(tag); el.className = className; if (text !== undefined) el.textContent = text; return el; } function closeDate(value) { if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}(?:T|$)/.test(value)) return null; const day = value.slice(0, 10); const date = new Date(`${day}T00:00:00Z`); return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === day ? day : null; } function eventsFor(items) { return items.flatMap((item, index) => { const start = closeDate(item.EffectiveDate); if (!start) return []; return [{ id: String(item.OptyNumber || item.OptyId || index), title: String(item.Name || item.OptyNumber || "-"), start, allDay: true, extendedProps: { item }, classNames: [`opportunities-calendar-stage-${stage(item)}`] }]; }); } function stage(item) { return ["SQL", "PIPELINE", "UPSIDE", "FORECAST", "WON"].includes(item.ForecastGroup_c) ? item.ForecastGroup_c : "OTHER"; } function initialDate(items, now = new Date()) { const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; const dates = items.map(item => closeDate(item.EffectiveDate)).filter(Boolean).sort(); return dates.find(date => date >= today) || dates.at(-1) || today; } function closeDetails(restoreFocus = true) { if (!details) return false; const previous = details; details = null; previous.overlay.remove(); if (restoreFocus) (previous.trigger?.isConnected ? previous.trigger : elements?.tabs.calendar)?.focus(); return true; } function showDialog(heading, content, contentControls, trigger) { closeDetails(false); const host = document.getElementById(modalId); if (!host || host.hidden) return; const overlay = node("section", "opportunities-extension-action-form-modal opportunities-calendar-detail"); overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-modal", "true"); overlay.setAttribute("aria-labelledby", "opportunities-calendar-detail-title"); const panel = node("div", "opportunities-extension-action-form-dialog"); const header = node("header", "opportunities-extension-account-plans-header"); const title = node("h2", "", heading); title.id = "opportunities-calendar-detail-title"; const dismiss = node("button", "opportunities-extension-icon-button"); dismiss.type = "button"; dismiss.title = "Close"; dismiss.setAttribute("aria-label", "Close opportunity details"); dismiss.append(createCloseIcon()); dismiss.addEventListener("click", () => closeDetails()); header.append(title, dismiss); panel.append(header, content); overlay.append(panel); host.append(overlay); details = { overlay, trigger }; overlay.addEventListener("click", event => { if (event.target === overlay) closeDetails(); }); overlay.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); closeDetails(); } if (event.key === "Tab") { const controls = [dismiss, ...contentControls]; const index = controls.indexOf(document.activeElement); if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus(); } } }); dismiss.focus(); } function showDetails(item, trigger) { const content = node("div", "opportunities-calendar-detail-content"); const link = createDetailLink(item.OptyNumber || "-", item.OptyNumber ? opportunityUrl + encodeURIComponent(item.OptyNumber) : ""); const identity = node("div", "opportunities-extension-opty-number-content"); identity.append(link); const copy = item.OptyNumber ? createCopyButton(item.OptyNumber) : null; if (copy) identity.append(copy); const summary = node("dl", "opportunities-extension-action-form-context"); for (const [label, value] of [["Customer", item.CustomerAccount?.PartyUniqueName], ["Owner", getOwnerName(item)], ["Close Date", closeDate(item.EffectiveDate) ? formatDate(item.EffectiveDate) : "N/A"], ["Amount", formatRevenue(item)]]) { summary.append(node("dt", "", label), node("dd", "", String(value || "-"))); } const badges = node("div", "opportunities-calendar-detail-badges"); badges.append(createStatusBadge(item.StatusCode), createStageBadge(item.ForecastGroup_c)); content.append(identity, summary, badges); showDialog(item.Name || item.OptyNumber || "Opportunity", content, item.OptyNumber ? [link, copy] : [], trigger); } function mount(body, tableSurface) { resizeObserver?.disconnect(); if (layoutFrame !== null) environment.window?.cancelAnimationFrame(layoutFrame); layoutFrame = null; calendar?.destroy(); calendar = null; lastEvents = ""; anchoredVersion = null; selectedView = "overview"; dayBadges.clear(); dayCounts.clear(); closeDetails(false); const workspace = node("section", "opportunities-extension-views"); const tablist = node("div", "opportunities-extension-view-tabs"); tablist.setAttribute("role", "tablist"); tablist.setAttribute("aria-label", "Opportunity views"); const tabs = {}; const surface = node("section", "opportunities-extension-calendar"); const message = node("p", "opportunities-calendar-message"); message.setAttribute("role", "status"); const calendarHost = node("div", "opportunities-calendar-grid"); const undated = node("details", "opportunities-calendar-undated"); surface.append(message, calendarHost, undated); for (const [key, label, panel] of [["overview", "Overview", tableSurface], ["calendar", "Calendar", surface]]) { const tab = node("button", ""); tab.type = "button"; tab.id = `opportunities-view-${key}-tab`; tab.append(createViewIcon(key), node("span", "", label)); panel.id = `opportunities-view-${key}`; panel.setAttribute("role", "tabpanel"); panel.setAttribute("aria-labelledby", tab.id); tab.setAttribute("role", "tab"); tab.setAttribute("aria-controls", panel.id); tab.addEventListener("click", () => selectView(key)); tab.addEventListener("keydown", event => { if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return; event.preventDefault(); const next = event.key === "Home" ? "overview" : event.key === "End" ? "calendar" : key === "overview" ? "calendar" : "overview"; selectView(next); tabs[next].focus(); }); tabs[key] = tab; tablist.append(tab); } const viewbar = node("div", "opportunities-view-bar"); viewbar.append(tablist); if (productTypeSelection) { const filters = node("div", "opportunities-product-type-filters"); const filterButtons = new Map(); filters.setAttribute("role", "group"); filters.setAttribute("aria-label", "Opportunity type"); for (const type of ["Booking", "Workload"]) { const button = node("button", "opportunities-product-type-filter", type); button.type = "button"; button.setAttribute("data-type", type); button.setAttribute("aria-pressed", String(productTypeSelection.has(type))); button.addEventListener("click", () => { if (["Booking", "Workload"].every(value => productTypeSelection.has(value))) { productTypeSelection.clear(); productTypeSelection.add(type); } else if (productTypeSelection.has(type)) productTypeSelection.delete(type); else productTypeSelection.add(type); for (const [value, filterButton] of filterButtons) { filterButton.setAttribute("aria-pressed", String(productTypeSelection.has(value))); } onProductTypeChange(); }); filterButtons.set(type, button); filters.append(button); } if (createPrintButton) filters.append(createPrintButton()); viewbar.append(filters); } workspace.append(viewbar, tableSurface, surface); body.append(workspace); elements = { workspace, tableSurface, surface, message, calendarHost, undated, tabs }; if (environment.window?.ResizeObserver) { resizeObserver = new environment.window.ResizeObserver(scheduleLayout); resizeObserver.observe(calendarHost); } selectView("overview"); } function selectView(view) { if (!elements || !["overview", "calendar"].includes(view)) return; if (view === "calendar" && selectedView !== "calendar") positionToday = true; closeDetails(false); closeSrPopover(); selectedView = view; elements.tableSurface.hidden = view !== "overview"; elements.surface.hidden = view !== "calendar"; for (const [key, tab] of Object.entries(elements.tabs)) { tab.setAttribute("aria-selected", String(view === key)); tab.tabIndex = view === key ? 0 : -1; } render(getItems(), getState()); if (view === "calendar") calendar?.updateSize(); scheduleLayout(); } function render(items, state) { if (!elements || selectedView !== "calendar") return; const { surface, message, calendarHost, undated } = elements; surface.setAttribute("aria-busy", String(state.status === "loading")); if (["loading", "error", "idle"].includes(state.status)) { message.textContent = state.message || (state.status === "loading" ? "Loading opportunities..." : "No opportunities found."); message.hidden = false; calendarHost.hidden = true; undated.hidden = true; closeDetails(false); return; } message.textContent = items.length ? "" : "No opportunities found."; message.hidden = !!items.length; calendarHost.hidden = false; const range = getRange(); if (!range) { calendarHost.hidden = true; return; } const start = range.startCloseDate.slice(0, 10); const end = range.endCloseDate.slice(0, 10); const visibleRange = { start, end }; // Fiscal ranges end at the next period's start (exclusive). const events = eventsFor(items).filter(event => event.start >= start && event.start < end); dayCounts = new Map(); dayEvents = new Map(); for (const event of events) { dayCounts.set(event.start, (dayCounts.get(event.start) || 0) + 1); if (!dayEvents.has(event.start)) dayEvents.set(event.start, []); dayEvents.get(event.start).push(event); } for (const { badge, key, cards } of dayBadges.values()) { updateDayBadge(badge, key); renderDayCards(cards, key); } const anchor = start; if (!calendar) { try { library ||= loadLibrary(); calendar = new library.Calendar(calendarHost, { initialView: "referenceMonths", initialDate: anchor, height: "100%", fixedWeekCount: false, views: { referenceMonths: { type: "multiMonth", visibleRange, dayMaxEvents: false, dayMaxEventRows: false } }, visibleRange, validRange: visibleRange, showNonCurrentDates: false, multiMonthMaxColumns: 1, multiMonthMinWidth: 0, multiMonthTitleFormat: { month: "long", year: "numeric" }, datesSet: scheduleLayout, locale: "en", firstDay: 0, editable: false, selectable: false, eventInteractive: true, headerToolbar: false, dayMaxEvents: false, dayMaxEventRows: false, displayEventTime: false, weekNumbers: true, weekNumberContent: ({ date }) => ({ domNodes: [node("span", "opportunities-calendar-fiscal-week", fiscalWeekLabel(date))] }), dayCellDidMount: ({ el, date }) => { const badge = node("span", "opportunities-calendar-day-count"); const key = dateKey(date); updateDayBadge(badge, key); const cards = node("div", "opportunities-calendar-day-cards"); cards.tabIndex = 0; cards.setAttribute("role", "region"); cards.setAttribute("aria-label", `Opportunities for ${formatDate(key)}`); renderDayCards(cards, key); el.querySelector(".fc-daygrid-day-frame").append(badge, cards); dayBadges.set(el, { badge, key, cards }); }, dayCellWillUnmount: ({ el }) => { dayBadges.get(el)?.badge.remove(); dayBadges.get(el)?.cards.remove(); dayBadges.delete(el); }, // MultiMonth imposes its own event limit. Render all cards in our scrollable day region. eventOrder: "title", eventDisplay: "none", events, eventContent: ({ event }) => { const item = event.extendedProps.item; const card = node("div", "opportunities-calendar-event-content"); const summary = node("span", "", `${item.OptyNumber || "-"} | ${item.ForecastGroup_c || "N/A"} | ${formatRevenue(item)}`); summary.title = summary.textContent; card.append(node("strong", "", event.title), summary); return { domNodes: [card] }; }, eventDidMount: ({ el, event }) => { el.title = `${event.title}\n${event.extendedProps.item.CustomerAccount?.PartyUniqueName || ""}\n${formatDate(event.startStr)} | ${event.extendedProps.item.ForecastGroup_c || "-"}`; el.setAttribute("role", "button"); el.setAttribute("aria-label", el.title); }, eventClick: ({ event, el, jsEvent }) => { jsEvent.preventDefault(); showDetails(event.extendedProps.item, el); } }); calendar.render(); lastEvents = JSON.stringify(events); anchoredVersion = getVersion(); } catch { calendar?.destroy(); calendar = null; message.textContent = "Unable to display the calendar. Use the overview or try again."; message.hidden = false; calendarHost.hidden = true; undated.hidden = true; return; } } const signature = JSON.stringify(events); calendar.batchRendering(() => { if (anchoredVersion !== getVersion()) { positionToday = true; calendar.setOption("views", { referenceMonths: { type: "multiMonth", visibleRange, dayMaxEvents: false, dayMaxEventRows: false } }); calendar.setOption("visibleRange", visibleRange); calendar.setOption("validRange", visibleRange); calendar.gotoDate(anchor); anchoredVersion = getVersion(); closeDetails(false); const scroller = calendarHost.querySelector?.(".fc-multimonth"); if (scroller) scroller.scrollTop = 0; } if (signature !== lastEvents) { calendar.removeAllEvents(); calendar.addEventSource(events); lastEvents = signature; closeDetails(false); } }); const missing = items.filter(item => !closeDate(item.EffectiveDate)); undated.hidden = !missing.length; undated.textContent = ""; if (missing.length) { undated.append(node("summary", "", `No valid Close Date (${missing.length})`)); for (const item of missing) { const button = node("button", "", `${item.OptyNumber || "-"} | ${item.Name || "-"}`); button.type = "button"; button.addEventListener("click", () => showDetails(item, button)); undated.append(button); } } scheduleLayout(); } return { mount, render, selectView, closeDetails, closeDate, eventsFor, initialDate, fiscalWeekLabel }; } // 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("en-US")}/${totalCount.toLocaleString("en-US")}` ); 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("en-US", { 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, matchesProductType = () => true, 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 (STAGE_OPTIONS.every(value => opportunitiesFilterState.selectedStages.has(value))) { opportunitiesFilterState.selectedStages.clear(); opportunitiesFilterState.selectedStages.add(stage); } else 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 (STATUS_OPTIONS.every(value => opportunitiesFilterState.selectedStatuses.has(value))) { opportunitiesFilterState.selectedStatuses.clear(); opportunitiesFilterState.selectedStatuses.add(status); } else 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; } } // Keep stored selections intact: narrowing a menu must not turn an implicit // "all" into a second filter that prevents expanding the original selection. function getAvailableCustomerOptions() { const items = opportunitiesTableState.opportunitiesTableState.items; const allOwners = getOwnerOptions().every(option => opportunitiesFilterState.selectedOwnerKeys.has(option.key)); return getCustomerOptions(allOwners ? items : items.filter(item => opportunitiesFilterState.selectedOwnerKeys.has(getOwnerKey(item)))); } function getAvailableOwnerOptions() { const items = opportunitiesTableState.opportunitiesTableState.items; const allCustomers = getCustomerOptions().every(option => opportunitiesFilterState.selectedCustomerKeys.has(option.key)); return getOwnerOptions(allCustomers ? items : items.filter(item => opportunitiesFilterState.selectedCustomerKeys.has(getCustomerKey(item)))); } 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 = getAvailableCustomerOptions(); if (!trigger || !searchInput || !selectAllCheckbox) { return; } const selectedCount = customerOptions.filter((customer) => opportunitiesFilterState.selectedCustomerKeys.has(customer.key)).length; trigger.disabled = getCustomerOptions().length === 0; trigger.textContent = getCustomerFilterSummary(customerOptions.length, selectedCount); searchInput.value = opportunitiesFilterState.customerSearch; selectAllCheckbox.disabled = getCustomerOptions().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 = getAvailableCustomerOptions().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 (getAvailableCustomerOptions().every(option => opportunitiesFilterState.selectedCustomerKeys.has(option.key))) { opportunitiesFilterState.selectedCustomerKeys.clear(); opportunitiesFilterState.selectedCustomerKeys.add(customer.key); } else if (checkbox.checked) { opportunitiesFilterState.selectedCustomerKeys.add(customer.key); } else { opportunitiesFilterState.selectedCustomerKeys.delete(customer.key); } opportunitiesFilterState.hasSavedCustomerFilter = true; saveFilterPreferences(); renderCustomerFilter(); renderOwnerFilter(); 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 = getAvailableOwnerOptions(); if (!trigger || !searchInput || !selectAllCheckbox) { return; } const selectedCount = ownerOptions.filter((owner) => opportunitiesFilterState.selectedOwnerKeys.has(owner.key)).length; trigger.disabled = getOwnerOptions().length === 0; trigger.textContent = getCustomerFilterSummary(ownerOptions.length, selectedCount) .replace("customer", "owner"); searchInput.value = opportunitiesFilterState.ownerSearch; selectAllCheckbox.disabled = getOwnerOptions().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 = getAvailableOwnerOptions().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 (getAvailableOwnerOptions().every(option => opportunitiesFilterState.selectedOwnerKeys.has(option.key))) { opportunitiesFilterState.selectedOwnerKeys.clear(); opportunitiesFilterState.selectedOwnerKeys.add(owner.key); } else if (checkbox.checked) { opportunitiesFilterState.selectedOwnerKeys.add(owner.key); } else { opportunitiesFilterState.selectedOwnerKeys.delete(owner.key); } opportunitiesFilterState.hasSavedOwnerFilter = true; saveFilterPreferences(); renderOwnerFilter(); renderCustomerFilter(); 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) && matchesProductType(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 { getAvailableCustomerOptions, getAvailableOwnerOptions, 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("en-US"); } try { return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(number); } catch (error) { return `${currency} ${number.toLocaleString("en-US")}`; } } 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, searchableSelect, environment }) { const { document, navigator, window } = environment; function chooseBookmarkFolder(loadFolders, saveBookmark) { const modal = document.getElementById(MODAL_ID); if (!modal || modal.querySelector(".oe-bookmark-picker")) return; const previous = document.activeElement; const overlay = document.createElement("div"); overlay.className = "opportunities-extension-action-form-modal oe-bookmark-picker"; const panel = document.createElement("section"); panel.className = "opportunities-extension-action-form-dialog"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "true"); panel.setAttribute("aria-labelledby", "oe-bookmark-title"); const header = document.createElement("header"); header.className = "opportunities-extension-account-plans-header"; const title = document.createElement("h2"); title.id = "oe-bookmark-title"; title.textContent = "Add to favorites"; const dismiss = document.createElement("button"); dismiss.type = "button"; dismiss.className = "opportunities-extension-icon-button"; dismiss.setAttribute("aria-label", "Close favorites form"); const closeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); closeIcon.setAttribute("viewBox", "0 0 24 24"); closeIcon.setAttribute("aria-hidden", "true"); const closePath = document.createElementNS("http://www.w3.org/2000/svg", "path"); closePath.setAttribute("d", "M6 6l12 12M18 6 6 18"); closePath.setAttribute("stroke", "currentColor"); closePath.setAttribute("stroke-width", "1.75"); closeIcon.append(closePath); dismiss.append(closeIcon); header.append(title, dismiss); const body = document.createElement("div"); body.className = "opportunities-extension-action-form"; const label = document.createElement("label"); label.className = "opportunities-extension-action-form-field opportunities-extension-action-form-wide"; label.textContent = "Folder (required)"; const select = document.createElement("select"); select.id = "oe-bookmark-folder"; select.required = true; select.setAttribute("aria-describedby", "oe-bookmark-status"); select.disabled = true; label.append(select); const status = document.createElement("p"); status.id = "oe-bookmark-status"; status.className = "opportunities-extension-action-form-status opportunities-extension-action-form-wide"; status.setAttribute("role", "status"); status.textContent = "Loading bookmarks folders…"; const actions = document.createElement("footer"); actions.className = "opportunities-extension-action-form-footer"; const cancel = document.createElement("button"); cancel.type = "button"; cancel.textContent = "Cancel"; const save = document.createElement("button"); save.type = "button"; save.className = "opportunities-extension-action-form-save"; save.textContent = "Save"; save.disabled = true; const autocomplete = searchableSelect(select, label, () => { save.disabled = !select.value; }, { floating: true }); autocomplete.input.setAttribute("aria-describedby", "oe-bookmark-status"); let saving = false; const siblings = Array.from(modal.children).map(node => [node, node.inert]); siblings.forEach(([node]) => { node.inert = true; }); function close() { if (saving) return; autocomplete.destroy(); overlay.remove(); siblings.forEach(([node, inert]) => { node.inert = inert; }); previous?.focus(); } cancel.addEventListener("click", close); dismiss.addEventListener("click", close); save.addEventListener("click", async () => { if (!select.value || saving) return; saving = true; save.disabled = cancel.disabled = select.disabled = dismiss.disabled = true; autocomplete.sync(); status.setAttribute("role", "status"); status.textContent = "Saving favorite…"; try { const result = await saveBookmark(select.value); if (!result?.ok) throw new Error(result?.error || "Unable to add favorite."); saving = false; close(); showCopyConfirmation(result.existing ? "This shortcut is already in the selected folder." : "Opportunities Extension added to favorites."); } catch (error) { status.setAttribute("role", "alert"); status.textContent = error.message; saving = false; save.disabled = cancel.disabled = select.disabled = dismiss.disabled = false; autocomplete.sync(); } }); overlay.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); } if (event.key === "Tab") { const controls = [dismiss, autocomplete.input, cancel, save].filter(control => !control.disabled); event.preventDefault(); const index = controls.indexOf(document.activeElement); controls[(index + (event.shiftKey ? -1 : 1) + controls.length) % controls.length]?.focus(); } }); actions.append(cancel, save); body.append(label, status, actions); panel.append(header, body); overlay.append(panel); modal.append(overlay); cancel.focus(); Promise.resolve().then(loadFolders).then(result => { if (!overlay.isConnected) return; if (!result?.ok) throw new Error(result?.error || "Unable to load bookmarks folders."); const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = "Select a folder"; select.append(placeholder); for (const folder of result.folders) { const option = document.createElement("option"); option.value = folder.id; option.textContent = folder.label; select.append(option); } select.disabled = !result.folders.length; status.textContent = result.folders.length ? "Choose where to save Opportunities Extension." : "No writable bookmarks folders available."; select.addEventListener("change", () => { save.disabled = !select.value; }); autocomplete.sync(); if (!select.disabled) autocomplete.input.focus(); }).catch(error => { if (overlay.isConnected) { status.setAttribute("role", "alert"); status.textContent = error.message; } }); } 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 { chooseBookmarkFolder, 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", "Close preferences"); 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 = "Cache duration"; const cacheSelect = document.createElement("select"); cacheSelect.name = "opptyProductsCacheHours"; cacheSelect.setAttribute("aria-label", "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, loadWorkloads, joinWorkloads, sendRuntimeMessage, environment }) { const { browser, chrome, document, window, setTimeout, clearTimeout, matchMedia } = environment; let completion = null; function updateCompletionVisibility(badge) { const requestId = productsContentState.currentProductsRequestId; const complete = productsContentState.productsProgress.state === "complete"; if (completion && (!complete || completion.requestId !== requestId || completion.badge !== badge)) { clearTimeout(completion.waitTimer); clearTimeout(completion.hideTimer); completion.badge.setAttribute("data-fading", "false"); completion = null; } if (!complete) { badge.setAttribute("data-fading", "false"); return; } if (completion) { badge.hidden = completion.hidden; return; } const current = { requestId, badge, hidden: false, waitTimer: null, hideTimer: null }; completion = current; badge.setAttribute("data-fading", "false"); current.waitTimer = setTimeout(() => { if (completion !== current) return; badge.setAttribute("data-fading", "true"); const reducedMotion = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches; current.hideTimer = setTimeout(() => { if (completion !== current) return; current.hidden = true; badge.hidden = true; }, reducedMotion ? 0 : 300); }, 5000); } function requestOpptyProductsInBackground(opportunities, token) { const optyNumbers = Array.from(new Set(opportunities .map((item) => item && item.OptyNumber) .filter(Boolean) .map(String))); const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`; productsContentState.currentProductsRequestId = requestId; beginProductActivities(requestId, token, optyNumbers); productsContentState.opptyProductsByNumber = new Map(); productsContentState.expandedProductOptyNumbers = new Set(); productsContentState.productsProgress = { state: optyNumbers.length ? "running" : "complete", completed: 0, total: optyNumbers.length, failed: 0, cacheHits: 0 }; renderProductsProgress(); loadWorkloads(opportunities, requestId); 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 || "" }); joinWorkloads(message.optyNumber); } 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("en-GB"); } } 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/workloads.js function createProductWorkloads({ productsContentState, opportunitiesQueryState, preferencesState, messageTypes, requestConsumerTokenServiceOnce, sendRuntimeMessage, appendBackgroundRequestLog, getPrincipal, queueProductsTableRender }) { let cycle = null; function reset() { cycle = null; productsContentState.workloadCustomerIds = []; productsContentState.workloadsByCustomer = new Map(); } function isCurrent(current) { return cycle === current && current.requestId === productsContentState.currentProductsRequestId && current.version === opportunitiesQueryState.periodRequestVersion; } function joinProducts(optyNumber) { if (!cycle || !isCurrent(cycle)) return; const key = String(optyNumber); const products = productsContentState.opptyProductsByNumber.get(key); if (!products) return; const customerId = cycle.customerByOpportunity.get(key); const record = productsContentState.workloadsByCustomer.get(customerId); products.items = products.items.map(product => ({ ...product, workload: record?.status === "ready" ? record.byId.get(String(product.RevnId ?? "")) || null : null })); } async function load(opportunities, requestId) { reset(); const current = { requestId, version: opportunitiesQueryState.periodRequestVersion, customerByOpportunity: new Map(), opportunitiesByCustomer: new Map() }; cycle = current; for (const opportunity of opportunities) { const customerId = String(opportunity?.CustomerAccount?.PartyId ?? "").trim(); if (!/^\d+$/.test(customerId)) continue; if (!current.opportunitiesByCustomer.has(customerId)) current.opportunitiesByCustomer.set(customerId, new Set()); if (opportunity.OptyNumber) { const number = String(opportunity.OptyNumber); current.customerByOpportunity.set(number, customerId); current.opportunitiesByCustomer.get(customerId).add(number); } } const ids = [...current.opportunitiesByCustomer.keys()]; productsContentState.workloadCustomerIds = ids; const records = productsContentState.workloadsByCustomer; for (const id of ids) records.set(id, { status: "loading", workloads: [], byId: new Map() }); if (!ids.length || !isCurrent(current)) return; const principal = getPrincipal(); const cacheMaxAgeMs = preferencesState.opptyProductsCacheHours * 60 * 60 * 1000; try { const authorizations = await requestConsumerTokenServiceOnce(); if (!isCurrent(current)) return; let next = 0; const worker = async () => { while (isCurrent(current) && next < ids.length) { const customerId = ids[next++]; const record = records.get(customerId); try { const result = await sendRuntimeMessage({ type: messageTypes.requestWorkloads, customerId, authorization: authorizations?.workloadActions, cacheMaxAgeMs, principal }); if (!isCurrent(current)) return; appendBackgroundRequestLog(result?.debugRequests); if (!result?.ok || !Array.isArray(result.workloads)) throw new Error(result?.error || "Unable to load workloads."); Object.assign(record, { status: "ready", workloads: result.workloads, byId: new Map(result.workloads.map(workload => [String(workload.id), workload])), fromCache: Boolean(result.fromCache), cachedAt: Number(result.cachedAt) || 0 }); } catch (error) { if (!isCurrent(current)) return; record.status = "error"; record.error = error.message || "Unable to load workloads."; } for (const number of current.opportunitiesByCustomer.get(customerId)) joinProducts(number); queueProductsTableRender(); } }; await Promise.all(Array.from({ length: Math.min(4, ids.length) }, worker)); } catch (error) { if (!isCurrent(current)) return; for (const record of records.values()) { record.status = "error"; record.error = error.message || "Unable to load workloads."; } } } return { load, joinProducts, reset }; } // Source: src/features/products/details.js function createProductsDetails({ OPPORTUNITIES_COLUMNS, setBadgeContent, createProjectionIndicator, openProjection, formatOracleResponseDate, productsContentState, renderOpportunitiesTable, environment }) { const { document } = environment; function isBooking(products) { return Boolean(products?.length) && products.every(item => !String(item?.WorkloadName_c ?? "").trim() || String(item.WorkloadName_c).trim() === "-"); } function createProductsRampBadge(productsResult, opportunity) { const products = Array.isArray(productsResult.items) ? productsResult.items : []; if (isBooking(products)) { const badge = document.createElement("span"); badge.className = "opportunities-extension-products-ramp"; badge.setAttribute("data-booking", "true"); badge.textContent = "Booking"; badge.title = "Booking: no product has a Workload assigned."; badge.setAttribute("aria-label", badge.title); return badge; } const values = products .map(item => item && item.RampMonths_c) .map(value => value === null || value === undefined || String(value).trim() === "" ? "N/A" : String(value).trim()) .filter((value, index, items) => value !== "N/A" || items.indexOf(value) === index); if (!values.length) return null; const badge = document.createElement("button"); badge.type = "button"; badge.className = "opportunities-extension-products-ramp"; badge.setAttribute("data-missing", String(values.includes("N/A"))); setBadgeContent(badge, "ramp", values.join(" | ")); badge.title = `Ramp Months: ${values.join(", ")}`; badge.setAttribute("aria-label", badge.title); badge.setAttribute("aria-haspopup", "dialog"); badge.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); openProjection(productsResult.items, opportunity, badge); }); return badge; } function expandProducts(optyNumber) { const number = String(optyNumber); if (productsContentState.expandedProductOptyNumbers.has(number)) return; productsContentState.expandedProductOptyNumbers.add(number); renderOpportunitiesTable(); } 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 { expandProducts(normalizedOptyNumber); return; } 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); 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("data-table-layout", "standard"); 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/A" : formatProductNumber(item.RampMonths_c) }, { label: "Projection", render: (item) => createProjectionIndicator(item, opportunity) } ]; const head = document.createElement("thead"); const headerRow = document.createElement("tr"); columns.forEach((column) => { const header = document.createElement("th"); header.scope = "col"; if (column.label === "Amount") header.setAttribute("data-align", "money"); 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"); if (column.label === "Amount") productCell.setAttribute("data-align", "money"); if (column.render) { productCell.className = "opportunities-extension-projection-cell"; productCell.append(column.render(product)); } else 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 { isBooking, expandProducts, createProductsRampBadge, createProductsCountButton, createProductsDetailRow, formatProductNumber, formatProductAmount, formatProductWinProbability }; } // Source: src/features/products/ramp-projection.js function createRampProjection() { function number(value) { if ((typeof value !== "number" && typeof value !== "string") || String(value).trim() === "") return null; const result = Number(value); return Number.isFinite(result) ? result : null; } function startDate(value) { const match = /^(\d{4})-(\d{2})-(\d{2})(?:T|$)/.exec(String(value ?? "")); if (!match) return null; const [, year, month, day] = match.map(Number); const date = new Date(Date.UTC(year, month - 1, day)); if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return null; return { year, month, day, days: new Date(Date.UTC(year, month, 0)).getUTCDate() }; } function monthlyAmount(annual, ramp, month) { if (month < 1) return 0; // calculadora rampa.xlsx, rampas!D6:P17: exact values, before currency rounding. return (2 * annual / (24 - ramp)) * Math.min(month / (ramp + 1), 1); } function cents(value) { return Math.round((value + Number.EPSILON * Math.abs(value)) * 100); } function compare(product) { const workload = product?.workload; const unavailable = reason => ({ status: "unavailable", reason, rows: [], divergentCount: 0 }); if (!workload) return unavailable("Workload data is not available for this product."); const annual = number(workload.adjustedACR); const ramp = number(product.RampMonths_c); const start = startDate(workload.consumptionStartDate); if (annual === null || annual < 0) return unavailable("Annual value (adjustedACR) is missing or invalid."); if (ramp === null || !Number.isInteger(ramp) || ramp < 0 || ramp > 11) { return unavailable("RampMonths_c must be an integer from 0 to 11, as covered by the reference workbook."); } if (!start) return unavailable("Consumption start date is missing or invalid."); if (!Array.isArray(workload.forecast) || !workload.forecast.length) return unavailable("Monthly forecast data is not available."); const periods = new Set(); const rows = workload.forecast.map(forecast => { const year = number(forecast?.year), month = number(forecast?.month); const actual = number(forecast?.adjustedConsumptionAmount); if (!Number.isInteger(year) || year < 1000 || year > 9999 || !Number.isInteger(month) || month < 1 || month > 12) return null; const key = `${year}-${String(month).padStart(2, "0")}`; if (periods.has(key)) return null; periods.add(key); const index = (year - start.year) * 12 + month - start.month + 1; const prorated = index === 1 && start.day > 1; const exact = monthlyAmount(annual, ramp, index) * (index === 1 ? (start.days - start.day + 1) / start.days : 1); const calculatedCents = cents(exact); const actualCents = actual === null ? null : cents(actual); if (!Number.isSafeInteger(calculatedCents) || (actualCents !== null && !Number.isSafeInteger(actualCents))) return null; const differenceCents = actualCents === null ? null : actualCents - calculatedCents; return { key, year, month, index, prorated, actual: actualCents === null ? null : actualCents / 100, calculated: calculatedCents / 100, difference: differenceCents === null ? null : differenceCents / 100, divergent: differenceCents !== null && differenceCents !== 0 }; }); if (rows.some(row => row === null)) return unavailable("Forecast contains invalid or duplicate months, or values outside the supported numeric range."); rows.sort((a, b) => a.key.localeCompare(b.key)); const divergentCount = rows.filter(row => row.divergent).length; const missingCount = rows.filter(row => row.actual === null).length; return { status: divergentCount ? "divergent" : missingCount ? "unavailable" : "matched", reason: missingCount ? "Some months have no adjusted consumption amount." : "", rows, divergentCount, missingCount, annual, ramp, start: `${start.year}-${String(start.month).padStart(2, "0")}-${String(start.day).padStart(2, "0")}`, firstMonthDays: start.days - start.day + 1, firstMonthTotalDays: start.days }; } return { compare, monthlyAmount }; } // Source: src/features/products/forecast-update.js function createWorkloadForecastUpdate({ compare, ensureCurrentUser, getCurrentUserId, requestAuthorization, sendRuntimeMessage, messageTypes, appendBackgroundRequestLog, productsContentState, queueProductsTableRender }) { const pending = new Set(); async function save(product) { const id = String(product?.workload?.id ?? ""); if (!/^\d+$/.test(id) || id !== String(product.RevnId)) throw new Error("The product does not have a matching workload ID."); if (pending.has(id)) throw new Error("Estimates are already being updated."); const comparison = compare(product); if (comparison.status !== "divergent") throw new Error("No forecast differences are available to update."); pending.add(id); try { await ensureCurrentUser(); const updatedBy = getCurrentUserId(); if (typeof updatedBy !== "string" || !updatedBy.trim()) throw new Error("The current user ID is unavailable."); // Keep zero months before consumption unchanged, unless they also need correcting. const payload = comparison.rows.filter(row => row.index >= 1 || row.divergent) .map(row => ({ year: row.year, month: row.month, amount: row.calculated, updatedBy })); const authorizations = await requestAuthorization(); const response = await sendRuntimeMessage({ type: messageTypes.requestWorkloadForecastUpdate, workloadId: id, payload, authorization: authorizations?.workloadActions }); appendBackgroundRequestLog(response?.debugRequests); if (!response?.ok) throw new Error(response?.error || "Unable to update workload estimates."); const amounts = new Map(payload.map(item => [`${item.year}-${item.month}`, item.amount])); const update = workload => { if (String(workload?.id) !== id) return workload; return { ...workload, forecast: workload.forecast.map(month => { const key = `${Number(month.year)}-${Number(month.month)}`; return amounts.has(key) ? { ...month, adjustedConsumptionAmount: amounts.get(key) } : month; }) }; }; product.workload = update(product.workload); for (const record of productsContentState.workloadsByCustomer.values()) { if (record.status !== "ready") continue; record.workloads = record.workloads.map(update); record.byId = new Map(record.workloads.map(workload => [String(workload.id), workload])); } for (const record of productsContentState.opptyProductsByNumber.values()) { record.items = record.items.map(item => ({ ...item, workload: update(item.workload) })); } queueProductsTableRender(); return response; } finally { pending.delete(id); } } return { save }; } // Source: src/features/products/ramp-projection-view.js function createRampProjectionView({ MODAL_ID, compare, createAlertIcon, createCloseIcon, expandProducts, saveEstimates, notify, formatAmount, formatDate, environment, tableSorting }) { const { document } = environment; let active = null; function node(tag, className, text) { const item = document.createElement(tag); if (className) item.className = className; if (text !== undefined) item.textContent = text; return item; } function close(restoreFocus = true) { if (!active) return false; if (active.saving && restoreFocus) return true; const previous = active; active = null; previous.modal.remove(); previous.siblings.forEach(([element, inert]) => { element.inert = inert; }); if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus(); return true; } function createIndicator(product, opportunity) { const result = compare(product); if (!product?.workload) { const disabled = node("button", "opportunities-extension-icon-button opportunities-extension-projection-alert"); disabled.type = "button"; disabled.disabled = true; disabled.title = "No Workload is available for this product."; disabled.setAttribute("aria-label", disabled.title); disabled.append(createAlertIcon()); return disabled; } if (result.status !== "divergent") { const text = node("span", "opportunities-extension-projection-status", result.status === "matched" ? "OK" : "-"); text.title = result.status === "matched" ? "Monthly forecast matches the ramp calculation." : result.reason; return text; } const button = node("button", "opportunities-extension-icon-button opportunities-extension-projection-alert"); button.type = "button"; button.title = `Consumption forecast differs in ${result.divergentCount} month${result.divergentCount === 1 ? "" : "s"}. View comparison.`; button.setAttribute("aria-label", button.title); button.setAttribute("aria-haspopup", "dialog"); button.append(createAlertIcon()); button.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); open(product, opportunity, button); }); return button; } function createOpportunityIndicator(productsResult, opportunity, isExpanded) { const products = productsResult?.items || []; const count = products.filter(product => compare(product).status === "divergent").length; if (!count) return null; const singleProduct = products.length === 1; const indicator = node("button", "opportunities-extension-icon-button opportunities-extension-projection-alert opportunities-extension-opportunity-projection-alert"); indicator.type = "button"; indicator.title = `Consumption forecast differs in ${count} product${count === 1 ? "" : "s"}. ${singleProduct ? "View comparison." : "View products."}`; if (singleProduct) indicator.setAttribute("aria-haspopup", "dialog"); else indicator.setAttribute("aria-expanded", String(Boolean(isExpanded))); indicator.setAttribute("aria-label", indicator.title); indicator.append(createAlertIcon()); indicator.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); if (singleProduct) open(products[0], opportunity, indicator); else expandProducts(opportunity.OptyNumber); }); return indicator; } function open(product, opportunity, trigger, products = []) { const host = document.getElementById(MODAL_ID); const result = compare(product); if (!host || host.hidden || !product) return; close(false); const modal = node("section", "opportunities-extension-action-form-modal opportunities-extension-projection-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "opportunities-extension-projection-title"); const dialog = node("div", "opportunities-extension-action-form-dialog opportunities-extension-projection-dialog"); const header = node("header", "opportunities-extension-account-plans-header"); const heading = node("div", "opportunities-extension-action-form-heading"); const title = node("h2", "", "Consumption projection"); title.id = "opportunities-extension-projection-title"; heading.append(title, node("p", "", `${opportunity.OptyNumber} - ${product.WorkloadName_c || product.workload.name || product.ProdGroupName || "Workload"}`)); const dismiss = node("button", "opportunities-extension-icon-button"); dismiss.type = "button"; dismiss.title = "Close consumption projection"; dismiss.setAttribute("aria-label", dismiss.title); dismiss.append(createCloseIcon()); dismiss.addEventListener("click", () => close()); header.append(heading, dismiss); const body = node("div", "opportunities-extension-projection-body"); let productSelect = null; if (products.length > 1) { const label = node("label", "", "Product "); productSelect = node("select", "opportunities-extension-projection-product"); productSelect.setAttribute("aria-label", "Product"); products.forEach((item, index) => { const option = node("option", "", `${item.ProdGroupName || "Product"} | ${item.WorkloadName_c || item.RevnId || index + 1}`); option.value = String(index); option.selected = item === product; productSelect.append(option); }); productSelect.addEventListener("change", () => open(products[Number(productSelect.value)], opportunity, trigger, products)); label.append(productSelect); body.append(label); } const details = node("dl", "opportunities-extension-projection-context"); const currency = product.RevnAmountCurcyCode || "USD"; for (const [label, value] of [ ["Customer", opportunity.CustomerAccount?.PartyUniqueName || "-"], ["Product", product.ProdGroupName || "-"], ["Annual value", result.annual == null ? "-" : formatAmount(result.annual, currency)], ["Ramp months", String(result.ramp ?? "-")], ["Consumption start", result.start ? formatDate(result.start) : "-"], ["First month", result.firstMonthDays == null ? "-" : `${result.firstMonthDays}/${result.firstMonthTotalDays} days`] ]) details.append(node("dt", "", label), node("dd", "", value)); body.append(details); const summary = node("p", "opportunities-extension-projection-summary", `${result.divergentCount} month${result.divergentCount === 1 ? "" : "s"} with a difference`); body.append(summary); if (result.reason) body.append(node("p", "opportunities-extension-projection-notice", result.reason)); const viewport = node("div", "opportunities-extension-projection-viewport"); viewport.tabIndex = 0; viewport.setAttribute("role", "region"); viewport.setAttribute("aria-label", "Monthly consumption comparison"); const table = node("table", "opportunities-extension-projection-table"); table.setAttribute("data-table-layout", "standard"); const caption = node("caption", "opportunities-extension-visually-hidden", `Monthly consumption comparison (${currency}). Difference is forecast minus calculated.`); const head = node("thead"), headRow = node("tr"), tbody = node("tbody"); const sorting = tableSorting.create("consumption-projection", [ { key: "month", type: "number", value: item => item.year * 12 + item.month }, ...["actual", "calculated", "difference"].map(key => ({ key, type: "number" })) ]); const rowNodes = new Map(); const renderRows = () => tbody.replaceChildren(...sorting.select(result.rows).map(item => rowNodes.get(item))); for (const [key, label] of [["month", "Month"], ["actual", "Forecast"], ["calculated", "Calculated"], ["difference", "Difference"]]) { const th = node("th"); th.scope = "col"; if (key !== "month") th.setAttribute("data-align", "money"); sorting.header(th, key, label, renderRows); headRow.append(th); } head.append(headRow); for (const item of result.rows) { const tr = node("tr"); tr.setAttribute("data-divergent", String(item.divergent)); const month = node("th", "", `${String(item.month).padStart(2, "0")}/${item.year}`); month.scope = "row"; if (item.prorated) month.append(node("span", "opportunities-extension-projection-prorated", " (Prorated)")); tr.append(month); for (const value of [item.actual, item.calculated, item.difference]) { const cell = node("td", "", value === null ? "-" : formatAmount(value, currency)); cell.setAttribute("data-align", "money"); tr.append(cell); } rowNodes.set(item, tr); } renderRows(); table.append(caption, head, tbody); viewport.append(table); body.append(viewport); const errorMessage = node("p", "opportunities-extension-projection-error"); errorMessage.setAttribute("role", "alert"); errorMessage.hidden = true; body.append(errorMessage); const footer = node("footer", "opportunities-extension-projection-footer"); const updateButton = node("button", "opportunities-extension-projection-update", "Update estimates"); updateButton.disabled = result.status !== "divergent"; updateButton.type = "button"; footer.append(node("span", "", "Difference = forecast - calculated."), updateButton); dialog.append(header, body, footer); modal.append(dialog); const siblings = Array.from(host.children).map(element => [element, element.inert]); siblings.forEach(([element]) => { element.inert = true; }); const current = { modal, siblings, trigger, saving: false }; active = current; async function updateEstimates() { if (current.saving) return; current.saving = true; updateButton.disabled = true; dismiss.disabled = true; updateButton.textContent = "Updating..."; modal.setAttribute("aria-busy", "true"); errorMessage.hidden = true; try { await saveEstimates(product); current.saving = false; if (active === current) close(); notify("Workload estimates updated successfully."); } catch (error) { const message = error.message || "Unable to update workload estimates."; if (active === current) { errorMessage.textContent = message; errorMessage.hidden = false; } notify(message, true); } finally { current.saving = false; updateButton.disabled = false; dismiss.disabled = false; updateButton.textContent = "Update estimates"; modal.setAttribute("aria-busy", "false"); } } updateButton.addEventListener("click", () => { if (current.saving || current.confirmation) return; const overlay = node("div", "opportunities-extension-action-form-modal"); const confirmation = node("section", "opportunities-extension-action-form-dialog opportunities-extension-projection-confirm"); confirmation.setAttribute("role", "alertdialog"); confirmation.setAttribute("aria-modal", "true"); confirmation.setAttribute("aria-labelledby", "projection-confirm-title"); confirmation.setAttribute("aria-describedby", "projection-confirm-message"); const title = node("h2", "", "Update estimates"); title.id = "projection-confirm-title"; const message = node("p", "", "The estimates will be updated directly in the SPA using the values in the Calculated column. Would you like to confirm the update?"); message.id = "projection-confirm-message"; const actions = node("div", "opportunities-extension-projection-confirm-actions"); const cancel = node("button", "opportunities-extension-projection-cancel", "Cancel"); const confirm = node("button", "opportunities-extension-projection-update", "Confirm update"); cancel.type = confirm.type = "button"; function dismissConfirmation() { overlay.remove(); current.confirmation = null; dialog.inert = false; updateButton.focus(); } cancel.addEventListener("click", dismissConfirmation); confirm.addEventListener("click", () => { if (current.confirmation !== overlay || active !== current) return; dismissConfirmation(); return updateEstimates(); }); overlay.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); dismissConfirmation(); } else if (event.key === "Tab") { event.preventDefault(); event.stopPropagation(); (document.activeElement === cancel ? confirm : cancel).focus(); } }); actions.append(cancel, confirm); confirmation.append(title, message, actions); overlay.append(confirmation); current.confirmation = overlay; dialog.inert = true; modal.append(overlay); cancel.focus(); }); host.append(modal); modal.addEventListener("keydown", event => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); } if (event.key === "Tab") { const controls = [dismiss, productSelect, viewport, ...table.querySelectorAll("button"), updateButton].filter(control => control && !control.disabled); const index = controls.indexOf(document.activeElement); if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus(); } } }); dismiss.focus(); } return { createIndicator, createOpportunityIndicator, open, close }; } // 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, tableSorting, environment }) { const { document } = environment; let active = null; const columns = [ ["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(); } const sortColumns = columns.map(([key, , type]) => ({ key, type: ["amount", "quantity"].includes(type) ? "number" : type })); function createTable(items, loading = false, sorting = tableSorting.create("installed-base", sortColumns), onSort) { const table = element("table", "opportunities-extension-install-base-table"); table.setAttribute("data-table-layout", "standard"); 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"); th.scope = "col"; sorting.header(th, key, label, onSort, loading); header.append(th); }); head.append(header); const body = element("tbody"); (loading ? Array.from({ length: 8 }, () => ({})) : sorting.select(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 sorting = tableSorting.create("installed-base", sortColumns); 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, ...content.querySelectorAll("button")].filter(control => !control.disabled); 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, false, sorting, key => { renderResults(); content.querySelector(`[data-sort-key="${key}"]`)?.focus(); })); 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, tableSorting, 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; } const sortColumns = columns.map(([key, , value]) => ({ key, value, type: ["created", "lastUpdated"].includes(key) ? "date" : "text" })); function createTable(items, loading = false, sorting = tableSorting.create("service-requests", sortColumns), onSort) { const table = element("table", "opportunities-extension-service-requests-table"); table.setAttribute("data-table-layout", "standard"); 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"); th.scope = "col"; sorting.header(th, key, label, onSort, loading); header.append(th); }); head.append(header); const body = element("tbody"); (loading ? Array.from({ length: 8 }, () => ({})) : sorting.select(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 sorting = tableSorting.create("service-requests", sortColumns); 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, ...content.querySelectorAll("button")].filter(control => !control.disabled); 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, false, sorting, key => { renderResults(); content.querySelector(`[data-sort-key="${key}"]`)?.focus(); })); 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, tableSorting, 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"); const orderFilter = element("div", "order-filter"); const filterElement = (tag, suffix, text) => { const node = element(tag, null, text); node.className = `opportunities-extension-customer-filter-${suffix}`; return node; }; const orderButton = filterElement("button", "trigger", "All order numbers"); orderButton.type = "button"; orderButton.disabled = true; orderButton.setAttribute("aria-label", "Filter by Order Number"); orderButton.setAttribute("aria-expanded", "false"); const orderPanel = filterElement("section", "panel"); orderPanel.id = "opportunities-extension-ratecard-orders"; orderButton.setAttribute("aria-controls", orderPanel.id); orderPanel.hidden = true; const orderSearch = filterElement("input", "search"); orderSearch.type = "search"; orderSearch.placeholder = "Search order numbers"; orderSearch.setAttribute("aria-label", "Search order numbers"); const allLabel = filterElement("label", "select-all"); const allOrders = element("input"); allOrders.type = "checkbox"; allLabel.append(allOrders, element("span", null, "Select all")); const orderList = filterElement("div", "list"); orderPanel.append(orderSearch, allLabel, orderList); orderFilter.append(orderButton, orderPanel); toolbar.append(search, orderFilter, count); let orders = [], selectedOrders = new Set(); const orderKey = item => String(item.orderNumber ?? "").trim(); function closeOrders() { orderPanel.hidden = true; orderButton.setAttribute("aria-expanded", "false"); } function updateOrders() { const all = orders.length > 0 && selectedOrders.size === orders.length; allOrders.checked = all; allOrders.indeterminate = selectedOrders.size > 0 && !all; orderButton.textContent = all ? "All order numbers" : `${selectedOrders.size} order numbers selected`; orderList.textContent = ""; const query = orderSearch.value.trim().toLowerCase(); orders.filter(value => (value || "Not provided").toLowerCase().includes(query)).forEach(value => { const label = filterElement("label", "option"); const checkbox = element("input"); checkbox.type = "checkbox"; checkbox.checked = selectedOrders.has(value); checkbox.addEventListener("change", () => { if (orders.every(order => selectedOrders.has(order))) { selectedOrders.clear(); selectedOrders.add(value); } else if (checkbox.checked) selectedOrders.add(value); else selectedOrders.delete(value); allOrders.checked = selectedOrders.size === orders.length; allOrders.indeterminate = selectedOrders.size > 0 && !allOrders.checked; orderButton.textContent = allOrders.checked ? "All order numbers" : `${selectedOrders.size} order numbers selected`; updateOrders(); renderRows(); }); label.append(checkbox, element("span", null, value || "Not provided")); orderList.append(label); }); } orderButton.addEventListener("click", () => { orderPanel.hidden = !orderPanel.hidden; orderButton.setAttribute("aria-expanded", String(!orderPanel.hidden)); if (!orderPanel.hidden) orderSearch.focus(); }); orderSearch.addEventListener("input", updateOrders); allOrders.addEventListener("change", () => { selectedOrders = new Set(allOrders.checked ? orders : []); updateOrders(); renderRows(); }); root.addEventListener("click", event => { if (!orderFilter.contains(event.target)) closeOrders(); }); const viewport = element("div", "viewport"); viewport.tabIndex = 0; viewport.setAttribute("role", "region"); viewport.setAttribute("aria-label", "Rate card items"); const table = element("table", "table"); table.setAttribute("data-table-layout", "standard"); 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 }; const savedSort = tableSorting.read("rate-card", columns); current.sortKey = savedSort.key; current.direction = savedSort.direction === "descending" ? "desc" : "asc"; 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"; if (column.type === "price") th.setAttribute("data-align", "money"); 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; tableSorting.save("rate-card", { key: current.sortKey, direction: current.direction === "desc" ? "descending" : "ascending" }); updateSortHeaders(); if (current.status === "ready") renderRows(); }); sortHeaders.push({ th, icon, key: column.key }); th.append(sort); headRow.append(th); }); head.append(headRow); updateSortHeaders(); 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.filter(item => selectedOrders.has(orderKey(item))), 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 (column.type === "price") cell.setAttribute("data-align", "money"); 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(); if (!orderPanel.hidden) { closeOrders(); orderButton.focus(); return; } closeRatecardDrawer(); } else if (event.key === "Tab") { const controls = [close, search, ...(!orderButton.disabled ? [orderButton] : []), ...(!orderPanel.hidden ? [orderSearch, allOrders, ...orderList.querySelectorAll("input")] : []), 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; orders = [...new Set(items.map(orderKey))].sort((a, b) => a.localeCompare(b, "en", { numeric: true })); selectedOrders = new Set(orders); orderButton.disabled = !orders.length; updateOrders(); 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, 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); debugState.persist(entry); renderDebugPanel(); return entry; } function logRequestSuccess(entry, response, responseBody) { if (!debugState.requestLog.includes(entry)) return; 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; debugState.persist(entry); renderDebugPanel(); } function logRequestFailure(entry, errorMessage) { if (!debugState.requestLog.includes(entry)) return; entry.status = "failed"; entry.completedAt = new Date(); entry.durationMs = entry.completedAt.getTime() - entry.startedAt.getTime(); entry.error = errorMessage; debugState.persist(entry); 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); debugState.persist(existing || 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); } 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, requestName = "") { 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; } } if (!debugState.debugSecretsVisible && requestName === "requestSrTokenCatalog") { value = value && typeof value === "object" && !Array.isArray(value) ? { ...value, ...(Object.hasOwn(value, "result") ? { result: formatHiddenDebugValue(value.result) } : {}) } : formatHiddenDebugValue(value); } return JSON.stringify(debugState.debugSecretsVisible ? value : maskSensitiveDebugValue(value), null, 2); } catch (error) { if (!debugState.debugSecretsVisible && /^(requestSrTokenCatalog|requestCurrentUser|requestTokenRelay.*)$/.test(requestName)) return formatHiddenDebugValue(text); 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$|x-appbuilder-client-id/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"], ["contacts", "Contacts"], ["products", "Products"], ["consumption", "Consumption"], ["installBase", "Installed Base"], ["serviceRequests", "Service Requests"], ["actions", "Actions"], ["other", "Other"] ]; let selectedCategory = "all"; const expandedIds = new Set(); let returnFocus = null; let generation = -1; function getRequestCategory(entry) { if (/^request(?:CurrentUser|TokenRelay|ConsumerTokenService|CustomerTokenService)/.test(entry.name)) return "authentication"; if (["requestOpportunities", "requestOpportunitiesSavedSearch", "requestOpportunitiesByContact", "resolveContactForOpportunities"].includes(entry.name)) return "opportunities"; if (["requestQueryAccountContact", "requestPersistContact"].includes(entry.name)) return "contacts"; if (entry.name === "requestOpptyProducts") return "products"; if (["requestWlActions", "requestActionTeamMembers", "requestActionFindLookup", "requestMgmtAction"].includes(entry.name)) return "actions"; if (entry.name === "requestInstallBase") return "installBase"; if (["requestSRSearchResources", "requestSRInfo", "requestLookupTypePlatform", "requestInternalSrServices", "requestPostInternalServiceRequest"].includes(entry.name)) return "serviceRequests"; if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestResourceUser", "requestSrCatalog", "requestSrTokenCatalog", "requestPostServiceRequest"].includes(entry.name)) return "serviceRequests"; if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption"; return "other"; } function getChronologicalRequests() { return [...debugState.requestLog].sort((a, b) => (new Date(a.startedAt).getTime() || 0) - (new Date(b.startedAt).getTime() || 0) || a.id - b.id); } function toggleDebugPanel() { const modal = document.getElementById(MODAL_ID); if (!modal || modal.hidden) { return; } const panel = document.getElementById(DEBUG_PANEL_ID); if (!panel) { return; } if (!panel.hidden) { closeDebugPanel(); return; } debugState.reload(); returnFocus = document.activeElement; // Escape nested stacking contexts and inert containers created by child modals. modal.append(panel); panel.inert = false; 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; panel.textContent = ""; panel.onkeydown = null; if (returnFocus?.isConnected) returnFocus.focus({ preventScroll: true }); returnFocus = null; } function renderDebugPanel() { const panel = document.getElementById(DEBUG_PANEL_ID); if (!panel || panel.hidden) { return; } if (generation !== debugState.generation) { expandedIds.clear(); selectedCategory = "all"; generation = debugState.generation; } 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"} in the current query history.`; if (debugState.storageError) summary.textContent += ` ${debugState.storageError}`; 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("en-GB")}`; 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/A"; duration.title = hasDuration ? `Request duration: ${entry.durationMs} ms` : "Request duration not yet available"; summaryMeta.append(startedTime, method, httpStatus, status, duration); heading.append(name, summaryMeta); item.append(heading); const renderDetails = () => { if (!item.open || item.children.length > 1) return; 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, entry.name)); item.append(details); }; item.addEventListener("toggle", () => { if (item.open) { expandedIds.add(String(entry.id)); renderDetails(); } else { expandedIds.delete(String(entry.id)); item.querySelector(".opportunities-extension-debug-details")?.remove(); } }); renderDetails(); 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("en-US", { 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(); environment.window?.addEventListener?.("hashchange", () => page.insertTile()); 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, crypto: globalThis.crypto, get localStorage() { return globalThis.localStorage; }, fetch: globalThis.fetch && ((...args) => globalThis.fetch.call(undefined, ...args)), browser: globalThis.browser, chrome: globalThis.chrome, MutationObserver: globalThis.MutationObserver, Node: globalThis.Node, requestAnimationFrame: globalThis.requestAnimationFrame, performance: globalThis.performance, cancelAnimationFrame: globalThis.cancelAnimationFrame, setTimeout: globalThis.setTimeout && ((...args) => globalThis.setTimeout.call(undefined, ...args)), clearTimeout: globalThis.clearTimeout && ((...args) => globalThis.clearTimeout.call(undefined, ...args)), AbortController: globalThis.AbortController, matchMedia: globalThis.matchMedia, ...overrides }; const messageTypes = createMessageTypes(); const currentUser = createCurrentUser({ environment, messageTypes, sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args) }); const contracts = createContentContracts(); const searchableSelect = createSearchableSelect({ environment }); const authSessionState = createAuthSessionState(contracts); const opportunitiesQueryState = createOpportunitiesQueryState(); const tableSorting = createTableSorting({ environment }); const orgChartIcons = createOrgChartIcons({ environment }); const accountContact = createFusionAccountContact({ environment, getToken: () => authContentSession.requestTokenRelayOnce(), logRequestStart: (...args) => debugContentLog.logRequestStart(...args), logRequestSuccess: (...args) => debugContentLog.logRequestSuccess(...args), logRequestFailure: (...args) => debugContentLog.logRequestFailure(...args), readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args) }); const orgChart = createOrgChartView({ environment, tableSorting, model: createOrgChartModel(), requestQueryAccountContact: (...args) => accountContact.requestQueryAccountContact(...args), requestPersistContact: (...args) => accountContact.requestPersistContact(...args), requestOpportunitiesByContact: (...args) => accountContact.requestOpportunitiesByContact(...args), loadLayout: loadOrganizationLayout, icon: orgChartIcons.icon, searchableSelect, modalId: contracts.MODAL_ID, getPrincipal: () => currentUser.getUserId(), notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args), async request(operation, data) { const result = await platformContentMessages.sendRuntimeMessage({ ...data, type: operation === "load" ? messageTypes.loadOrganizationChart : messageTypes.saveOrganizationChart }); if (!result?.ok) throw new Error(result?.error || "Unable to access local organization charts."); return result.chart; } }); const opportunitiesTableState = createOpportunitiesTableState(); const opportunitiesFilterState = createOpportunitiesFilterState(contracts); const preferencesState = createPreferencesState(contracts); const activityCache = createActivityCache({ authSessionState, preferencesState, environment }); const productsContentState = createProductsContentState(); const consumptionPlansState = createConsumptionPlansState(); const consumptionTrendState = createConsumptionTrendState(); const opportunitiesDashboardState = createOpportunitiesDashboardState(contracts); const debugState = createDebugState(environment); 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({ getShortcutBookmarkStatus: () => platformContentMessages.sendRuntimeMessage({ type: messageTypes.getShortcutBookmarkStatus }), openBookmarkPicker: () => opportunitiesLinks.chooseBookmarkFolder( () => platformContentMessages.sendRuntimeMessage({ type: messageTypes.getBookmarkFolders }), async parentId => { const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.addShortcutBookmark, parentId }); if (result?.ok) contentModal.hideBookmarkHint(); return result; } ), createManageTimeButton: () => manageTime.createButton(), prepareSrSession: () => srSessionPreparation.ensure(), closeActionForm: (...args) => rampProjectionView.close(...args) || manageTime.close(...args) || internalSrForm.close(...args) || opportunityCalendar.closeDetails(...args) || crmSrForm.close(...args) || workloadActionForm.close(...args), mountOpportunityViews: (...args) => opportunityCalendar.mount(...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), closeOrganizationChart: () => orgChart.close(), 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 manageTime = createManageTime({ MODAL_ID: contracts.MODAL_ID, environment, tableSorting, searchableSelect, dates: createTimeDates(), notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args), async requestDelete(id) { await srSessionPreparation.ensure(); const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestTimeEntriesDelete, id }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to delete time entry."); return result.result; }, async requestSave(method, items) { await srSessionPreparation.ensure(); await currentUser.ensure(); const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestTimeEntriesSave, method, items, emailAddress: currentUser.getUserId() }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to save time entries."); return result.result; }, async requestEntries(weekId) { await srSessionPreparation.ensure(); await currentUser.ensure(); const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestTimeEntries, weekId, emailAddress: currentUser.getUserId() }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to load time entries."); return result.items; }, async requestTaskTypes() { const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestLookupTaskType }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to load Task Types."); return result.items; }, async requestExpand(srNumbers) { const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestServiceRequestExpand, srNumbers }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to load activities."); return result.items; }, async requestServices(weekId) { await srSessionPreparation.ensure(); await currentUser.ensure(); const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestActiveServiceRequests, weekId, emailAddress: currentUser.getUserId() }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to load Service Requests."); return result.items; }, async requestWeek(dateReference) { const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestWeekDetails, dateReference }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to load week details."); return result.items; } }); const dialogDrag = createDialogDrag({ environment }); const contentStyles = createContentStyles({ enableDialogDrag: () => dialogDrag.install(), 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 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({ requestCache: createFormRequestCache({ getHours: () => preferencesState.opptyProductsCacheHours, getUserId: () => currentUser.getUserId() }), messageTypes, environment }); const opportunitiesController = createOpportunitiesController({ loadServiceRequests: (token, version) => opportunityCrmServiceRequests.load(token, version), ensureCurrentUser: () => currentUser.ensure(), resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args), resetWorkloads: () => { rampProjectionView.close(false); productWorkloads.reset(); }, 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({ resetDebugRequests: () => debugState.reset(), 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, srNumber) => { const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId, srNumber }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); return result; } }); const crmSrPopover = createCrmSrPopover({ openInternalSrForm: (parent, trigger) => internalSrForm.open(parent, trigger), formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), requestDetails: (srId, srNumber) => internalSrSession.request(srId, srNumber), MODAL_ID: contracts.MODAL_ID, createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args), createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment }); const opportunityBadgeContent = createOpportunityBadgeContent({ environment }); async function requestSrFormLookup(type, fields = {}) { const result = await platformContentMessages.sendRuntimeMessage({ type, ...fields }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to load Service Request options."); return result.items; } const internalSrForm = createInternalSrForm({ notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args), requestSave: async payload => { const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostInternalServiceRequest, payload, emailAddress: currentUser.getUserId() }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to create Internal Service Request."); return result.result; }, getOpportunity: number => number == null ? null : opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)), MODAL_ID: contracts.MODAL_ID, environment, searchableSelect, formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), requestPlatform: () => requestSrFormLookup(messageTypes.requestLookupTypePlatform), requestServices: parentServiceId => requestSrFormLookup(messageTypes.requestInternalSrServices, { parentServiceId, emailAddress: currentUser.getUserId() }) }); const crmSrForm = createCrmSrForm({ requestSave: async fields => { const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostServiceRequest, ...fields, emailAddress: currentUser.getUserId() }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to save Service Request."); return result; }, notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args), getOpportunity: number => opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)), formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), requestLanguage: () => requestSrFormLookup(messageTypes.requestSrLookupLanguage), requestPlatform: () => requestSrFormLookup(messageTypes.requestSrLookupTechPlatform), requestServices: () => requestSrFormLookup(messageTypes.requestSrLookupOpportunityBusiness, { emailAddress: currentUser.getUserId() }), searchableSelect, MODAL_ID: contracts.MODAL_ID, environment, allowsCrmActivities: () => currentUser.allowsCrmActivities(), isMissing: number => opportunityCrmServiceRequests.getSummary(number)?.state === "missing", createCloseIcon: () => contentIcons.createCloseIcon(), requestPillars: () => requestSrFormLookup(messageTypes.requestSrLookupPrimaryPillar) }); const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({ onStateChange: () => { if (environment.document?.getElementById?.(contracts.OPPORTUNITIES_TABLE_ID)) opportunitiesTable.renderOpportunitiesTable(); }, formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), createSrButton: number => crmSrForm.createButton(number), closeSrForm: () => { internalSrForm.close(false); crmSrForm.close(false); }, setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args), activityCache, reportProgress: (...args) => activityProgress.update("sr", ...args), allowsCrmActivities: () => currentUser.allowsCrmActivities(), bindPopover: (...args) => crmSrPopover.bind(...args), closePopover: (...args) => crmSrPopover.close(...args), requestCrmServiceRequest: (...args) => fusionCrmServiceRequests.requestCrmServiceRequest(...args), isCurrentPeriodRequest: (...args) => opportunitiesController.isCurrentPeriodRequest(...args), environment }); const opportunitiesPrint = createOpportunitiesPrint({ environment, getSnapshot: () => { const state = opportunitiesTableState.opportunitiesTableState; const items = opportunitiesFilters.getSortedOpportunities(); const filter = opportunitiesFilterState; const selectedLabels = (options, selected, allLabel) => { const matching = options.filter(option => selected.has(option.key)); if (options.length && matching.length === options.length) return allLabel; return matching.map(option => option.label).join(", ") || "None"; }; const root = environment.document.getElementById(contracts.MODAL_ID); const filters = [ ["Reference", root?.querySelector(".opportunities-extension-form select")?.value || "-"], ["Customer", selectedLabels(opportunitiesFormatters.getCustomerOptions(), filter.selectedCustomerKeys, "All customers")], ["Owner", selectedLabels(opportunitiesFormatters.getOwnerOptions(), filter.selectedOwnerKeys, "All owners")], ["Search", filter.tableSearch || "All"], ["Status", [...filter.selectedStatuses].join(", ")], ["Stage", [...filter.selectedStages].join(", ")], ["Opportunity type", [...filter.selectedProductTypes].join(", ")] ]; for (const group of root?.querySelectorAll(".opportunities-extension-activity-filter-controls") || []) { filters.push([group.getAttribute("aria-label").replace(/^Filter by /, ""), [...group.querySelectorAll('[aria-pressed="true"]')].map(button => button.getAttribute("aria-label")).join("; ")]); } const totals = [{ label: "Total Opportunities", value: `${items.length}/${state.items.length}`, detail: "Filtered / total" }]; for (const stage of contracts.STAGE_OPTIONS) { const stageItems = items.filter(item => item.ForecastGroup_c === stage); const amount = stageItems.reduce((total, item) => { const value = Number(item.PrimaryRevenue?.RevnAmount); return total + (Number.isFinite(value) ? value : 0); }, 0); totals.push({ label: stage, value: opportunitiesDashboard.createStageAmountFormatter(stageItems)(amount), detail: `${stageItems.length} opportunities` }); } return { status: state.status, filters, totals, columns, rows: items.map(item => columns.map(column => opportunitiesFormatters.displayOpportunityValue(column, item))) }; } }); const opportunityCalendar = createOpportunityCalendar({ createPrintButton: () => opportunitiesPrint.createButton(), productTypeSelection: opportunitiesFilterState.selectedProductTypes, onProductTypeChange: () => opportunitiesTable.renderOpportunitiesTable(), createViewIcon: view => contentIcons.createViewIcon(view), environment, loadLibrary: () => loadOpportunityCalendarLibrary(), getState: () => opportunitiesTableState.opportunitiesTableState, getItems: () => opportunitiesFilters.getSortedOpportunities(), getVersion: () => opportunitiesQueryState.periodRequestVersion, getRange: () => opportunitiesQueryState.dateRange, formatRevenue: item => opportunitiesFormatters.formatRevenue(item), formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), getOwnerName: item => opportunitiesFormatters.getOwnerName(item), createDetailLink: (...args) => opportunitiesLinks.createDetailLink(...args), createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args), createCloseIcon: () => contentIcons.createCloseIcon(), createStageBadge: value => opportunitiesLinks.createStageBadge(value), createStatusBadge: value => opportunitiesLinks.createStatusBadge(value), opportunityUrl: contracts.OPPORTUNITY_DETAIL_URL, modalId: contracts.MODAL_ID, closeSrPopover: () => crmSrPopover.close() }); const opportunitiesTable = createOpportunitiesTable({ tableSorting, renderCalendar: (...args) => opportunityCalendar.render(...args), 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), createOrganizationChartButton: (...args) => orgChart.createButton(...args), createCopyOpportunityButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args), createDetailLink: (...args) => opportunitiesLinks.createDetailLink(...args), createProductsCountButton: (...args) => productsDetails.createProductsCountButton(...args), createProductsRampBadge: (...args) => productsDetails.createProductsRampBadge(...args), createProjectionIndicator: (...args) => rampProjectionView.createOpportunityIndicator(...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({ matchesProductType: item => { const selected = opportunitiesFilterState.selectedProductTypes; if (selected.size === 2) return true; const result = productsContentState.opptyProductsByNumber.get(String(item.OptyNumber)); if (!result || result.error) return false; return selected.has(productsDetails.isBooking(result.items) ? "Booking" : "Workload"); }, 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, searchableSelect, 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, 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(); productWorkloads.reset(); rampProjectionView.close(false); 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, retrySession: () => srSessionPreparation.retry() }); const srSessionPreparation = createSrSessionPreparation({ async onReady() { const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestResourceUser, emailAddress: currentUser.getUserId() }); debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []); if (!result?.ok) throw new Error(result?.error || "Unable to load resource user."); }, ensureCurrentUser: () => currentUser.ensure(), allowsCrmActivities: () => currentUser.allowsCrmActivities(), sendRequest: () => platformContentMessages.sendRuntimeMessage({ type: messageTypes.prepareSrSession }), reportProgress: state => { activityProgress.update("session", state); manageTime.setReady(state === "complete"); }, appendLog: entries => debugContentLog.appendBackgroundRequestLog(entries) }); const activityFilters = createActivityFilters({ setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args), allowsCrmActivities: () => currentUser.allowsCrmActivities(), getSrSummary: number => opportunityCrmServiceRequests.getSummary(number), getActionSummary: number => opportunityWorkloadActions.getSummary(number), renderTable: () => opportunitiesTable.renderOpportunitiesTable(), environment }); const productActivities = createProductActivities({ productsContentState, opportunitiesQueryState, loadCrm: (...args) => opportunityCrmServiceRequests.load(...args), loadActions: (...args) => opportunityWorkloadActions.loadProducts(...args), blockActions: products => opportunityWorkloadActions.blockProducts(products), render: () => { productsContentJobs.renderProductsProgress(); productsContentJobs.queueProductsTableRender(); } }); const productsContentJobs = createProductsContentJobs({ loadWorkloads: (...args) => productWorkloads.load(...args), joinWorkloads: number => productWorkloads.joinProducts(number), 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 productWorkloads = createProductWorkloads({ productsContentState, opportunitiesQueryState, preferencesState, messageTypes, requestConsumerTokenServiceOnce: () => authContentSession.requestConsumerTokenServiceOnce(), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), getPrincipal: () => currentUser.getUserId() || authSessionState.principal, queueProductsTableRender: () => productsContentJobs.queueProductsTableRender() }); const workloadActionOptions = createWorkloadActionOptions({ requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), messageTypes }); const workloadActionForm = createWorkloadActionForm({ searchableSelect, onActionSaved: (...args) => opportunityWorkloadActions.applySavedAction(...args), saveAction: (...args) => workloadActionOptions.save(...args), notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args), getCurrentUserId: () => currentUser.getUserId(), requestTeamMembers: (...args) => workloadActionOptions.requestTeamMembers(...args), requestLookup: (...args) => workloadActionOptions.requestLookup(...args), getProducts: number => productsContentState.opptyProductsByNumber.get(String(number))?.items || [], getOpportunity: number => opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)), formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), MODAL_ID: contracts.MODAL_ID, allowsCrmActivities: () => currentUser.allowsCrmActivities(), isMissingAction: number => opportunityWorkloadActions.getSummary(number)?.state === "missing", createCloseIcon: () => contentIcons.createCloseIcon(), environment }); const opportunityWorkloadActions = createOpportunityWorkloadActions({ setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args), createActionButton: number => workloadActionForm.createButton(number), closeActionForm: (...args) => workloadActionForm.close(...args), activityCache, reportProgress: (...args) => activityProgress.update("actions", ...args), allowsCrmActivities: () => currentUser.allowsCrmActivities(), authSessionState, requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), queueProductsTableRender: () => productsContentJobs.queueProductsTableRender(), productsContentState, messageTypes, environment }); const productsDetails = createProductsDetails({ createProjectionIndicator: (...args) => rampProjectionView.createIndicator(...args), openProjection: (products, opportunity, trigger) => rampProjectionView.open(products[0], opportunity, trigger, products), setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args), OPPORTUNITIES_COLUMNS: columns, formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args), productsContentState, renderOpportunitiesTable: (...args) => opportunitiesTable.renderOpportunitiesTable(...args), environment }); const rampProjection = createRampProjection(); const workloadForecastUpdate = createWorkloadForecastUpdate({ compare: product => rampProjection.compare(product), ensureCurrentUser: () => currentUser.ensure(), getCurrentUserId: () => currentUser.getUserId(), requestAuthorization: () => authContentSession.requestConsumerTokenServiceOnce(), sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args), messageTypes, appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args), productsContentState, queueProductsTableRender: () => productsContentJobs.queueProductsTableRender() }); const rampProjectionView = createRampProjectionView({ tableSorting, saveEstimates: product => workloadForecastUpdate.save(product), notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args), MODAL_ID: contracts.MODAL_ID, compare: product => rampProjection.compare(product), expandProducts: number => productsDetails.expandProducts(number), createAlertIcon: () => contentIcons.createAlertIcon(), createCloseIcon: () => contentIcons.createCloseIcon(), formatAmount: (...args) => productsDetails.formatProductAmount(...args), formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), environment }); const customerTableSearch = createCustomerTableSearch({ environment }); const installBaseView = createInstallBaseView({ tableSorting, 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({ tableSorting, 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({ tableSorting, 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, 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 { opportunityCalendar, orgChart, workloadActionForm, activityFilters, start, contracts, opportunityWorkloadActions, fusionCrmServiceRequests, opportunityCrmServiceRequests, authSessionState, opportunitiesQueryState, opportunitiesTableState, opportunitiesFilterState, preferencesState, productsContentState, consumptionPlansState, consumptionTrendState, opportunitiesDashboardState, debugState, contentFusionPage, contentModal, contentTheme, contentIcons, contentStyles, authContentSession, currentUser, authTokenRelay, authXsrfContent, platformContentMessages, opportunitiesController, fusionOpportunities, opportunitiesFiscalPeriods, opportunitiesTable, opportunitiesDashboard, opportunitiesFilters, opportunitiesFormatters, opportunitiesLinks, preferencesStorage, preferencesView, productsContentJobs, productWorkloads, productActivities, activityProgress, srSessionPreparation, refreshAll, activityCache, productsDetails, rampProjection, workloadForecastUpdate, rampProjectionView, consumptionPlansAction, installBaseAction, installBaseView, serviceRequestsAction, serviceRequestsView, consumptionPlansView, consumptionPlanDetailController, consumptionPlanDetailView, ratecardModel, ratecardView, ratecardController, consumptionTrendController, consumptionChart, consumptionModel, debugContentLog, debugContentView }; } createContentApplication().start(); })();