Compare commits

...

10 Commits

13 changed files with 13638 additions and 369 deletions

View File

@@ -1,42 +1,52 @@
# Opportunities Extension
# Opportunities Extension 1.0.0
Extensao de navegador para adicionar o atalho "Opportunities Extension" nas paginas Oracle Fusion permitidas.
Browser extension that adds the "Opportunities Extension" shortcut to the permitted Oracle Fusion pages, retrieves opportunities, and displays them in a filterable table.
## Estrutura de desenvolvimento
## Development Structure
- `src/content.js`: script unico mantido no projeto.
- `scripts/build.mjs`: gera os pacotes especificos para cada navegador.
- `dist/chromium/`: output para Google Chrome e Microsoft Edge.
- `dist/firefox/`: output para Firefox.
- `src/content.js`: single source file maintained by the project.
- `scripts/build.mjs`: generates browser-specific packages.
- `dist/chromium/`: output for Google Chrome and Microsoft Edge.
- `dist/firefox/`: output for Firefox.
## Build
Use o Node.js:
Use Node.js:
```bash
npm run build
```
O build recria `dist/chromium` e `dist/firefox` a partir da mesma fonte.
The build recreates `dist/chromium` and `dist/firefox` from the same source.
## Como carregar em modo desenvolvimento
## Release 1.0.0
- Paginated opportunity retrieval with filters for reference, customer, owner, status, stage, and text search.
- Column sorting, links to opportunities and accounts, opportunity number copying, and stage indicators.
- Opportunity type view preferences persisted in the browser.
- Background product retrieval with configurable caching, progress tracking, and expandable product details.
- Cloud consumption account-plan lookup from each opportunity, displayed in a tabbed detail modal.
- Light and dark themes based on Oracle Redwood.
- Separate outputs for Chromium/Edge and Firefox.
## Loading in Development Mode
### Chrome ou Microsoft Edge
1. Abra `chrome://extensions` ou `edge://extensions`.
2. Ative o modo de desenvolvedor.
3. Clique em "Carregar sem compactacao".
4. Selecione a pasta `dist/chromium`.
1. Open `chrome://extensions` or `edge://extensions`.
2. Enable Developer mode.
3. Click **Load unpacked**.
4. Select the `dist/chromium` folder.
### Firefox
1. Abra `about:debugging#/runtime/this-firefox`.
2. Clique em "Carregar extensao temporaria".
3. Selecione o arquivo `dist/firefox/manifest.json`.
1. Open `about:debugging#/runtime/this-firefox`.
2. Click **Load Temporary Add-on**.
3. Select the `dist/firefox/manifest.json` file.
## Paginas atendidas
## Supported Pages
- `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`.
When it finds the `#yourapps_groupNode_sales` group, the extension adds the tile before the `.flat-grid-cell.flat-grid-cell-addicon` item. Clicking the tile opens the opportunities interface.

File diff suppressed because it is too large Load Diff

