diff --git a/README.md b/README.md index e6ad844..261795f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Opportunities Extension +# Opportunities Extension 1.0.0 -Extensao de navegador para adicionar o atalho "Opportunities Extension" nas paginas Oracle Fusion permitidas. +Extensao de navegador para adicionar o atalho "Opportunities Extension" nas paginas Oracle Fusion permitidas, consultar oportunidades e exibi-las em uma tabela filtravel. ## Estrutura de desenvolvimento @@ -19,6 +19,14 @@ npm run build O build recria `dist/chromium` e `dist/firefox` a partir da mesma fonte. +## Release 1.0.0 + +- Consulta paginada de oportunidades com filtros por periodo, customer, owner, status, stage e busca textual. +- Ordenacao por coluna, links para oportunidades e contas, copia do numero da oportunidade e indicadores por stage. +- Preferencias de tipos de oportunidade persistidas no navegador. +- Tema claro/escuro baseado no Oracle Redwood. +- Outputs separados para Chromium/Edge e Firefox. + ## Como carregar em modo desenvolvimento ### Chrome ou Microsoft Edge @@ -39,4 +47,4 @@ O build recria `dist/chromium` e `dist/firefox` a partir da mesma fonte. - `https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome` - `https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome` -Ao encontrar o grupo `#yourapps_groupNode_sales`, a extensao adiciona o tile antes do item `.flat-grid-cell.flat-grid-cell-addicon`. Ao clicar no tile, exibe o alerta `Deu certo`. +Ao encontrar o grupo `#yourapps_groupNode_sales`, a extensao adiciona o tile antes do item `.flat-grid-cell.flat-grid-cell-addicon`. Ao clicar no tile, abre a interface de oportunidades. diff --git a/dist/chromium/content.js b/dist/chromium/content.js index c37fae5..2d3a97e 100644 --- a/dist/chromium/content.js +++ b/dist/chromium/content.js @@ -26,6 +26,8 @@ const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard"; 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 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; @@ -40,6 +42,24 @@ }; 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" + ]; let tokenRelayRequest = null; let accessToken = ""; let periodRequestVersion = 0; @@ -52,6 +72,7 @@ let selectedOwnerKeys = new Set(); let ownerSearch = ""; let tableSearch = ""; + let selectedOpportunityTypeValues = new Set(DEFAULT_OPPORTUNITY_TYPE_VALUES); let hasSavedCustomerFilter = false; let hasSavedOwnerFilter = false; let customerFilterInitialized = false; @@ -176,6 +197,7 @@ periodRequestVersion = 0; authStatus = AUTH_STATUS.idle; resetOpportunitiesTable(); + selectedOpportunityTypeValues = new Set(getSavedOpportunityTypeValues()); overlay.id = MODAL_ID; overlay.className = "opportunities-extension-modal"; overlay.setAttribute("data-theme", getSavedTheme()); @@ -226,9 +248,17 @@ }); applyTheme(overlay, themeButton, overlay.getAttribute("data-theme")); + const preferencesButton = document.createElement("button"); + preferencesButton.type = "button"; + preferencesButton.className = "opportunities-extension-icon-button"; + preferencesButton.setAttribute("aria-label", "Abrir preferências"); + preferencesButton.title = "Preferences"; + preferencesButton.append(createSettingsIcon()); + preferencesButton.addEventListener("click", openPreferencesModal); + const headerActions = document.createElement("div"); headerActions.className = "opportunities-extension-header-actions"; - headerActions.append(themeButton, closeButton); + headerActions.append(themeButton, preferencesButton, closeButton); header.append(titleBlock, headerActions); @@ -634,6 +664,15 @@ const dateRange = getFiscalPeriodRange(period, new Date()); const requestVersion = ++periodRequestVersion; + if (selectedOpportunityTypeValues.size === 0) { + setOpportunitiesTableState({ + items: [], + status: "empty", + message: "Select at least one Opportunity Type View in Preferences." + }); + return; + } + if (!dateRange) { setOpportunitiesTableState({ items: [], @@ -1753,10 +1792,11 @@ criteria: [ { op: "$or", - criteria: [ - { op: "$eq", attribute: "RecordSet", value: "ORA_MYSALESTEAMOPTIES" }, - { op: "$eq", attribute: "RecordSet", value: "ORA_MYASSGTERROPTIES" } - ] + criteria: Array.from(selectedOpportunityTypeValues, (value) => ({ + op: "$eq", + attribute: "RecordSet", + value + })) }, { op: "$wi", @@ -2235,6 +2275,7 @@ const modal = document.getElementById(MODAL_ID); if (modal) { + closePreferencesModal(); modal.hidden = true; } @@ -2288,6 +2329,185 @@ 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 getSavedOpportunityTypeValues() { + try { + const rawPreferences = localStorage.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 openPreferencesModal() { + const overlay = document.getElementById(MODAL_ID); + + if (!overlay) { + return; + } + + const existingModal = document.getElementById(PREFERENCES_MODAL_ID); + + if (existingModal) { + existingModal.hidden = false; + existingModal.querySelector("input")?.focus(); + return; + } + + const preferencesModal = document.createElement("section"); + preferencesModal.id = PREFERENCES_MODAL_ID; + preferencesModal.className = "opportunities-extension-preferences-modal"; + preferencesModal.setAttribute("role", "dialog"); + preferencesModal.setAttribute("aria-modal", "true"); + preferencesModal.setAttribute("aria-labelledby", "opportunities-extension-preferences-title"); + + const dialog = document.createElement("div"); + dialog.className = "opportunities-extension-preferences-dialog"; + + const header = document.createElement("header"); + header.className = "opportunities-extension-preferences-header"; + + const title = document.createElement("h2"); + title.id = "opportunities-extension-preferences-title"; + title.textContent = "Preferences"; + + const closeButton = document.createElement("button"); + closeButton.type = "button"; + closeButton.className = "opportunities-extension-icon-button"; + closeButton.setAttribute("aria-label", "Fechar preferências"); + closeButton.title = "Close"; + closeButton.append(createCloseIcon()); + closeButton.addEventListener("click", closePreferencesModal); + header.append(title, closeButton); + + const content = document.createElement("div"); + content.className = "opportunities-extension-preferences-content"; + + const fieldset = document.createElement("fieldset"); + fieldset.className = "opportunities-extension-preferences-fieldset"; + + const legend = document.createElement("legend"); + legend.textContent = "Opportunity Type View"; + fieldset.append(legend); + + const selectedValues = new Set(getSavedOpportunityTypeValues()); + const options = document.createElement("div"); + options.className = "opportunities-extension-preferences-options"; + + OPPORTUNITY_TYPE_OPTIONS.forEach((option) => { + const label = document.createElement("label"); + label.className = "opportunities-extension-preferences-option"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.name = "opportunityTypeView"; + checkbox.value = option.value; + checkbox.checked = selectedValues.has(option.value); + + const text = document.createElement("span"); + text.textContent = option.label; + label.append(checkbox, text); + options.append(label); + }); + + fieldset.append(options); + content.append(fieldset); + + 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); + selectedOpportunityTypeValues = new Set(selectedValues); + + try { + localStorage.setItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, JSON.stringify({ + opportunityTypeValues: selectedValues + })); + } 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; + } + } + function ensureEscapeKeyHandler() { if (window[ESCAPE_LISTENER_KEY]) { return; @@ -2309,6 +2529,14 @@ } if (event.key === "Escape") { + const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID); + + if (preferencesModal && !preferencesModal.hidden) { + closePreferencesModal(); + event.preventDefault(); + return; + } + if (closeCustomerFilterPanel()) { event.preventDefault(); return; @@ -2485,6 +2713,11 @@ vertical-align: middle; } + .opportunities-extension-icon-button .opportunities-extension-settings-icon { + width: 20px; + height: 20px; + } + .opportunities-extension-icon-button:hover, .opportunities-extension-icon-button:focus { border-color: #b8b2ad; @@ -3313,6 +3546,135 @@ border: 0; } + .opportunities-extension-preferences-modal { + position: absolute; + inset: 0; + z-index: 3; + display: grid; + place-items: center; + padding: 24px; + background: rgba(0, 0, 0, .42); + } + + .opportunities-extension-preferences-modal[hidden] { + display: none !important; + } + + .opportunities-extension-preferences-dialog { + display: grid; + width: min(680px, 100%); + max-height: min(720px, calc(100vh - 48px)); + grid-template-rows: auto minmax(0, 1fr) auto; + overflow: hidden; + border: 1px solid #8f8a85; + border-radius: 4px; + background: #ffffff; + box-shadow: 0 8px 24px rgba(0, 0, 0, .24); + color: #312d2a; + } + + .opportunities-extension-preferences-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 18px 20px; + border-bottom: 1px solid #dedbd7; + } + + .opportunities-extension-preferences-header h2 { + margin: 0; + font-size: 20px; + font-weight: 700; + line-height: 1.2; + } + + .opportunities-extension-preferences-content { + min-height: 0; + overflow: auto; + padding: 20px; + } + + .opportunities-extension-preferences-fieldset { + min-width: 0; + margin: 0; + padding: 0; + border: 0; + } + + .opportunities-extension-preferences-fieldset legend { + margin-bottom: 12px; + padding: 0; + color: #312d2a; + font-size: 15px; + font-weight: 700; + line-height: 1.3; + } + + .opportunities-extension-preferences-options { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px 20px; + } + + .opportunities-extension-preferences-option { + display: flex; + align-items: flex-start; + gap: 10px; + min-width: 0; + padding: 9px 8px; + border-radius: 4px; + color: #312d2a; + font-size: 14px; + line-height: 1.35; + cursor: pointer; + } + + .opportunities-extension-preferences-option:hover { + background: #f0eeeb; + } + + .opportunities-extension-preferences-option input { + width: 18px; + height: 18px; + flex: 0 0 18px; + margin: 0; + accent-color: #00758f; + cursor: pointer; + } + + .opportunities-extension-preferences-footer { + display: flex; + justify-content: flex-end; + padding: 14px 20px; + border-top: 1px solid #dedbd7; + } + + .opportunities-extension-button { + min-width: 88px; + min-height: 36px; + padding: 7px 16px; + border: 1px solid transparent; + border-radius: 4px; + font-size: 14px; + font-weight: 600; + line-height: 1.2; + cursor: pointer; + } + + .opportunities-extension-button-primary { + border-color: #00758f; + background: #00758f; + color: #ffffff; + } + + .opportunities-extension-button-primary:hover, + .opportunities-extension-button-primary:focus-visible { + border-color: #005e73; + background: #005e73; + outline: none; + } + .opportunities-extension-debug-panel { position: fixed; right: 24px; @@ -3511,6 +3873,7 @@ .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-dialog, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] { border-color: #4e4a46; @@ -3548,6 +3911,9 @@ .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header h2, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-fieldset legend, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 { @@ -3559,6 +3925,30 @@ border-bottom-color: #4e4a46; } + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-footer { + border-color: #4e4a46; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option:hover { + background: #3b3937; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option input { + accent-color: #43c4d5; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary { + border-color: #00a6be; + background: #008aa6; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:hover, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:focus-visible { + border-color: #43c4d5; + background: #006d7a; + } + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table tbody tr:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sort-button:hover, @@ -3826,6 +4216,18 @@ font-size: 18px; } + .opportunities-extension-preferences-modal { + padding: 12px; + } + + .opportunities-extension-preferences-dialog { + max-height: calc(100vh - 24px); + } + + .opportunities-extension-preferences-options { + grid-template-columns: 1fr; + } + .opportunities-extension-stage-filter-controls { gap: 6px; } diff --git a/dist/chromium/manifest.json b/dist/chromium/manifest.json index ddb6efd..6ab29d8 100644 --- a/dist/chromium/manifest.json +++ b/dist/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Opportunities Extension", "description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.", - "version": "0.1.0", + "version": "1.0.0", "permissions": [ "cookies" ], diff --git a/dist/firefox/content.js b/dist/firefox/content.js index c37fae5..2d3a97e 100644 --- a/dist/firefox/content.js +++ b/dist/firefox/content.js @@ -26,6 +26,8 @@ const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard"; 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 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; @@ -40,6 +42,24 @@ }; 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" + ]; let tokenRelayRequest = null; let accessToken = ""; let periodRequestVersion = 0; @@ -52,6 +72,7 @@ let selectedOwnerKeys = new Set(); let ownerSearch = ""; let tableSearch = ""; + let selectedOpportunityTypeValues = new Set(DEFAULT_OPPORTUNITY_TYPE_VALUES); let hasSavedCustomerFilter = false; let hasSavedOwnerFilter = false; let customerFilterInitialized = false; @@ -176,6 +197,7 @@ periodRequestVersion = 0; authStatus = AUTH_STATUS.idle; resetOpportunitiesTable(); + selectedOpportunityTypeValues = new Set(getSavedOpportunityTypeValues()); overlay.id = MODAL_ID; overlay.className = "opportunities-extension-modal"; overlay.setAttribute("data-theme", getSavedTheme()); @@ -226,9 +248,17 @@ }); applyTheme(overlay, themeButton, overlay.getAttribute("data-theme")); + const preferencesButton = document.createElement("button"); + preferencesButton.type = "button"; + preferencesButton.className = "opportunities-extension-icon-button"; + preferencesButton.setAttribute("aria-label", "Abrir preferências"); + preferencesButton.title = "Preferences"; + preferencesButton.append(createSettingsIcon()); + preferencesButton.addEventListener("click", openPreferencesModal); + const headerActions = document.createElement("div"); headerActions.className = "opportunities-extension-header-actions"; - headerActions.append(themeButton, closeButton); + headerActions.append(themeButton, preferencesButton, closeButton); header.append(titleBlock, headerActions); @@ -634,6 +664,15 @@ const dateRange = getFiscalPeriodRange(period, new Date()); const requestVersion = ++periodRequestVersion; + if (selectedOpportunityTypeValues.size === 0) { + setOpportunitiesTableState({ + items: [], + status: "empty", + message: "Select at least one Opportunity Type View in Preferences." + }); + return; + } + if (!dateRange) { setOpportunitiesTableState({ items: [], @@ -1753,10 +1792,11 @@ criteria: [ { op: "$or", - criteria: [ - { op: "$eq", attribute: "RecordSet", value: "ORA_MYSALESTEAMOPTIES" }, - { op: "$eq", attribute: "RecordSet", value: "ORA_MYASSGTERROPTIES" } - ] + criteria: Array.from(selectedOpportunityTypeValues, (value) => ({ + op: "$eq", + attribute: "RecordSet", + value + })) }, { op: "$wi", @@ -2235,6 +2275,7 @@ const modal = document.getElementById(MODAL_ID); if (modal) { + closePreferencesModal(); modal.hidden = true; } @@ -2288,6 +2329,185 @@ 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 getSavedOpportunityTypeValues() { + try { + const rawPreferences = localStorage.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 openPreferencesModal() { + const overlay = document.getElementById(MODAL_ID); + + if (!overlay) { + return; + } + + const existingModal = document.getElementById(PREFERENCES_MODAL_ID); + + if (existingModal) { + existingModal.hidden = false; + existingModal.querySelector("input")?.focus(); + return; + } + + const preferencesModal = document.createElement("section"); + preferencesModal.id = PREFERENCES_MODAL_ID; + preferencesModal.className = "opportunities-extension-preferences-modal"; + preferencesModal.setAttribute("role", "dialog"); + preferencesModal.setAttribute("aria-modal", "true"); + preferencesModal.setAttribute("aria-labelledby", "opportunities-extension-preferences-title"); + + const dialog = document.createElement("div"); + dialog.className = "opportunities-extension-preferences-dialog"; + + const header = document.createElement("header"); + header.className = "opportunities-extension-preferences-header"; + + const title = document.createElement("h2"); + title.id = "opportunities-extension-preferences-title"; + title.textContent = "Preferences"; + + const closeButton = document.createElement("button"); + closeButton.type = "button"; + closeButton.className = "opportunities-extension-icon-button"; + closeButton.setAttribute("aria-label", "Fechar preferências"); + closeButton.title = "Close"; + closeButton.append(createCloseIcon()); + closeButton.addEventListener("click", closePreferencesModal); + header.append(title, closeButton); + + const content = document.createElement("div"); + content.className = "opportunities-extension-preferences-content"; + + const fieldset = document.createElement("fieldset"); + fieldset.className = "opportunities-extension-preferences-fieldset"; + + const legend = document.createElement("legend"); + legend.textContent = "Opportunity Type View"; + fieldset.append(legend); + + const selectedValues = new Set(getSavedOpportunityTypeValues()); + const options = document.createElement("div"); + options.className = "opportunities-extension-preferences-options"; + + OPPORTUNITY_TYPE_OPTIONS.forEach((option) => { + const label = document.createElement("label"); + label.className = "opportunities-extension-preferences-option"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.name = "opportunityTypeView"; + checkbox.value = option.value; + checkbox.checked = selectedValues.has(option.value); + + const text = document.createElement("span"); + text.textContent = option.label; + label.append(checkbox, text); + options.append(label); + }); + + fieldset.append(options); + content.append(fieldset); + + 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); + selectedOpportunityTypeValues = new Set(selectedValues); + + try { + localStorage.setItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, JSON.stringify({ + opportunityTypeValues: selectedValues + })); + } 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; + } + } + function ensureEscapeKeyHandler() { if (window[ESCAPE_LISTENER_KEY]) { return; @@ -2309,6 +2529,14 @@ } if (event.key === "Escape") { + const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID); + + if (preferencesModal && !preferencesModal.hidden) { + closePreferencesModal(); + event.preventDefault(); + return; + } + if (closeCustomerFilterPanel()) { event.preventDefault(); return; @@ -2485,6 +2713,11 @@ vertical-align: middle; } + .opportunities-extension-icon-button .opportunities-extension-settings-icon { + width: 20px; + height: 20px; + } + .opportunities-extension-icon-button:hover, .opportunities-extension-icon-button:focus { border-color: #b8b2ad; @@ -3313,6 +3546,135 @@ border: 0; } + .opportunities-extension-preferences-modal { + position: absolute; + inset: 0; + z-index: 3; + display: grid; + place-items: center; + padding: 24px; + background: rgba(0, 0, 0, .42); + } + + .opportunities-extension-preferences-modal[hidden] { + display: none !important; + } + + .opportunities-extension-preferences-dialog { + display: grid; + width: min(680px, 100%); + max-height: min(720px, calc(100vh - 48px)); + grid-template-rows: auto minmax(0, 1fr) auto; + overflow: hidden; + border: 1px solid #8f8a85; + border-radius: 4px; + background: #ffffff; + box-shadow: 0 8px 24px rgba(0, 0, 0, .24); + color: #312d2a; + } + + .opportunities-extension-preferences-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 18px 20px; + border-bottom: 1px solid #dedbd7; + } + + .opportunities-extension-preferences-header h2 { + margin: 0; + font-size: 20px; + font-weight: 700; + line-height: 1.2; + } + + .opportunities-extension-preferences-content { + min-height: 0; + overflow: auto; + padding: 20px; + } + + .opportunities-extension-preferences-fieldset { + min-width: 0; + margin: 0; + padding: 0; + border: 0; + } + + .opportunities-extension-preferences-fieldset legend { + margin-bottom: 12px; + padding: 0; + color: #312d2a; + font-size: 15px; + font-weight: 700; + line-height: 1.3; + } + + .opportunities-extension-preferences-options { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px 20px; + } + + .opportunities-extension-preferences-option { + display: flex; + align-items: flex-start; + gap: 10px; + min-width: 0; + padding: 9px 8px; + border-radius: 4px; + color: #312d2a; + font-size: 14px; + line-height: 1.35; + cursor: pointer; + } + + .opportunities-extension-preferences-option:hover { + background: #f0eeeb; + } + + .opportunities-extension-preferences-option input { + width: 18px; + height: 18px; + flex: 0 0 18px; + margin: 0; + accent-color: #00758f; + cursor: pointer; + } + + .opportunities-extension-preferences-footer { + display: flex; + justify-content: flex-end; + padding: 14px 20px; + border-top: 1px solid #dedbd7; + } + + .opportunities-extension-button { + min-width: 88px; + min-height: 36px; + padding: 7px 16px; + border: 1px solid transparent; + border-radius: 4px; + font-size: 14px; + font-weight: 600; + line-height: 1.2; + cursor: pointer; + } + + .opportunities-extension-button-primary { + border-color: #00758f; + background: #00758f; + color: #ffffff; + } + + .opportunities-extension-button-primary:hover, + .opportunities-extension-button-primary:focus-visible { + border-color: #005e73; + background: #005e73; + outline: none; + } + .opportunities-extension-debug-panel { position: fixed; right: 24px; @@ -3511,6 +3873,7 @@ .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-dialog, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] { border-color: #4e4a46; @@ -3548,6 +3911,9 @@ .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header h2, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-fieldset legend, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 { @@ -3559,6 +3925,30 @@ border-bottom-color: #4e4a46; } + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-footer { + border-color: #4e4a46; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option:hover { + background: #3b3937; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option input { + accent-color: #43c4d5; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary { + border-color: #00a6be; + background: #008aa6; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:hover, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:focus-visible { + border-color: #43c4d5; + background: #006d7a; + } + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table tbody tr:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sort-button:hover, @@ -3826,6 +4216,18 @@ font-size: 18px; } + .opportunities-extension-preferences-modal { + padding: 12px; + } + + .opportunities-extension-preferences-dialog { + max-height: calc(100vh - 24px); + } + + .opportunities-extension-preferences-options { + grid-template-columns: 1fr; + } + .opportunities-extension-stage-filter-controls { gap: 6px; } diff --git a/dist/firefox/manifest.json b/dist/firefox/manifest.json index 50c12a3..9e02624 100644 --- a/dist/firefox/manifest.json +++ b/dist/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Opportunities Extension", "description": "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.", - "version": "0.1.0", + "version": "1.0.0", "permissions": [ "cookies" ], diff --git a/package.json b/package.json index 956cb9f..04a8644 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opportunities-extension", - "version": "0.1.0", + "version": "1.0.0", "private": true, "type": "module", "scripts": { diff --git a/scripts/build.mjs b/scripts/build.mjs index 391101c..ff41537 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -13,7 +13,7 @@ const baseManifest = { manifest_version: 3, name: "Opportunities Extension", description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.", - version: "0.1.0", + version: "1.0.0", permissions: [ "cookies" ], diff --git a/src/content.js b/src/content.js index c37fae5..2d3a97e 100644 --- a/src/content.js +++ b/src/content.js @@ -26,6 +26,8 @@ const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard"; 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 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; @@ -40,6 +42,24 @@ }; 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" + ]; let tokenRelayRequest = null; let accessToken = ""; let periodRequestVersion = 0; @@ -52,6 +72,7 @@ let selectedOwnerKeys = new Set(); let ownerSearch = ""; let tableSearch = ""; + let selectedOpportunityTypeValues = new Set(DEFAULT_OPPORTUNITY_TYPE_VALUES); let hasSavedCustomerFilter = false; let hasSavedOwnerFilter = false; let customerFilterInitialized = false; @@ -176,6 +197,7 @@ periodRequestVersion = 0; authStatus = AUTH_STATUS.idle; resetOpportunitiesTable(); + selectedOpportunityTypeValues = new Set(getSavedOpportunityTypeValues()); overlay.id = MODAL_ID; overlay.className = "opportunities-extension-modal"; overlay.setAttribute("data-theme", getSavedTheme()); @@ -226,9 +248,17 @@ }); applyTheme(overlay, themeButton, overlay.getAttribute("data-theme")); + const preferencesButton = document.createElement("button"); + preferencesButton.type = "button"; + preferencesButton.className = "opportunities-extension-icon-button"; + preferencesButton.setAttribute("aria-label", "Abrir preferências"); + preferencesButton.title = "Preferences"; + preferencesButton.append(createSettingsIcon()); + preferencesButton.addEventListener("click", openPreferencesModal); + const headerActions = document.createElement("div"); headerActions.className = "opportunities-extension-header-actions"; - headerActions.append(themeButton, closeButton); + headerActions.append(themeButton, preferencesButton, closeButton); header.append(titleBlock, headerActions); @@ -634,6 +664,15 @@ const dateRange = getFiscalPeriodRange(period, new Date()); const requestVersion = ++periodRequestVersion; + if (selectedOpportunityTypeValues.size === 0) { + setOpportunitiesTableState({ + items: [], + status: "empty", + message: "Select at least one Opportunity Type View in Preferences." + }); + return; + } + if (!dateRange) { setOpportunitiesTableState({ items: [], @@ -1753,10 +1792,11 @@ criteria: [ { op: "$or", - criteria: [ - { op: "$eq", attribute: "RecordSet", value: "ORA_MYSALESTEAMOPTIES" }, - { op: "$eq", attribute: "RecordSet", value: "ORA_MYASSGTERROPTIES" } - ] + criteria: Array.from(selectedOpportunityTypeValues, (value) => ({ + op: "$eq", + attribute: "RecordSet", + value + })) }, { op: "$wi", @@ -2235,6 +2275,7 @@ const modal = document.getElementById(MODAL_ID); if (modal) { + closePreferencesModal(); modal.hidden = true; } @@ -2288,6 +2329,185 @@ 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 getSavedOpportunityTypeValues() { + try { + const rawPreferences = localStorage.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 openPreferencesModal() { + const overlay = document.getElementById(MODAL_ID); + + if (!overlay) { + return; + } + + const existingModal = document.getElementById(PREFERENCES_MODAL_ID); + + if (existingModal) { + existingModal.hidden = false; + existingModal.querySelector("input")?.focus(); + return; + } + + const preferencesModal = document.createElement("section"); + preferencesModal.id = PREFERENCES_MODAL_ID; + preferencesModal.className = "opportunities-extension-preferences-modal"; + preferencesModal.setAttribute("role", "dialog"); + preferencesModal.setAttribute("aria-modal", "true"); + preferencesModal.setAttribute("aria-labelledby", "opportunities-extension-preferences-title"); + + const dialog = document.createElement("div"); + dialog.className = "opportunities-extension-preferences-dialog"; + + const header = document.createElement("header"); + header.className = "opportunities-extension-preferences-header"; + + const title = document.createElement("h2"); + title.id = "opportunities-extension-preferences-title"; + title.textContent = "Preferences"; + + const closeButton = document.createElement("button"); + closeButton.type = "button"; + closeButton.className = "opportunities-extension-icon-button"; + closeButton.setAttribute("aria-label", "Fechar preferências"); + closeButton.title = "Close"; + closeButton.append(createCloseIcon()); + closeButton.addEventListener("click", closePreferencesModal); + header.append(title, closeButton); + + const content = document.createElement("div"); + content.className = "opportunities-extension-preferences-content"; + + const fieldset = document.createElement("fieldset"); + fieldset.className = "opportunities-extension-preferences-fieldset"; + + const legend = document.createElement("legend"); + legend.textContent = "Opportunity Type View"; + fieldset.append(legend); + + const selectedValues = new Set(getSavedOpportunityTypeValues()); + const options = document.createElement("div"); + options.className = "opportunities-extension-preferences-options"; + + OPPORTUNITY_TYPE_OPTIONS.forEach((option) => { + const label = document.createElement("label"); + label.className = "opportunities-extension-preferences-option"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.name = "opportunityTypeView"; + checkbox.value = option.value; + checkbox.checked = selectedValues.has(option.value); + + const text = document.createElement("span"); + text.textContent = option.label; + label.append(checkbox, text); + options.append(label); + }); + + fieldset.append(options); + content.append(fieldset); + + 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); + selectedOpportunityTypeValues = new Set(selectedValues); + + try { + localStorage.setItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, JSON.stringify({ + opportunityTypeValues: selectedValues + })); + } 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; + } + } + function ensureEscapeKeyHandler() { if (window[ESCAPE_LISTENER_KEY]) { return; @@ -2309,6 +2529,14 @@ } if (event.key === "Escape") { + const preferencesModal = document.getElementById(PREFERENCES_MODAL_ID); + + if (preferencesModal && !preferencesModal.hidden) { + closePreferencesModal(); + event.preventDefault(); + return; + } + if (closeCustomerFilterPanel()) { event.preventDefault(); return; @@ -2485,6 +2713,11 @@ vertical-align: middle; } + .opportunities-extension-icon-button .opportunities-extension-settings-icon { + width: 20px; + height: 20px; + } + .opportunities-extension-icon-button:hover, .opportunities-extension-icon-button:focus { border-color: #b8b2ad; @@ -3313,6 +3546,135 @@ border: 0; } + .opportunities-extension-preferences-modal { + position: absolute; + inset: 0; + z-index: 3; + display: grid; + place-items: center; + padding: 24px; + background: rgba(0, 0, 0, .42); + } + + .opportunities-extension-preferences-modal[hidden] { + display: none !important; + } + + .opportunities-extension-preferences-dialog { + display: grid; + width: min(680px, 100%); + max-height: min(720px, calc(100vh - 48px)); + grid-template-rows: auto minmax(0, 1fr) auto; + overflow: hidden; + border: 1px solid #8f8a85; + border-radius: 4px; + background: #ffffff; + box-shadow: 0 8px 24px rgba(0, 0, 0, .24); + color: #312d2a; + } + + .opportunities-extension-preferences-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 18px 20px; + border-bottom: 1px solid #dedbd7; + } + + .opportunities-extension-preferences-header h2 { + margin: 0; + font-size: 20px; + font-weight: 700; + line-height: 1.2; + } + + .opportunities-extension-preferences-content { + min-height: 0; + overflow: auto; + padding: 20px; + } + + .opportunities-extension-preferences-fieldset { + min-width: 0; + margin: 0; + padding: 0; + border: 0; + } + + .opportunities-extension-preferences-fieldset legend { + margin-bottom: 12px; + padding: 0; + color: #312d2a; + font-size: 15px; + font-weight: 700; + line-height: 1.3; + } + + .opportunities-extension-preferences-options { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px 20px; + } + + .opportunities-extension-preferences-option { + display: flex; + align-items: flex-start; + gap: 10px; + min-width: 0; + padding: 9px 8px; + border-radius: 4px; + color: #312d2a; + font-size: 14px; + line-height: 1.35; + cursor: pointer; + } + + .opportunities-extension-preferences-option:hover { + background: #f0eeeb; + } + + .opportunities-extension-preferences-option input { + width: 18px; + height: 18px; + flex: 0 0 18px; + margin: 0; + accent-color: #00758f; + cursor: pointer; + } + + .opportunities-extension-preferences-footer { + display: flex; + justify-content: flex-end; + padding: 14px 20px; + border-top: 1px solid #dedbd7; + } + + .opportunities-extension-button { + min-width: 88px; + min-height: 36px; + padding: 7px 16px; + border: 1px solid transparent; + border-radius: 4px; + font-size: 14px; + font-weight: 600; + line-height: 1.2; + cursor: pointer; + } + + .opportunities-extension-button-primary { + border-color: #00758f; + background: #00758f; + color: #ffffff; + } + + .opportunities-extension-button-primary:hover, + .opportunities-extension-button-primary:focus-visible { + border-color: #005e73; + background: #005e73; + outline: none; + } + .opportunities-extension-debug-panel { position: fixed; right: 24px; @@ -3511,6 +3873,7 @@ .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-dialog, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] { border-color: #4e4a46; @@ -3548,6 +3911,9 @@ .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-select-all, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header h2, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-fieldset legend, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 { @@ -3559,6 +3925,30 @@ border-bottom-color: #4e4a46; } + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-footer { + border-color: #4e4a46; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option:hover { + background: #3b3937; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option input { + accent-color: #43c4d5; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary { + border-color: #00a6be; + background: #008aa6; + } + + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:hover, + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-button-primary:focus-visible { + border-color: #43c4d5; + background: #006d7a; + } + .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-option:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table tbody tr:hover, .opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sort-button:hover, @@ -3826,6 +4216,18 @@ font-size: 18px; } + .opportunities-extension-preferences-modal { + padding: 12px; + } + + .opportunities-extension-preferences-dialog { + max-height: calc(100vh - 24px); + } + + .opportunities-extension-preferences-options { + grid-template-columns: 1fr; + } + .opportunities-extension-stage-filter-controls { gap: 6px; }