3553
dist/chromium/content.js vendored

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,14 @@
"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"
"cookies",
"storage"
],
"host_permissions": [
"https://eeho.fa.us2.oraclecloud.com/*"
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*"
],
"content_scripts": [
{

File diff suppressed because it is too large Load Diff

3553
dist/firefox/content.js vendored

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,14 @@
"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"
"cookies",
"storage"
],
"host_permissions": [
"https://eeho.fa.us2.oraclecloud.com/*"
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*"
],
"content_scripts": [
{

View File

@@ -1,10 +1,11 @@
{
"name": "opportunities-extension",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "node scripts/build.mjs",
"check": "node --check src/content.js && node scripts/build.mjs --check"
"check": "node --check src/content.js && node --check src/background.js && node tests/consumptionTrend.test.cjs && node scripts/build.mjs --check",
"test": "node tests/consumptionTrend.test.cjs"
}
}

View File

@@ -13,12 +13,14 @@ 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"
"cookies",
"storage"
],
host_permissions: [
"https://eeho.fa.us2.oraclecloud.com/*"
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*"
],
content_scripts: [
{

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const root = path.resolve(__dirname, "..");
function loadContentHelpers() {
const source = fs.readFileSync(path.join(root, "src", "content.js"), "utf8");
const marker = "\n if (insertTile()) {";
const instrumented = source.replace(marker, `
globalThis.__consumptionTrendTest = {
normalizeConsumptionTrendItems,
formatConsumptionPointLabel,
createNiceConsumptionMaximum,
calculateAverageConsumption
};
${marker}`);
const context = {
chrome: { runtime: { onMessage: { addListener() {} } } },
document: { getElementById: () => ({}) },
window: {
location: {
hostname: "eeho.fa.us2.oraclecloud.com",
pathname: "/fscmUI/faces/FuseWelcome"
}
},
Intl,
Date,
Map,
Set,
Number,
String,
Array,
Object,
Math
};
vm.runInNewContext(instrumented, context);
return context.__consumptionTrendTest;
}
function loadBackground(fetchCalls) {
let listener;
const source = fs.readFileSync(path.join(root, "src", "background.js"), "utf8");
const headers = {
get(name) {
return name.toLowerCase() === "content-type" ? "application/json" : null;
},
forEach(callback) {
callback("application/json", "content-type");
}
};
const context = {
chrome: {
runtime: {
onMessage: {
addListener(callback) {
listener = callback;
}
}
}
},
fetch: async (url, options) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
statusText: "OK",
headers,
text: async () => JSON.stringify([{ fyDate: "2026-09-01", usedAmount: 25 }])
};
},
URL,
Date,
JSON,
Map,
Set,
Promise,
String,
Array,
Object,
Number,
Math,
setTimeout,
clearTimeout
};
vm.runInNewContext(source, context);
return listener;
}
function dispatch(listener, message) {
return new Promise((resolve) => {
const asynchronous = listener(message, {}, resolve);
assert.equal(asynchronous, true);
});
}
async function run() {
const helpers = loadContentHelpers();
const points = helpers.normalizeConsumptionTrendItems([
{
fyDate: "2026-08-22",
fiscalTime: ["22-Aug-26"],
fiscalQtr: "FY27-Q1",
usageCategory: "Funded Allocation",
usedAmount: 31000,
arrCd: 120000,
currencyCode: "BRL"
},
{
fyDate: "2026-08-22",
fiscalTime: ["22-Aug-26"],
usageCategory: "OVERAGE",
usedAmount: 4200,
currencyCode: "BRL"
},
{
fyDate: "2026-08-15",
fiscalTime: ["15-Aug-26"],
usageCategory: null,
usedAmount: 29000,
currencyCode: "BRL"
}
]);
assert.equal(points.length, 2);
assert.equal(points[0].dateKey, "2026-08-15");
assert.equal(points[0].funded, 29000);
assert.equal(points[1].funded, 31000);
assert.equal(points[1].overage, 4200);
assert.equal(points[1].total, 35200);
assert.equal(points[1].arr, 120000);
assert.equal(helpers.formatConsumptionPointLabel(points[1], "weekly"), "22-Aug-26");
assert.equal(helpers.createNiceConsumptionMaximum(35200), 50000);
assert.equal(helpers.calculateAverageConsumption(points, true), 29000);
assert.equal(helpers.calculateAverageConsumption(points, false), 32100);
assert.equal(helpers.calculateAverageConsumption(points, true, new Set(["funded"])), 29000);
assert.equal(helpers.calculateAverageConsumption(points, false, new Set(["overage"])), 2100);
const fetchCalls = [];
const listener = loadBackground(fetchCalls);
const periods = ["quarterly", "monthly", "weekly", "daily"];
for (const period of periods) {
const response = await dispatch(listener, {
type: "opportunitiesExtension.requestConsumptionTrend",
subPlanNum: "11276098",
period,
authorization: { name: "g2m-authorization", value: "ocicons-secret" }
});
assert.equal(response.ok, true);
assert.equal(response.period, period);
assert.equal(response.items.length, 1);
assert.equal(response.debugRequests[0].name, `requestConsumptionService${period[0].toUpperCase()}${period.slice(1)}`);
}
assert.equal(fetchCalls.length, 4);
fetchCalls.forEach((call, index) => {
assert.equal(call.options.method, "GET");
assert.equal(call.options.headers["G2m-Authorization"], "ocicons-secret");
assert.match(call.url, /\/11276098\?quarter=All&timePeriod=/);
assert.ok(call.url.endsWith(periods[index]));
});
console.log("Consumption trend tests passed.");
}
run().catch((error) => {
console.error(error);
process.exitCode = 1;
});

103
tests/fixtures/consumption-trend.html vendored Normal file
View File

@@ -0,0 +1,103 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Consumption Trend Visual Test</title>
<style>
html, body { width: 100%; min-height: 100%; margin: 0; }
body { font-family: Arial, sans-serif; }
#opportunities-extension-modal { position: fixed; inset: 0; }
</style>
</head>
<body>
<div id="opportunities-extension-modal" class="opportunities-extension-modal" data-theme="light"></div>
<script>
window.chrome = { runtime: { onMessage: { addListener() {} } } };
async function initialize() {
const source = await fetch("/src/content.js").then((response) => response.text());
const marker = /\r?\n if \(insertTile\(\)\) \{/;
const exposure = `
window.__trendHarness = {
ensureStyles: ensureExtensionStyles,
createAccountPlanDetails,
createConsumptionTrendSection,
renderConsumptionTrendChart
};
return;
`;
const testableSource = source
.replace(" if (!isAllowedPage()) {", " if (false) {")
.replace(marker, `${exposure}\n if (insertTile()) {`);
window.eval(testableSource);
const plan = {
subPlanNum: "11276098",
planType: "Universal Credits",
bookingCustomer: "Example Customer",
soldToPartyName: "Example Sold To",
contractId: "12345678",
hdrStatusCode: "ACTIVE",
startDate: "2026-06-01",
endDate: "2027-05-31"
};
const items = [
["20-Jun-26", 29000, 0], ["27-Jun-26", 30000, 0], ["04-Jul-26", 33000, 0],
["11-Jul-26", 28500, 0], ["18-Jul-26", 29500, 0], ["25-Jul-26", 23000, 10000],
["01-Aug-26", 0, 29000], ["08-Aug-26", 0, 32000], ["15-Aug-26", 0, 30000],
["22-Aug-26", 31000, 4500], ["29-Aug-26", 37000, 0], ["05-Sep-26", 35000, 0],
["12-Sep-26", 2000, 0]
].flatMap(([label, funded, overage]) => {
const [day, month, year] = label.split("-");
const monthNumber = { Jun: "06", Jul: "07", Aug: "08", Sep: "09" }[month];
const base = {
fyDate: `20${year}-${monthNumber}-${day}`,
fiscalTime: [label],
fiscalQtr: "FY27-Q1",
currencyCode: "USD",
arrCd: 0
};
const rows = [{ ...base, usageCategory: "Funded Allocation", usedAmount: funded }];
if (overage) rows.push({ ...base, usageCategory: "OVERAGE", usedAmount: overage });
return rows;
});
window.__trendHarness.ensureStyles();
const overlay = document.getElementById("opportunities-extension-modal");
overlay.dataset.theme = new URLSearchParams(window.location.search).get("theme") === "dark"
? "dark"
: "light";
const modal = document.createElement("section");
modal.className = "opportunities-extension-account-plans-modal";
const dialog = document.createElement("div");
dialog.className = "opportunities-extension-account-plans-dialog";
const header = document.createElement("header");
header.className = "opportunities-extension-account-plans-header";
header.innerHTML = "<div><h2>Cloud Consumption</h2><p>Example customer</p></div>";
const content = document.createElement("div");
content.className = "opportunities-extension-account-plans-content";
const tabs = document.createElement("div");
tabs.className = "opportunities-extension-account-plans-tabs";
tabs.innerHTML = '<button class="opportunities-extension-account-plan-tab" aria-selected="true"><span class="opportunities-extension-account-plan-status-indicator" data-active="true"></span>Universal Credits 11276098</button>';
const panel = document.createElement("section");
panel.className = "opportunities-extension-account-plan-panel";
const trend = window.__trendHarness.createConsumptionTrendSection(plan);
panel.append(window.__trendHarness.createAccountPlanDetails(plan), trend);
content.append(tabs, panel);
dialog.append(header, content);
modal.append(dialog);
overlay.append(modal);
window.__trendHarness.renderConsumptionTrendChart(
trend,
trend.querySelector(".opportunities-extension-consumption-chart"),
items,
"weekly",
plan.subPlanNum
);
}
initialize();
</script>
</body>
</html>