Reestruturação dos arquivos e pastas do código da extensão

This commit is contained in:
Paulo Porto
2026-05-13 23:40:29 -03:00
parent f196067b21
commit c989088ab1
33 changed files with 13154 additions and 11866 deletions

653
src/content/export-html.js Normal file
View File

@@ -0,0 +1,653 @@
(() => {
function escapeJsonForInlineScript(json) {
return String(json || "")
.replace(/</g, "\\u003c")
.replace(/>/g, "\\u003e")
.replace(/&/g, "\\u0026")
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029");
}
function createExportHtml(dataset) {
const json = escapeJsonForInlineScript(JSON.stringify(dataset.exportPayload));
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Arch Panel Dashboard</title>
<style>
:root {
--bg: #0f141b;
--panel: #171e27;
--panel-strong: #202a36;
--border: #33404f;
--text: #f4f7fb;
--text-muted: #a8b4c2;
--accent: #7dc6e7;
--accent-soft: rgba(125, 198, 231, 0.14);
--success: #61bf81;
--warning: #ef8d67;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: linear-gradient(180deg, #0d1218, #121923 45%, #10161e 100%);
color: var(--text);
font: 14px/1.45 "Segoe UI", Arial, sans-serif;
}
.shell {
max-width: 1480px;
margin: 0 auto;
padding: 28px;
}
.hero, .panel {
border: 1px solid var(--border);
background: var(--panel);
border-radius: 18px;
}
.hero {
display: flex;
justify-content: space-between;
gap: 24px;
padding: 24px;
margin-bottom: 18px;
}
.eyebrow {
margin: 0 0 8px;
color: var(--text-muted);
font-size: 11px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
}
h1, h2, h3, p { margin: 0; }
h1 { font-size: 34px; line-height: 1.1; }
.subtitle { margin-top: 12px; color: var(--text-muted); max-width: 720px; }
.pill {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 0 12px;
border-radius: 999px;
background: var(--accent-soft);
color: var(--text);
border: 1px solid rgba(255,255,255,.08);
font-weight: 600;
}
.grid {
display: grid;
gap: 16px;
}
.metrics { grid-template-columns: repeat(4, minmax(0,1fr)); margin-bottom: 18px; }
.metric, .boolean-card {
border: 1px solid var(--border);
background: var(--panel);
border-radius: 16px;
padding: 18px;
}
.metric-label { color: var(--text-muted); font-size: 12px; }
.metric-value { margin-top: 8px; font-size: 28px; font-weight: 700; }
.metric-detail { margin-top: 6px; color: var(--text-muted); font-size: 12px; }
.indicator-grid { grid-template-columns: repeat(4, minmax(0,1fr)); margin-bottom: 18px; }
.status-grid { grid-template-columns: repeat(2, minmax(0,1fr)); margin-bottom: 18px; }
.boolean-grid { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; margin-top: 14px; }
.progress { display: grid; gap: 8px; margin-bottom: 12px; }
.progress-track {
display: flex;
width: 100%;
height: 10px;
overflow: hidden;
border-radius: 999px;
background: rgba(255,255,255,.06);
border: 1px solid var(--border);
}
.progress-segment { height: 100%; }
.progress-segment.is-success { background: linear-gradient(90deg, #7fd29a, #3fa35c); }
.progress-segment.is-danger { background: linear-gradient(90deg, #f2a28d, #d94f2b); }
.progress-labels {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: var(--text-muted);
font-size: 11px;
font-weight: 600;
}
.boolean-card {
appearance: none;
color: var(--text);
text-align: left;
cursor: pointer;
}
.boolean-card strong { display: block; margin-top: 8px; font-size: 28px; }
.layout { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
.panel { padding: 18px; }
.panel-head { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 14px; }
.panel-title { margin-top: 6px; font-size: 18px; }
table { width: 100%; border-collapse: collapse; }
.main-table { table-layout: fixed; }
.main-table col.col-opportunity { width: 14%; }
.main-table col.col-customer { width: 16%; }
.main-table col.col-workload { width: auto; }
.main-table col.col-acr { width: 11%; }
.main-table col.col-type { width: 11%; }
.main-table col.col-flag { width: 9%; }
th, td { padding: 12px 14px; border-bottom: 1px solid var(--border); font-size: 12px; white-space: nowrap; }
th { color: var(--text-muted); text-align: left; background: var(--panel-strong); }
td { color: var(--text); }
td.is-numeric, th.is-numeric { text-align: right; }
.main-table th:nth-child(3),
.main-table td:nth-child(3) {
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.main-table td:nth-child(3) a {
display: block;
}
.main-table th:nth-child(2),
.main-table td:nth-child(2) {
white-space: normal;
overflow-wrap: anywhere;
}
tr:hover td { background: rgba(255,255,255,.02); }
a { color: var(--accent); text-decoration: none; }
.status {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
}
.status.true { color: #d9f9e4; background: rgba(97,191,129,.2); }
.status.false { color: #ffd8cc; background: rgba(239,141,103,.18); }
.row-stack { display: grid; gap: 12px; }
.row {
display: grid;
gap: 8px;
padding: 12px 0;
border-top: 1px solid var(--border);
}
.row:first-child { border-top: 0; padding-top: 0; }
.row-top {
display: flex;
justify-content: space-between;
gap: 12px;
}
.row-label { font-weight: 700; }
.row-sub { color: var(--text-muted); font-size: 12px; }
.bar { position: relative; height: 8px; border-radius: 999px; overflow: hidden; background: rgba(255,255,255,.08); }
.bar > span { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: linear-gradient(90deg, var(--accent), #4b8ce2); }
.sr-table td { white-space: normal; vertical-align: top; }
.sr-main { width: 78%; }
.sr-side { width: 22%; text-align: right; }
.sr-line { display: flex; flex-wrap: wrap; gap: 10px 14px; margin-top: 6px; }
.sr-line.head { margin-top: 0; }
.sr-customer { color: var(--text); font-size: 13px; font-weight: 700; }
.sr-moment { color: var(--text); font-size: 18px; font-weight: 700; }
.sr-exact, .sr-hours { margin-top: 6px; color: var(--text-muted); font-size: 12px; }
.empty {
padding: 18px;
border: 1px dashed var(--border);
border-radius: 8px;
color: var(--text-muted);
font-size: 13px;
}
.overlay {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(8, 12, 18, .78);
backdrop-filter: blur(10px);
}
.overlay.open { display: flex; }
.detail {
width: min(1480px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow: auto;
border: 1px solid var(--border);
background: var(--panel);
border-radius: 18px;
padding: 18px;
}
.detail-head {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
margin-bottom: 12px;
}
.close {
appearance: none;
min-height: 34px;
padding: 0 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel-strong);
color: var(--text);
cursor: pointer;
}
.detail-actions {
display: flex;
justify-content: flex-end;
position: sticky;
bottom: 0;
margin-top: 16px;
padding-top: 12px;
background: linear-gradient(180deg, rgba(23, 30, 39, 0), var(--panel) 35%);
}
.detail table { table-layout: fixed; }
.detail th, .detail td {
white-space: normal;
vertical-align: top;
overflow-wrap: anywhere;
}
.detail-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.sort-button {
appearance: none;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.sort-button.is-numeric {
width: 100%;
text-align: right;
}
.clickable {
appearance: none;
width: 100%;
color: inherit;
text-align: left;
cursor: pointer;
}
@media (max-width: 1100px) {
.metrics, .indicator-grid, .status-grid, .layout { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div id="app"></div>
<div id="detailOverlay" class="overlay"></div>
<script>
const payload = ${json};
const allowedTypes = ["SQL", "Pipeline", "Upside", "Forecast"];
const detailSortKeys = {
opportunityNumber: "opportunityNumber",
customerName: "customerName",
workload: "workload",
adjustedACR: "adjustedACR",
opportunityForecastTypeGroup: "opportunityForecastTypeGroup",
hasSR: "hasSR",
hasAction: "hasAction"
};
let detailState = null;
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function normalizeForecastType(value) {
const text = String(value || "").trim();
return text ? text.split(" -")[0].trim() || "Unknown" : "Unknown";
}
function formatWholeNumber(value) {
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(value);
}
function formatCurrency(value) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0
}).format(value || 0);
}
function formatPercent(value) {
return new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1
}).format(value || 0) + "%";
}
function formatRelativeMoment(value) {
if (!value) {
return "-";
}
const target = new Date(value);
if (Number.isNaN(target.getTime())) {
return String(value);
}
let remaining = Math.max(Date.now() - target.getTime(), 0);
const day = 24 * 60 * 60 * 1000;
const hour = 60 * 60 * 1000;
const minute = 60 * 1000;
const second = 1000;
const days = Math.floor(remaining / day);
remaining -= days * day;
const hours = Math.floor(remaining / hour);
remaining -= hours * hour;
const minutes = Math.floor(remaining / minute);
remaining -= minutes * minute;
const seconds = Math.floor(remaining / second);
return days + "d " + hours + "h " + minutes + "m " + seconds + "s";
}
function flattenWorkloads() {
return (payload.customers || []).flatMap((customer) =>
(customer.workloads || []).map((workload) => ({
...workload,
customerName: customer.name
}))
);
}
function buildSummary(workloads) {
const eligible = workloads
.map((workload) => ({
...workload,
opportunityForecastTypeGroup: normalizeForecastType(workload.opportunityForecastTypeGroup || workload.opportunityForecastType)
}))
.filter((workload) => allowedTypes.includes(workload.opportunityForecastTypeGroup))
.sort((a, b) => (b.adjustedACR || 0) - (a.adjustedACR || 0));
return {
eligible,
stageIndicators: allowedTypes.map((label) => {
const items = eligible.filter((workload) => workload.opportunityForecastTypeGroup === label);
return {
label,
count: items.length,
totalAcr: items.reduce((sum, item) => sum + (item.adjustedACR || 0), 0)
};
}),
sr: {
trueCount: eligible.filter((item) => item.hasSR).length,
falseCount: eligible.filter((item) => !item.hasSR).length
},
action: {
trueCount: eligible.filter((item) => item.hasAction).length,
falseCount: eligible.filter((item) => !item.hasAction).length
}
};
}
function opportunityLink(workload) {
if (!workload.opportunityId || !workload.opportunityNumber) {
return escapeHtml(workload.opportunityNumber || "Opportunity");
}
const href = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?id=" +
encodeURIComponent(workload.opportunityId) +
"&puid=" +
encodeURIComponent(workload.opportunityNumber);
return '<a href="' + href + '" target="_blank" rel="noreferrer">' + escapeHtml(workload.opportunityNumber) + '</a>';
}
function workloadLink(workload) {
const label = (workload.name || "Unnamed workload") + (workload.description ? " | " + workload.description : "");
if (!workload.workloadId) {
return escapeHtml(label);
}
const href = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/ui/index.html?ojr=workload_workbench/workload_details;workloadId=" +
encodeURIComponent(workload.workloadId);
return '<a href="' + href + '" target="_blank" rel="noreferrer">' + escapeHtml(label) + '</a>';
}
function statusToken(value) {
const label = value === "true" ? "Sim" : "Não";
return '<span class="status ' + value + '">' + label + '</span>';
}
function renderTable(items) {
return '<table class="main-table"><colgroup><col class="col-opportunity"><col class="col-customer"><col class="col-workload"><col class="col-acr"><col class="col-type"><col class="col-flag"><col class="col-flag"></colgroup><thead><tr><th>Opportunity</th><th>Customer</th><th>Workload</th><th class="is-numeric">ACR</th><th>Type</th><th>Tem SR?</th><th>Tem Action?</th></tr></thead><tbody>' +
items.map((workload) => '<tr><td>' + opportunityLink(workload) + '</td><td>' + escapeHtml(workload.customerName || '') + '</td><td>' + workloadLink(workload) + '</td><td class="is-numeric">' + formatCurrency(workload.adjustedACR) + '</td><td>' + escapeHtml(workload.opportunityForecastTypeGroup) + '</td><td>' + statusToken(String(Boolean(workload.hasSR))) + '</td><td>' + statusToken(String(Boolean(workload.hasAction))) + '</td></tr>').join('') +
'</tbody></table>';
}
function getSortableValue(workload, sortKey) {
if (sortKey === detailSortKeys.opportunityNumber) {
return String(workload.opportunityNumber || "");
}
if (sortKey === detailSortKeys.customerName) {
return String(workload.customerName || "");
}
if (sortKey === detailSortKeys.workload) {
return String((workload.name || "") + "|" + (workload.description || ""));
}
if (sortKey === detailSortKeys.adjustedACR) {
return Number(workload.adjustedACR || 0);
}
if (sortKey === detailSortKeys.opportunityForecastTypeGroup) {
return String(workload.opportunityForecastTypeGroup || "");
}
if (sortKey === detailSortKeys.hasSR) {
return Boolean(workload.hasSR);
}
if (sortKey === detailSortKeys.hasAction) {
return Boolean(workload.hasAction);
}
return String(workload.name || "");
}
function compareSortableValues(left, right) {
if (typeof left === "number" || typeof right === "number") {
return Number(left || 0) - Number(right || 0);
}
if (typeof left === "boolean" || typeof right === "boolean") {
return Number(Boolean(left)) - Number(Boolean(right));
}
return String(left || "").localeCompare(String(right || ""), undefined, {
numeric: true,
sensitivity: "base"
});
}
function sortItems(items, sortKey, sortDirection) {
const factor = sortDirection === "desc" ? -1 : 1;
return items.slice().sort((left, right) => {
const comparison = compareSortableValues(
getSortableValue(left, sortKey),
getSortableValue(right, sortKey)
);
if (comparison !== 0) {
return comparison * factor;
}
return compareSortableValues(
getSortableValue(left, detailSortKeys.workload),
getSortableValue(right, detailSortKeys.workload)
);
});
}
function renderSortButton(label, sortKey, isNumeric) {
const direction = detailState && detailState.sortKey === sortKey
? (detailState.sortDirection === "asc" ? " ↑" : " ↓")
: "";
return '<button class="sort-button' + (isNumeric ? ' is-numeric' : '') + '" data-sort-key="' + sortKey + '">' + label + direction + '</button>';
}
function renderDetailTable(items) {
return '<table><thead><tr><th>' + renderSortButton('Opportunity', detailSortKeys.opportunityNumber, false) + '</th><th>' + renderSortButton('Customer', detailSortKeys.customerName, false) + '</th><th>' + renderSortButton('Workload', detailSortKeys.workload, false) + '</th><th class="is-numeric">' + renderSortButton('ACR', detailSortKeys.adjustedACR, true) + '</th><th>' + renderSortButton('Type', detailSortKeys.opportunityForecastTypeGroup, false) + '</th><th>' + renderSortButton('HasSR', detailSortKeys.hasSR, false) + '</th><th>' + renderSortButton('HasAction', detailSortKeys.hasAction, false) + '</th></tr></thead><tbody>' +
items.map((workload) => '<tr><td>' + opportunityLink(workload) + '</td><td>' + escapeHtml(workload.customerName || '') + '</td><td>' + workloadLink(workload) + '</td><td class="is-numeric">' + formatCurrency(workload.adjustedACR) + '</td><td>' + escapeHtml(workload.opportunityForecastTypeGroup) + '</td><td>' + statusToken(String(Boolean(workload.hasSR))) + '</td><td>' + statusToken(String(Boolean(workload.hasAction))) + '</td></tr>').join('') +
'</tbody></table>';
}
function openDetail(title, label, items) {
const totalAcr = items.reduce((sum, item) => sum + (item.adjustedACR || 0), 0);
const overlay = document.getElementById("detailOverlay");
overlay.innerHTML = '<div class="detail"><div class="detail-head"><div><p class="eyebrow">' + label + '</p><h2>' + title + '</h2></div><button class="close" id="closeDetail">Close</button></div><p class="subtitle">' + formatWholeNumber(items.length) + ' workloads · Total (ACR): ' + formatCurrency(totalAcr) + '</p><div style="margin-top:16px">' + renderTable(items) + '</div></div>';
overlay.classList.add("open");
document.getElementById("closeDetail").addEventListener("click", () => overlay.classList.remove("open"));
overlay.onclick = (event) => {
if (event.target === overlay) {
overlay.classList.remove("open");
}
};
}
function renderSortButtonStateful(label, sortKey, isNumeric) {
const direction = detailState && detailState.sortKey === sortKey
? (detailState.sortDirection === "asc" ? " ^" : " v")
: "";
return '<button class="sort-button' + (isNumeric ? ' is-numeric' : '') + '" data-sort-key="' + sortKey + '">' + label + direction + '</button>';
}
function renderDetailTableStateful(items) {
return '<table><thead><tr><th>' + renderSortButtonStateful('Opportunity', detailSortKeys.opportunityNumber, false) + '</th><th>' + renderSortButtonStateful('Customer', detailSortKeys.customerName, false) + '</th><th>' + renderSortButtonStateful('Workload', detailSortKeys.workload, false) + '</th><th class="is-numeric">' + renderSortButtonStateful('ACR', detailSortKeys.adjustedACR, true) + '</th><th>' + renderSortButtonStateful('Type', detailSortKeys.opportunityForecastTypeGroup, false) + '</th><th>' + renderSortButtonStateful('Tem SR?', detailSortKeys.hasSR, false) + '</th><th>' + renderSortButtonStateful('Tem Action?', detailSortKeys.hasAction, false) + '</th></tr></thead><tbody>' +
items.map((workload) => '<tr><td>' + opportunityLink(workload) + '</td><td>' + escapeHtml(workload.customerName || '') + '</td><td>' + workloadLink(workload) + '</td><td class="is-numeric">' + formatCurrency(workload.adjustedACR) + '</td><td>' + escapeHtml(workload.opportunityForecastTypeGroup) + '</td><td>' + statusToken(String(Boolean(workload.hasSR))) + '</td><td>' + statusToken(String(Boolean(workload.hasAction))) + '</td></tr>').join('') +
'</tbody></table>';
}
function openDetailStateful(title, label, items) {
detailState = {
title: title,
label: label,
items: items.slice(),
sortKey: detailSortKeys.adjustedACR,
sortDirection: "desc"
};
renderDetailStateful();
}
function renderDetailStateful() {
if (!detailState) {
return;
}
const totalAcr = detailState.items.reduce((sum, item) => sum + (item.adjustedACR || 0), 0);
const sortedItems = sortItems(detailState.items, detailState.sortKey, detailState.sortDirection);
const overlay = document.getElementById("detailOverlay");
overlay.innerHTML = '<div class="detail"><div class="detail-head"><div><p class="eyebrow">' + detailState.label + '</p><h2>' + detailState.title + '</h2></div><button class="close" id="closeDetail">Fechar</button></div><p class="subtitle">' + formatWholeNumber(sortedItems.length) + ' workloads · Total (ACR): ' + formatCurrency(totalAcr) + '</p><div style="margin-top:16px">' + renderDetailTableStateful(sortedItems) + '</div><div class="detail-actions"><button class="close" id="closeDetailFooter">Fechar</button></div></div>';
overlay.classList.add("open");
const close = () => {
overlay.classList.remove("open");
detailState = null;
};
document.getElementById("closeDetail").addEventListener("click", close);
document.getElementById("closeDetailFooter").addEventListener("click", close);
overlay.querySelectorAll("[data-sort-key]").forEach((button) => {
button.addEventListener("click", () => {
const sortKey = button.getAttribute("data-sort-key");
const nextDirection = detailState.sortKey === sortKey && detailState.sortDirection === "asc"
? "desc"
: "asc";
detailState.sortKey = sortKey;
detailState.sortDirection = nextDirection;
renderDetailStateful();
});
});
overlay.onclick = (event) => {
if (event.target === overlay) {
close();
}
};
}
function render() {
const workloads = flattenWorkloads();
const summary = buildSummary(workloads);
const app = document.getElementById("app");
app.innerHTML = '<div class="shell">' +
'<section class="hero"><div><p class="eyebrow">Live Oracle dataset</p><h1>Arch panel export</h1><p class="subtitle">Generated from user, customer, workload, action and service request APIs.</p></div><div><span class="pill">' + formatCurrency(summary.eligible.reduce((sum, item) => sum + (item.adjustedACR || 0), 0)) + ' total eligible ACR</span></div></section>' +
'<section class="grid metrics">' +
'<article class="metric"><p class="metric-label">Total clients</p><p class="metric-value">' + formatWholeNumber(payload.customers.length) + '</p><p class="metric-detail">Loaded from listSummary</p></article>' +
'<article class="metric"><p class="metric-label">Total workloads</p><p class="metric-value">' + formatWholeNumber(workloads.length) + '</p><p class="metric-detail">Won workloads excluded upstream</p></article>' +
'<article class="metric"><p class="metric-label">Current user</p><p class="metric-value" style="font-size:18px">' + escapeHtml(payload.user.name || payload.user.userEmail) + '</p><p class="metric-detail">' + escapeHtml(payload.user.userEmail) + '</p></article>' +
'</section>' +
'<section class="grid indicator-grid">' +
summary.stageIndicators.map((indicator) => '<button class="metric clickable" data-kind="stage" data-value="' + indicator.label + '"><p class="metric-label">' + indicator.label + '</p><p class="metric-value">' + formatWholeNumber(indicator.count) + '</p><p class="metric-detail">Total (ACR): ' + formatCurrency(indicator.totalAcr) + '</p></button>').join('') +
'</section>' +
'<section class="grid status-grid">' +
'<article class="panel"><p class="eyebrow">Status de SRs</p><div class="progress"><div class="progress-track"><span class="progress-segment is-success" style="width:' + ((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.trueCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '%"></span><span class="progress-segment is-danger" style="width:' + ((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.falseCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '%"></span></div><div class="progress-labels"><span>Workloads com SR: ' + formatPercent((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.trueCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '</span><span>Workloads sem SR: ' + formatPercent((summary.sr.trueCount + summary.sr.falseCount) > 0 ? (summary.sr.falseCount / (summary.sr.trueCount + summary.sr.falseCount)) * 100 : 0) + '</span></div></div><div class="boolean-grid"><button class="boolean-card" data-kind="sr" data-value="true"><span>Workloads com SR</span><strong>' + formatWholeNumber(summary.sr.trueCount) + '</strong></button><button class="boolean-card" data-kind="sr" data-value="false"><span>Workloads sem SR</span><strong>' + formatWholeNumber(summary.sr.falseCount) + '</strong></button></div></article>' +
'<article class="panel"><p class="eyebrow">Status de Consumption Plan/action</p><div class="progress"><div class="progress-track"><span class="progress-segment is-success" style="width:' + ((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.trueCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '%"></span><span class="progress-segment is-danger" style="width:' + ((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.falseCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '%"></span></div><div class="progress-labels"><span>Workloads com action: ' + formatPercent((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.trueCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '</span><span>Workloads sem action: ' + formatPercent((summary.action.trueCount + summary.action.falseCount) > 0 ? (summary.action.falseCount / (summary.action.trueCount + summary.action.falseCount)) * 100 : 0) + '</span></div></div><div class="boolean-grid"><button class="boolean-card" data-kind="action" data-value="true"><span>Workloads com action</span><strong>' + formatWholeNumber(summary.action.trueCount) + '</strong></button><button class="boolean-card" data-kind="action" data-value="false"><span>Workloads sem action</span><strong>' + formatWholeNumber(summary.action.falseCount) + '</strong></button></div></article>' +
'</section>' +
'</div>';
app.querySelectorAll("[data-kind]").forEach((element) => {
element.addEventListener("click", () => {
const kind = element.getAttribute("data-kind");
const value = element.getAttribute("data-value");
let items = [];
let title = "";
let label = "";
if (kind === "stage") {
items = summary.eligible.filter((item) => item.opportunityForecastTypeGroup === value);
label = "Opportunity type: " + value;
title = value + " workloads";
} else if (kind === "sr") {
const flag = value === "true";
items = summary.eligible.filter((item) => Boolean(item.hasSR) === flag);
label = "Status de SRs";
title = flag ? "Workloads com SR" : "Workloads sem SR";
} else if (kind === "action") {
const flag = value === "true";
items = summary.eligible.filter((item) => Boolean(item.hasAction) === flag);
label = "Status de Consumption Plan/action";
title = flag ? "Workloads com action" : "Workloads sem action";
}
openDetailStateful(title, label, items);
});
});
}
render();
</script>
</body>
</html>`;
}
globalThis.ArchPanelExportHtml = Object.freeze({
createExportHtml,
});
})();

118
src/content/hcm-page.js Normal file
View File

@@ -0,0 +1,118 @@
(() => {
function create({
HCM_MY_INFORMATION_APPS_GROUP_ID,
WORKLIST_ORIGIN,
WORKLIST_SAASUI_PATHNAME,
IDS,
cleanString,
hcmTileTemplate,
onOpenArchPanel,
writeCachedWorklistToken,
}) {
let lastPersistedWorklistIframeUrl = "";
function syncHcmTile() {
const appsGroup = document.getElementById(HCM_MY_INFORMATION_APPS_GROUP_ID);
const addTile = appsGroup?.querySelector(
".flat-grid-cell.flat-grid-cell-addicon"
);
if (!addTile?.parentElement) {
document.getElementById(IDS.hcmTile)?.remove();
return;
}
const tile = document.getElementById(IDS.hcmTile) || createHcmTile();
if (
tile.parentElement !== addTile.parentElement ||
tile.nextElementSibling !== addTile
) {
addTile.parentElement.insertBefore(tile, addTile);
}
}
function syncWorklistIframeToken(options = {}) {
for (const frame of document.querySelectorAll("iframe")) {
const tokenPayload = extractWorklistIframeToken(frame);
if (tokenPayload?.token) {
persistWorklistIframeToken(tokenPayload, options);
return tokenPayload;
}
}
return null;
}
function extractWorklistIframeToken(frame) {
const rawSrc = cleanString(frame?.getAttribute?.("src") || frame?.src);
if (!rawSrc) {
return null;
}
try {
const url = new URL(rawSrc, window.location.href);
const token = cleanString(url.searchParams.get("token"));
if (
url.origin !== WORKLIST_ORIGIN ||
url.pathname !== WORKLIST_SAASUI_PATHNAME ||
!token
) {
return null;
}
return {
token,
url: url.toString(),
};
} catch {
return null;
}
}
function persistWorklistIframeToken(tokenPayload, options = {}) {
if (!options.force && tokenPayload.url === lastPersistedWorklistIframeUrl) {
return;
}
lastPersistedWorklistIframeUrl = tokenPayload.url;
void writeCachedWorklistToken(tokenPayload);
}
function createHcmTile() {
const template = document.createElement("template");
template.innerHTML = hcmTileTemplate(IDS.hcmTile);
const tile = template.content.firstElementChild;
tile.addEventListener("click", openHcmArchPanelTile);
tile.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
openHcmArchPanelTile(event);
}
});
tile.tabIndex = 0;
tile.setAttribute("role", "button");
tile.setAttribute("aria-label", "Open Arch Panel");
return tile;
}
function openHcmArchPanelTile(event) {
event.preventDefault();
event.stopPropagation();
onOpenArchPanel();
}
return Object.freeze({
syncHcmTile,
syncWorklistIframeToken,
});
}
globalThis.ArchPanelHcmPage = Object.freeze({
create,
});
})();

View File

@@ -0,0 +1,944 @@
(() => {
function create(dependencies) {
const {
appState,
COMCIP_REQUEST,
comcipRepository,
readCachedTaskTypePayload,
writeCachedTaskTypePayload,
getStoredTimeManagementSelectedDate,
setStoredTimeManagementSelectedDate,
renderModal,
cleanString,
normalizeString,
getDateInputValue,
parseDatePreservingDateOnly,
getFieldValue,
extractArray,
getErrorMessage,
cssEscape,
getTimeManagementRows,
isTimeManagementSaveEnabled,
getTimeManagementPendingChanges,
createTimeManagementSavePayload,
isNonServiceRequestSrNumber,
normalizeInfoWeekResponse,
normalizeTimeManagementServiceRequests,
mergeTimeManagementServiceRequests,
normalizeTimeManagementEntryRows,
normalizeTimeManagementEntryRowSelection,
getActivityRequestSrNumbers,
normalizeActivityRequest,
normalizeTaskTypeRequest,
getTimeManagementTaskTypeOptions,
getTimeManagementActivityOptions,
findTimeManagementServiceRequest,
findOptionValue,
} = dependencies;
let requestCounter = 0;
function toggleTimeManagementDatePicker() {
const selectedDate = getTimeManagementSelectedDate();
const pickerDate = parseDatePreservingDateOnly(selectedDate);
const fallback = Number.isNaN(pickerDate.getTime()) ? new Date() : pickerDate;
appState.timeManagementDatePicker = {
isOpen: !appState.timeManagementDatePicker.isOpen,
year: fallback.getFullYear(),
month: fallback.getMonth(),
};
renderModal();
}
function shiftTimeManagementDatePickerMonth(delta) {
const picker = appState.timeManagementDatePicker;
const nextDate = new Date(picker.year, picker.month + delta, 1);
appState.timeManagementDatePicker = {
isOpen: true,
year: nextDate.getFullYear(),
month: nextDate.getMonth(),
};
renderModal();
}
async function selectTimeManagementDate(selectedDate) {
appState.timeManagementDatePicker = {
...appState.timeManagementDatePicker,
isOpen: false,
};
await loadTimeManagementData(selectedDate);
}
async function shiftTimeManagementDateByWeeks(delta) {
const selectedDate = getTimeManagementSelectedDate();
const parsedDate = parseDatePreservingDateOnly(selectedDate);
const baseDate = Number.isNaN(parsedDate.getTime())
? new Date()
: parsedDate;
baseDate.setDate(baseDate.getDate() + (Number(delta) || 0) * 7);
appState.timeManagementDatePicker = {
...appState.timeManagementDatePicker,
isOpen: false,
};
await loadTimeManagementData(getDateInputValue(baseDate));
}
function getTimeManagementSelectedDate() {
return appState.timeManagementModal?.selectedDate || getDateInputValue(new Date());
}
async function openTimeManagementModal() {
const selectedDate = getStoredTimeManagementSelectedDate();
appState.timeManagementModal = createTimeManagementLoadingState(selectedDate);
renderModal();
await loadTimeManagementData(selectedDate);
}
async function loadTimeManagementData(selectedDate) {
const normalizedDate = cleanString(selectedDate) || getDateInputValue(new Date());
const requestId = `${Date.now()}-${++requestCounter}`;
setStoredTimeManagementSelectedDate(normalizedDate);
appState.timeManagementDatePicker = {
...appState.timeManagementDatePicker,
isOpen: false,
};
appState.timeManagementModal = {
...(appState.timeManagementModal || {}),
requestId,
selectedDate: normalizedDate,
status: "loading",
errorMessage: "",
};
renderModal();
try {
const resourcePartyId = getCurrentUserResourcePartyId();
const taskTypePromise = fetchTaskTypeRequest();
if (!resourcePartyId) {
throw new Error("resourceCurrentUser.resourcePartyId nao esta disponivel.");
}
const weekInfoPayload = await comcipRepository.fetchInfoWeekRequest(
normalizedDate
);
const weekDays = normalizeInfoWeekResponse(weekInfoPayload);
const weekId = cleanString(weekDays[0]?.weekId);
if (!weekId) {
throw new Error("WeekId nao encontrado no retorno de infoWeekRequest.");
}
const timeEntryWeekUrl = comcipRepository.buildTimeEntryWeekUrl(
resourcePartyId,
weekId
);
const [srPayload, taskTypePayload, timeEntryWeekPayload] = await Promise.all([
comcipRepository.fetchActiveServiceRequests(resourcePartyId, weekId),
taskTypePromise,
comcipRepository.fetchTimeEntryWeek(timeEntryWeekUrl),
]);
const timeEntryRows = normalizeTimeManagementEntryRows(
timeEntryWeekPayload,
weekDays
);
const serviceRequests = mergeTimeManagementServiceRequests(
normalizeTimeManagementServiceRequests(srPayload),
timeEntryRows
);
const activitySrNumbers = getActivityRequestSrNumbers(
srPayload,
serviceRequests
);
const activityRequest = await fetchCachedActivityRequest(activitySrNumbers);
const activityRequestUrl = activityRequest.url;
const activityPayload = activityRequest.payload;
const activityOptionsBySr = normalizeActivityRequest(
activityPayload,
serviceRequests
);
const taskTypes = normalizeTaskTypeRequest(taskTypePayload);
const rows = timeEntryRows;
const selectedSrNumber = rows[0]?.selectedSrNumber || "";
const normalizedRows = rows.map((row, index) =>
normalizeTimeManagementEntryRowSelection(
row,
index,
activityOptionsBySr,
taskTypes
)
);
if (appState.timeManagementModal?.requestId !== requestId) {
return;
}
appState.timeManagementModal = {
requestId,
selectedDate: normalizedDate,
status: "ready",
errorMessage: "",
weekId,
weekDays,
serviceRequests,
selectedSrNumber,
activityOptionsBySr,
activityRequestUrl,
activityRequestPayload: activityPayload,
timeEntryRows: normalizedRows,
selectedActivityValue: normalizedRows[0]?.selectedActivityValue || "",
taskTypes,
selectedTaskTypeValue: normalizedRows[0]?.selectedTaskTypeValue || "",
highlightRowId: "",
deletedTimeEntryIds: [],
isSaving: false,
saveErrorMessage: "",
};
renderModal();
} catch (error) {
if (appState.timeManagementModal?.requestId !== requestId) {
return;
}
appState.timeManagementModal = {
...(appState.timeManagementModal || {}),
requestId,
selectedDate: normalizedDate,
status: "error",
errorMessage: getErrorMessage(error),
};
renderModal();
}
}
function createTimeManagementLoadingState(selectedDate) {
return {
requestId: "",
selectedDate,
status: "loading",
errorMessage: "",
weekId: "",
weekDays: [],
serviceRequests: [],
selectedSrNumber: "",
activityOptionsBySr: {},
activityRequestUrl: "",
activityRequestPayload: null,
selectedActivityValue: "",
taskTypes: [],
selectedTaskTypeValue: "",
timeEntryRows: [],
highlightRowId: "",
deletedTimeEntryIds: [],
isSaving: false,
saveErrorMessage: "",
};
}
function createBlankTimeManagementEntryRow(srNumber = "", source = "blank") {
return {
id: `row-${Date.now()}-${Math.random().toString(16).slice(2)}`,
timeEntryId: "",
selectedSrNumber: cleanString(srNumber),
selectedActivityValue: "",
selectedTaskTypeValue: "",
dayHours: {},
source,
originalSignature: "",
};
}
function addTimeManagementEntryRow(type) {
if (!appState.timeManagementModal) {
return;
}
const existingDuplicateRow = findExistingTimeManagementDuplicateRow(
appState.timeManagementModal,
type
);
if (existingDuplicateRow) {
highlightTimeManagementDuplicateRow(existingDuplicateRow.id);
return;
}
const srNumber = type === "non-service" ? "non-service-sr" : "";
const nextRow = createBlankTimeManagementEntryRow(srNumber, "manual");
const nextRows = [
...getTimeManagementRows(appState.timeManagementModal),
nextRow,
];
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
highlightRowId: "",
saveErrorMessage: "",
};
appState.pendingFocusSelector = getTimeManagementComboboxFocusSelector(
nextRow.id,
type === "non-service" ? "taskType" : "sr"
);
renderModal();
}
function removeTimeManagementEntryRow(rowId) {
if (!appState.timeManagementModal) {
return;
}
const normalizedRowId = cleanString(rowId);
const rows = getTimeManagementRows(appState.timeManagementModal);
const removedRow = rows.find(
(row) => cleanString(row?.id) === normalizedRowId
);
const removedTimeEntryId = cleanString(removedRow?.timeEntryId);
const nextRows = rows.filter(
(row) => cleanString(row?.id) !== normalizedRowId
);
const deletedTimeEntryIds = removedTimeEntryId
? Array.from(
new Set([
...(appState.timeManagementModal.deletedTimeEntryIds || []),
removedTimeEntryId,
])
)
: appState.timeManagementModal.deletedTimeEntryIds || [];
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
selectedSrNumber: nextRows[0]?.selectedSrNumber || "",
selectedActivityValue: nextRows[0]?.selectedActivityValue || "",
selectedTaskTypeValue: nextRows[0]?.selectedTaskTypeValue || "",
highlightRowId: "",
deletedTimeEntryIds,
saveErrorMessage: "",
};
appState.pendingFocusSelector = getTimeManagementPostDeleteFocusSelector(
nextRows,
rows,
normalizedRowId
);
renderModal();
}
function getTimeManagementComboboxFocusSelector(rowId, field) {
return `.arch-panel-extension-combobox input[data-row-id="${cssEscape(rowId)}"][data-combobox-field="${cssEscape(field)}"]`;
}
function getTimeManagementPostDeleteFocusSelector(
nextRows,
previousRows,
removedRowId
) {
if (!nextRows.length) {
return "";
}
const removedIndex = previousRows.findIndex(
(row) => cleanString(row?.id) === cleanString(removedRowId)
);
const focusIndex = Math.min(Math.max(removedIndex, 0), nextRows.length - 1);
const focusRow = nextRows[focusIndex];
return focusRow?.id
? `.arch-panel-extension-time-management-delete[data-row-id="${cssEscape(focusRow.id)}"]`
: "";
}
function getTimeManagementRowById(modal, rowId) {
const rows = getTimeManagementRows(modal);
const normalizedRowId = cleanString(rowId);
return (
rows.find((row) => cleanString(row?.id) === normalizedRowId) ||
rows[0] ||
null
);
}
function updateTimeManagementRows(modal, rowId, nextRow) {
const rows = getTimeManagementRows(modal);
const normalizedRowId = cleanString(rowId || nextRow?.id);
return rows.map((row, index) => {
const isTarget = normalizedRowId
? cleanString(row?.id) === normalizedRowId
: index === 0;
return isTarget
? {
...row,
...nextRow,
id: row?.id || nextRow?.id || `row-${index + 1}`,
}
: row;
});
}
function updateTimeManagementSelectedSr(srNumber, rowId = "") {
if (!appState.timeManagementModal) {
return;
}
const targetRow = getTimeManagementRowById(appState.timeManagementModal, rowId);
const selectedServiceRequest = findTimeManagementServiceRequest(
appState.timeManagementModal,
srNumber
);
const selectedSrNumber =
selectedServiceRequest?.srNumber || cleanString(srNumber);
const duplicateRow = findDuplicateTimeManagementSrRow(
appState.timeManagementModal,
selectedSrNumber,
rowId
);
if (duplicateRow) {
highlightTimeManagementDuplicateRow(duplicateRow.id);
return;
}
const nextRow = {
...(targetRow || createBlankTimeManagementEntryRow()),
selectedSrNumber,
selectedActivityValue: "",
selectedTaskTypeValue: "",
};
const nextRows = updateTimeManagementRows(
appState.timeManagementModal,
rowId,
nextRow
);
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
selectedSrNumber: nextRows[0]?.selectedSrNumber || selectedSrNumber,
selectedActivityValue: nextRows[0]?.selectedActivityValue || "",
selectedTaskTypeValue: nextRows[0]?.selectedTaskTypeValue || "",
highlightRowId: "",
saveErrorMessage: "",
};
renderModal();
}
function updateTimeManagementField(fieldName, inputValue, rowId = "") {
if (!appState.timeManagementModal) {
return;
}
const targetRow = getTimeManagementRowById(appState.timeManagementModal, rowId);
const normalizedInput = cleanString(inputValue);
let value = normalizedInput;
if (fieldName === "selectedActivityValue") {
const options = getTimeManagementActivityOptions(
appState.timeManagementModal,
targetRow?.selectedSrNumber || appState.timeManagementModal.selectedSrNumber
);
value = findOptionValue(options, normalizedInput) || normalizedInput;
}
if (fieldName === "selectedTaskTypeValue") {
const options = getTimeManagementTaskTypeOptions(
appState.timeManagementModal,
targetRow?.selectedSrNumber || appState.timeManagementModal.selectedSrNumber
);
value = findOptionValue(options, normalizedInput) || normalizedInput;
const duplicateRow = findDuplicateTimeManagementNonServiceTaskTypeRow(
appState.timeManagementModal,
value,
rowId
);
if (duplicateRow) {
highlightTimeManagementDuplicateRow(duplicateRow.id);
return;
}
}
const nextRows = updateTimeManagementRows(appState.timeManagementModal, rowId, {
...(targetRow || createBlankTimeManagementEntryRow()),
[fieldName]: value,
});
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: nextRows,
[fieldName]: value,
highlightRowId: "",
saveErrorMessage: "",
};
}
function findDuplicateTimeManagementSrRow(modal, srNumber, currentRowId = "") {
const normalizedSrNumber = normalizeString(srNumber);
const normalizedCurrentRowId = cleanString(currentRowId);
if (!normalizedSrNumber || isNonServiceRequestSrNumber(srNumber)) {
return null;
}
return (
getTimeManagementRows(modal).find((row) => {
return (
cleanString(row?.id) !== normalizedCurrentRowId &&
normalizeString(row?.selectedSrNumber) === normalizedSrNumber
);
}) || null
);
}
function findExistingTimeManagementDuplicateRow(modal, type) {
const seen = new Map();
for (const row of getTimeManagementRows(modal)) {
const rowId = cleanString(row?.id);
let key = "";
if (type === "non-service") {
if (!isNonServiceRequestSrNumber(row?.selectedSrNumber)) {
continue;
}
key = normalizeString(row?.selectedTaskTypeValue);
} else {
if (isNonServiceRequestSrNumber(row?.selectedSrNumber)) {
continue;
}
key = normalizeString(row?.selectedSrNumber);
}
if (!key) {
continue;
}
if (seen.has(key)) {
return (
getTimeManagementRows(modal).find(
(candidate) => cleanString(candidate?.id) === seen.get(key)
) || row
);
}
seen.set(key, rowId);
}
return null;
}
function findDuplicateTimeManagementNonServiceTaskTypeRow(
modal,
taskTypeValue,
currentRowId = ""
) {
const normalizedTaskTypeValue = normalizeString(taskTypeValue);
const normalizedCurrentRowId = cleanString(currentRowId);
if (!normalizedTaskTypeValue) {
return null;
}
const currentRow = getTimeManagementRowById(modal, currentRowId);
if (!isNonServiceRequestSrNumber(currentRow?.selectedSrNumber)) {
return null;
}
return (
getTimeManagementRows(modal).find((row) => {
return (
cleanString(row?.id) !== normalizedCurrentRowId &&
isNonServiceRequestSrNumber(row?.selectedSrNumber) &&
normalizeString(row?.selectedTaskTypeValue) === normalizedTaskTypeValue
);
}) || null
);
}
function highlightTimeManagementDuplicateRow(rowId) {
const normalizedRowId = cleanString(rowId);
if (!appState.timeManagementModal || !normalizedRowId) {
return;
}
appState.pendingTimeManagementHighlightRowId = normalizedRowId;
appState.timeManagementModal = {
...appState.timeManagementModal,
highlightRowId: normalizedRowId,
};
renderModal();
}
function updateTimeManagementDayHours(rowId, dateKey, inputValue) {
if (!appState.timeManagementModal) {
return;
}
const targetRow = getTimeManagementRowById(appState.timeManagementModal, rowId);
const nextRow = {
...(targetRow || createBlankTimeManagementEntryRow()),
dayHours: {
...(targetRow?.dayHours || {}),
[cleanString(dateKey)]: cleanString(inputValue),
},
};
appState.timeManagementModal = {
...appState.timeManagementModal,
timeEntryRows: updateTimeManagementRows(
appState.timeManagementModal,
rowId,
nextRow
),
highlightRowId: "",
saveErrorMessage: "",
};
renderModal();
}
async function saveTimeManagementChanges() {
const modal = appState.timeManagementModal;
if (!modal || modal.isSaving || !isTimeManagementSaveEnabled(modal)) {
return;
}
appState.timeManagementModal = {
...modal,
isSaving: true,
saveErrorMessage: "",
};
renderModal();
try {
const resourceId = getCurrentUserResourcePartyId();
const weekId = cleanString(modal.weekId);
if (!resourceId) {
throw new Error("resourceCurrentUser.resourcePartyId nao esta disponivel.");
}
if (!weekId) {
throw new Error("WeekId nao encontrado para gravacao do apontamento.");
}
const changes = getTimeManagementPendingChanges(modal);
const baseUrl = COMCIP_REQUEST.timeEntriesUrl;
const updatedPayload = changes.updatedRows.map((row) =>
createTimeManagementSavePayload(row, modal.weekDays, weekId, resourceId, {
includeId: true,
})
);
const newPayload = changes.newRows.map((row) =>
createTimeManagementSavePayload(row, modal.weekDays, weekId, resourceId)
);
for (const timeEntryId of changes.deletedIds) {
await comcipRepository.mutateComcipPayload(
"DELETE",
`${baseUrl}/${encodeURIComponent(timeEntryId)}`,
null,
"timeEntryDelete"
);
}
if (updatedPayload.length) {
await comcipRepository.mutateComcipPayload(
"PUT",
baseUrl,
updatedPayload,
"timeEntryUpdate"
);
}
if (newPayload.length) {
await comcipRepository.mutateComcipPayload(
"POST",
baseUrl,
newPayload,
"timeEntryCreate"
);
}
await loadTimeManagementData(
modal.selectedDate || getDateInputValue(new Date())
);
} catch (error) {
appState.timeManagementModal = {
...(appState.timeManagementModal || modal),
isSaving: false,
saveErrorMessage: getErrorMessage(error),
};
renderModal();
}
}
function selectTimeManagementComboboxOption(field, value, rowId = "") {
if (field === "sr") {
updateTimeManagementSelectedSr(value || "", rowId);
return;
}
if (field === "activity") {
updateTimeManagementField("selectedActivityValue", value || "", rowId);
renderModal();
return;
}
if (field === "taskType") {
updateTimeManagementField("selectedTaskTypeValue", value || "", rowId);
renderModal();
}
}
function filterTimeManagementCombobox(input) {
const query = normalizeString(input.value);
const combobox = input.closest(".arch-panel-extension-combobox");
if (!combobox) {
return;
}
const options = Array.from(
combobox.querySelectorAll(".arch-panel-extension-combobox-option")
);
for (const option of options) {
const searchText = normalizeString(option.getAttribute("data-search") || "");
option.hidden = Boolean(query) && !searchText.includes(query);
}
setActiveTimeManagementComboboxOption(
combobox,
getVisibleTimeManagementComboboxOptions(combobox).find((option) =>
option.classList.contains("is-selected")
) || getVisibleTimeManagementComboboxOptions(combobox)[0]
);
}
function handleTimeManagementComboboxKeydown(event, input) {
const combobox = input.closest(".arch-panel-extension-combobox");
if (!combobox) {
return;
}
if (event.key === "Escape") {
clearActiveTimeManagementComboboxOption(combobox);
input.blur();
return;
}
const visibleOptions = getVisibleTimeManagementComboboxOptions(combobox);
if (!visibleOptions.length) {
return;
}
event.preventDefault();
if (event.key === "Enter") {
const activeOption =
visibleOptions.find((option) => option.classList.contains("is-active")) ||
visibleOptions[0];
activeOption.click();
return;
}
const activeIndex = visibleOptions.findIndex((option) =>
option.classList.contains("is-active")
);
const selectedIndex = visibleOptions.findIndex((option) =>
option.classList.contains("is-selected")
);
const baseIndex = activeIndex >= 0 ? activeIndex : selectedIndex;
let nextIndex = 0;
if (event.key === "ArrowDown") {
nextIndex = baseIndex >= 0 ? baseIndex + 1 : 0;
} else if (event.key === "ArrowUp") {
nextIndex = baseIndex >= 0 ? baseIndex - 1 : visibleOptions.length - 1;
} else if (event.key === "Home") {
nextIndex = 0;
} else if (event.key === "End") {
nextIndex = visibleOptions.length - 1;
}
if (nextIndex < 0) {
nextIndex = visibleOptions.length - 1;
} else if (nextIndex >= visibleOptions.length) {
nextIndex = 0;
}
setActiveTimeManagementComboboxOption(combobox, visibleOptions[nextIndex]);
}
function getVisibleTimeManagementComboboxOptions(combobox) {
return Array.from(
combobox.querySelectorAll(".arch-panel-extension-combobox-option")
).filter((option) => !option.hidden);
}
function setActiveTimeManagementComboboxOption(combobox, option) {
clearActiveTimeManagementComboboxOption(combobox);
if (!option) {
return;
}
option.classList.add("is-active");
combobox
.querySelector("input[data-combobox-field]")
?.setAttribute("aria-activedescendant", option.id || "");
option.scrollIntoView({ block: "nearest" });
}
function clearActiveTimeManagementComboboxOption(combobox) {
combobox
.querySelectorAll(".arch-panel-extension-combobox-option.is-active")
.forEach((option) => option.classList.remove("is-active"));
combobox
.querySelector("input[data-combobox-field]")
?.removeAttribute("aria-activedescendant");
}
async function fetchCachedActivityRequest(srNumbers) {
const cacheKey = getActivityRequestCacheKey(srNumbers);
if (!cacheKey) {
return {
url: "",
payload: null,
};
}
const cachedRequest = appState.timeManagementActivityCache.get(cacheKey);
if (cachedRequest?.payload) {
return cachedRequest;
}
if (cachedRequest?.promise) {
return cachedRequest.promise;
}
const requestPromise = comcipRepository
.fetchActivityRequest(srNumbers)
.then((result) => {
appState.timeManagementActivityCache.set(cacheKey, {
url: result.url,
payload: result.payload,
});
return result;
})
.catch((error) => {
appState.timeManagementActivityCache.delete(cacheKey);
throw error;
});
appState.timeManagementActivityCache.set(cacheKey, {
promise: requestPromise,
});
return requestPromise;
}
function getActivityRequestCacheKey(srNumbers) {
const normalizedSrNumbers = (srNumbers || [])
.map((srNumber) => cleanString(srNumber).toUpperCase())
.filter((srNumber) => /^SR\d+$/i.test(srNumber))
.sort();
return normalizedSrNumbers.join("|");
}
async function fetchTaskTypeRequest() {
if (appState.taskTypeCache.promise) {
return appState.taskTypeCache.promise;
}
appState.taskTypeCache.promise = (async () => {
return readCachedTaskTypePayload().catch(() => null);
})().finally(() => {
appState.taskTypeCache.promise = null;
});
return appState.taskTypeCache.promise;
}
async function refreshTaskTypeCache() {
const payload = await comcipRepository.fetchTaskTypePayload();
await writeCachedTaskTypePayload(payload);
return payload;
}
function getCurrentUserResourcePartyId() {
const resourceCurrentUser = appState.dataset?.resourceCurrentUser;
const directResourcePartyId = cleanString(
getFieldValue(resourceCurrentUser, [
"resourcePartyId",
"ResourcePartyId",
"ResourcePartyID",
"partyId",
"PartyId",
])
);
if (directResourcePartyId) {
return directResourcePartyId;
}
const candidates = Array.isArray(resourceCurrentUser)
? resourceCurrentUser
: extractArray(resourceCurrentUser, ["items", "data", "results", "content"]);
const source = candidates[0] || resourceCurrentUser;
return cleanString(
getFieldValue(source, [
"resourcePartyId",
"ResourcePartyId",
"ResourcePartyID",
"partyId",
"PartyId",
])
);
}
return Object.freeze({
toggleTimeManagementDatePicker,
shiftTimeManagementDatePickerMonth,
selectTimeManagementDate,
shiftTimeManagementDateByWeeks,
openTimeManagementModal,
loadTimeManagementData,
addTimeManagementEntryRow,
removeTimeManagementEntryRow,
updateTimeManagementSelectedSr,
updateTimeManagementField,
updateTimeManagementDayHours,
saveTimeManagementChanges,
selectTimeManagementComboboxOption,
filterTimeManagementCombobox,
handleTimeManagementComboboxKeydown,
refreshTaskTypeCache,
});
}
globalThis.ArchPanelManageTimeController = Object.freeze({
create,
});
})();

533
src/content/modal-events.js Normal file
View File

@@ -0,0 +1,533 @@
(() => {
function create(dependencies) {
const {
appState,
renderModal,
closeModal,
closeDetailModal,
toggleTheme,
toggleSidebar,
refreshData,
shiftCalendarPeriod,
goToCurrentCalendarPeriod,
setCalendarView,
downloadFile,
createExportHtml,
openDetailModal,
openActionFormModal,
openRampComparisonModal,
openForecastUpdateConfirmModal,
submitForecastUpdate,
openTimeEntriesDrawer,
openTimeManagementModal,
addTimeManagementEntryRow,
saveTimeManagementChanges,
removeTimeManagementEntryRow,
toggleTimeManagementDatePicker,
shiftTimeManagementDateByWeeks,
shiftTimeManagementDatePickerMonth,
selectTimeManagementDate,
selectTimeManagementComboboxOption,
submitActionForm,
toggleDetailSort,
loadTimeManagementData,
updateTimeManagementSelectedSr,
updateTimeManagementField,
updateTimeManagementDayHours,
filterTimeManagementCombobox,
handleTimeManagementComboboxKeydown,
} = dependencies;
function handleOverlayClick(event) {
const overlay = document.getElementById(
globalThis.ArchPanelConfig.IDS.overlay
);
if (!overlay) {
return;
}
if (event.target === overlay) {
if (appState.actionFormModal) {
appState.actionFormModal = null;
renderModal();
} else if (appState.timeManagementModal) {
appState.timeManagementModal = null;
renderModal();
} else if (appState.forecastUpdateConfirmModal) {
appState.forecastUpdateConfirmModal = null;
renderModal();
} else if (appState.rampComparisonModal) {
appState.rampComparisonModal = null;
renderModal();
} else if (appState.detailModal) {
closeDetailModal();
renderModal();
} else {
closeModal();
}
return;
}
const actionElement = event.target.closest("[data-action]");
if (!actionElement) {
return;
}
const action = actionElement.getAttribute("data-action");
if (
action === "time-management-date-change" ||
action === "time-management-sr-change" ||
action === "time-management-activity-change" ||
action === "time-management-task-type-change"
) {
return;
}
event.preventDefault();
if (action === "close") {
closeModal();
return;
}
if (action === "toggle-theme") {
toggleTheme();
return;
}
if (action === "toggle-sidebar") {
toggleSidebar();
return;
}
if (action === "refresh") {
void refreshData();
return;
}
if (action === "calendar-prev") {
shiftCalendarPeriod(-1);
return;
}
if (action === "calendar-next") {
shiftCalendarPeriod(1);
return;
}
if (action === "calendar-today") {
goToCurrentCalendarPeriod();
return;
}
if (action === "calendar-view") {
const calendarView = actionElement.getAttribute("data-calendar-view");
if (calendarView) {
setCalendarView(calendarView);
}
return;
}
if (action === "download-json") {
if (appState.dataset) {
downloadFile(
"arch-panel-data.json",
JSON.stringify(appState.dataset.exportPayload, null, 2),
"application/json"
);
}
return;
}
if (action === "download-html") {
if (appState.dataset) {
downloadFile(
"arch-panel-dashboard.html",
createExportHtml(appState.dataset),
"text/html"
);
}
return;
}
if (action === "open-detail") {
const detailScope = actionElement.getAttribute("data-detail-scope");
const detailValue = actionElement.getAttribute("data-detail-value");
openDetailModal(detailScope, detailValue);
return;
}
if (action === "open-action-form") {
const workloadId = actionElement.getAttribute("data-workload-id");
if (workloadId) {
openActionFormModal(workloadId);
}
return;
}
if (action === "open-ramp-comparison") {
const workloadId = actionElement.getAttribute("data-workload-id");
if (workloadId) {
openRampComparisonModal(workloadId);
}
return;
}
if (action === "close-ramp-comparison") {
appState.forecastUpdateConfirmModal = null;
appState.rampComparisonModal = null;
renderModal();
return;
}
if (action === "open-forecast-update-confirm") {
openForecastUpdateConfirmModal();
return;
}
if (action === "close-forecast-update-confirm") {
appState.forecastUpdateConfirmModal = null;
renderModal();
return;
}
if (action === "submit-forecast-update") {
void submitForecastUpdate();
return;
}
if (action === "close-action-form") {
appState.actionFormModal = null;
renderModal();
return;
}
if (action === "open-time-entries") {
const srNumber = actionElement.getAttribute("data-sr-number");
if (srNumber) {
void openTimeEntriesDrawer(srNumber);
}
return;
}
if (action === "close-time-entries") {
appState.timeEntriesDrawer = null;
renderModal();
return;
}
if (action === "retry-time-entries") {
const srNumber = appState.timeEntriesDrawer?.srNumber;
if (srNumber) {
void openTimeEntriesDrawer(srNumber);
}
return;
}
if (action === "open-time-management") {
void openTimeManagementModal();
return;
}
if (action === "close-time-management") {
appState.timeManagementModal = null;
renderModal();
return;
}
if (action === "time-management-add-sr") {
addTimeManagementEntryRow("sr");
return;
}
if (action === "time-management-add-non-service") {
addTimeManagementEntryRow("non-service");
return;
}
if (action === "time-management-save") {
void saveTimeManagementChanges();
return;
}
if (action === "time-management-remove-row") {
removeTimeManagementEntryRow(actionElement.getAttribute("data-row-id"));
return;
}
if (action === "open-time-management-date-picker") {
if (!actionElement.matches("button.arch-panel-extension-date-picker-button")) {
return;
}
toggleTimeManagementDatePicker();
return;
}
if (action === "time-management-date-week-prev") {
void shiftTimeManagementDateByWeeks(-1);
return;
}
if (action === "time-management-date-week-next") {
void shiftTimeManagementDateByWeeks(1);
return;
}
if (action === "time-management-date-picker-prev") {
shiftTimeManagementDatePickerMonth(-1);
return;
}
if (action === "time-management-date-picker-next") {
shiftTimeManagementDatePickerMonth(1);
return;
}
if (action === "time-management-date-picker-select") {
const selectedDate = actionElement.getAttribute("data-date-value");
if (selectedDate) {
void selectTimeManagementDate(selectedDate);
}
return;
}
if (action === "time-management-combobox-option") {
selectTimeManagementComboboxOption(
actionElement.getAttribute("data-field"),
actionElement.getAttribute("data-value"),
actionElement.getAttribute("data-row-id")
);
return;
}
if (action === "submit-action-form") {
const form = actionElement.closest("form");
if (form instanceof HTMLFormElement) {
void submitActionForm(form);
}
return;
}
if (action === "close-detail") {
closeDetailModal();
renderModal();
return;
}
if (action === "sort-detail") {
const sortKey = actionElement.getAttribute("data-sort-key");
if (sortKey) {
toggleDetailSort(sortKey);
}
}
}
function handleOverlayChange(event) {
const target = event.target;
if (!(target instanceof HTMLInputElement || target instanceof HTMLSelectElement)) {
return;
}
const action = target.getAttribute("data-action");
if (action === "time-management-date-change") {
void loadTimeManagementData(target.value);
return;
}
if (action === "time-management-sr-change") {
updateTimeManagementSelectedSr(
target.value,
target.getAttribute("data-row-id")
);
return;
}
if (action === "time-management-activity-change") {
updateTimeManagementField(
"selectedActivityValue",
target.value,
target.getAttribute("data-row-id")
);
return;
}
if (action === "time-management-task-type-change") {
updateTimeManagementField(
"selectedTaskTypeValue",
target.value,
target.getAttribute("data-row-id")
);
return;
}
if (action === "time-management-hours-change") {
updateTimeManagementDayHours(
target.getAttribute("data-row-id"),
target.getAttribute("data-date-key"),
target.value
);
}
}
function handleOverlayInput(event) {
const target = event.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
const field = target.getAttribute("data-combobox-field");
if (!field) {
return;
}
filterTimeManagementCombobox(target);
}
function handleOverlayKeydown(event) {
const target = event.target;
if (!(target instanceof HTMLInputElement)) {
return;
}
if (target.getAttribute("data-action") === "time-management-hours-change") {
handleTimeManagementHourKeydown(event, target);
return;
}
const field = target.getAttribute("data-combobox-field");
if (!field) {
return;
}
if (
!["ArrowDown", "ArrowUp", "Home", "End", "Enter", "Escape"].includes(
event.key
)
) {
return;
}
handleTimeManagementComboboxKeydown(event, target);
}
function handleTimeManagementHourKeydown(event, input) {
if (
![
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Home",
"End",
"Enter",
].includes(event.key)
) {
return;
}
const nextInput = getNextTimeManagementHourInput(input, event);
if (!nextInput) {
return;
}
event.preventDefault();
nextInput.focus();
nextInput.select();
}
function getNextTimeManagementHourInput(currentInput, event) {
const rows = Array.from(
document.querySelectorAll(
".arch-panel-extension-time-management-entry-row[data-row-id]"
)
);
const currentRow = currentInput.closest(
".arch-panel-extension-time-management-entry-row"
);
const rowIndex = rows.indexOf(currentRow);
if (rowIndex < 0) {
return null;
}
const currentInputs = getTimeManagementHourInputs(rows[rowIndex]);
const columnIndex = currentInputs.indexOf(currentInput);
if (columnIndex < 0) {
return null;
}
let nextRowIndex = rowIndex;
let nextColumnIndex = columnIndex;
if (event.key === "ArrowLeft") {
nextColumnIndex -= 1;
} else if (event.key === "ArrowRight") {
nextColumnIndex += 1;
} else if (event.key === "ArrowUp") {
nextRowIndex -= 1;
} else if (event.key === "ArrowDown" || event.key === "Enter") {
nextRowIndex += event.shiftKey ? -1 : 1;
} else if (event.key === "Home") {
nextColumnIndex = 0;
} else if (event.key === "End") {
nextColumnIndex = currentInputs.length - 1;
}
const nextRow = rows[nextRowIndex];
if (!nextRow) {
return null;
}
const nextInputs = getTimeManagementHourInputs(nextRow);
return nextInputs[nextColumnIndex] || null;
}
function getTimeManagementHourInputs(row) {
return Array.from(
row.querySelectorAll('input[data-action="time-management-hours-change"]')
);
}
return Object.freeze({
handleOverlayClick,
handleOverlayChange,
handleOverlayInput,
handleOverlayKeydown,
});
}
globalThis.ArchPanelModalEvents = Object.freeze({
create,
});
})();

328
src/content/shell-view.js Normal file
View File

@@ -0,0 +1,328 @@
(() => {
function create({
IDS,
CLASSES,
USER_BUTTON_LABEL,
normalizeText,
isVisible,
}) {
function applyDialogTheme(overlay, theme) {
const dialog = overlay?.querySelector(`#${IDS.dialog}`);
if (!dialog) {
return;
}
dialog.setAttribute("data-theme", theme);
}
function ensureStyles() {
// Styles are loaded by manifest.json. This function remains as a stable callsite.
}
function applyTheme(header, functionsContainer, anchor, mount, overlay) {
const headerStyle = window.getComputedStyle(header);
const functionsStyle = window.getComputedStyle(functionsContainer);
const sourceBackground = resolveBackgroundColor(header, headerStyle);
const sourceForeground = resolveForegroundColor(
header,
functionsContainer,
anchor,
headerStyle,
functionsStyle
);
const workbenchHeader = mix(
sourceBackground,
{ r: 75, g: 51, b: 93, a: 1 },
0.52
);
const workbenchAccent = mix(
workbenchHeader,
{ r: 108, g: 77, b: 128, a: 1 },
0.38
);
const focusColor = mix(sourceForeground, workbenchHeader, 0.28);
const themeTargets = [mount, overlay].filter(Boolean);
for (const target of themeTargets) {
target.style.setProperty(
"--arch-panel-bg",
toColor(mix(sourceBackground, sourceForeground, 0.12))
);
target.style.setProperty(
"--arch-panel-bg-hover",
toColor(mix(sourceBackground, sourceForeground, 0.22))
);
target.style.setProperty(
"--arch-panel-border",
toColor(mix(sourceBackground, sourceForeground, 0.32))
);
target.style.setProperty("--arch-panel-fg", toColor(sourceForeground, 1));
target.style.setProperty("--arch-panel-focus", toColor(focusColor, 0.94));
target.style.setProperty(
"--arch-panel-font",
functionsStyle.fontFamily || headerStyle.fontFamily || "inherit"
);
target.style.setProperty(
"--arch-workbench-header",
toColor(workbenchHeader, 1)
);
target.style.setProperty(
"--arch-workbench-accent",
toColor(workbenchAccent, 1)
);
}
}
function resolveForegroundColor(
header,
functionsContainer,
anchor,
headerStyle,
functionsStyle
) {
const salesPlanningElement = findTextElement(header, "Sales Planning");
const salesPlanningColor = salesPlanningElement
? parseColor(window.getComputedStyle(salesPlanningElement).color)
: null;
const functionsItemColor = findVisibleChildColor(functionsContainer);
const userColor = anchor
? parseColor(window.getComputedStyle(anchor).color)
: null;
return (
userColor ||
functionsItemColor ||
parseColor(functionsStyle.color) ||
salesPlanningColor ||
parseColor(headerStyle.color) || {
r: 255,
g: 255,
b: 255,
a: 1,
}
);
}
function resolveBackgroundColor(element, style) {
const directBackground = parseColor(style.backgroundColor);
if (directBackground && directBackground.a > 0) {
return directBackground;
}
let current = element.parentElement;
while (current) {
const currentBackground = parseColor(
window.getComputedStyle(current).backgroundColor
);
if (currentBackground && currentBackground.a > 0) {
return currentBackground;
}
current = current.parentElement;
}
return { r: 75, g: 51, b: 93, a: 1 };
}
function parseColor(value) {
if (!value || value === "transparent") {
return null;
}
const rgbMatch = value.match(
/rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})(?:[\s,/]+([.\d]+))?\s*\)/i
);
if (rgbMatch) {
return {
r: Number.parseInt(rgbMatch[1], 10),
g: Number.parseInt(rgbMatch[2], 10),
b: Number.parseInt(rgbMatch[3], 10),
a: rgbMatch[4] === undefined ? 1 : Number.parseFloat(rgbMatch[4]),
};
}
const hexMatch = value.match(/^#([\da-f]{3,8})$/i);
if (!hexMatch) {
return null;
}
const hexValue = hexMatch[1];
if (hexValue.length === 3 || hexValue.length === 4) {
const [r, g, b, a = "f"] = hexValue.split("");
return {
r: Number.parseInt(r + r, 16),
g: Number.parseInt(g + g, 16),
b: Number.parseInt(b + b, 16),
a: Number.parseInt(a + a, 16) / 255,
};
}
if (hexValue.length === 6 || hexValue.length === 8) {
return {
r: Number.parseInt(hexValue.slice(0, 2), 16),
g: Number.parseInt(hexValue.slice(2, 4), 16),
b: Number.parseInt(hexValue.slice(4, 6), 16),
a:
hexValue.length === 8
? Number.parseInt(hexValue.slice(6, 8), 16) / 255
: 1,
};
}
return null;
}
function mix(base, tint, amount) {
const ratio = clamp(amount, 0, 1);
return {
r: Math.round(base.r + (tint.r - base.r) * ratio),
g: Math.round(base.g + (tint.g - base.g) * ratio),
b: Math.round(base.b + (tint.b - base.b) * ratio),
a: base.a + (tint.a - base.a) * ratio,
};
}
function toColor(color, alphaOverride) {
const alpha = alphaOverride ?? color.a ?? 1;
return `rgba(${color.r}, ${color.g}, ${color.b}, ${clamp(alpha, 0, 1)})`;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function findUserAnchor(container) {
const selector = [
`button[label="${USER_BUTTON_LABEL}"]`,
`[role="button"][label="${USER_BUTTON_LABEL}"]`,
`oj-button[label="${USER_BUTTON_LABEL}"]`,
`button[aria-label="${USER_BUTTON_LABEL}"]`,
`[role="button"][aria-label="${USER_BUTTON_LABEL}"]`,
`[label="${USER_BUTTON_LABEL}"]`,
`[aria-label="${USER_BUTTON_LABEL}"]`,
].join(", ");
const directMatch = container.querySelector(selector);
if (directMatch && isVisible(directMatch)) {
return directMatch;
}
const fallbackMatch = Array.from(container.querySelectorAll("*")).find(
(element) => {
if (!isVisible(element) || element.closest(`#${IDS.mount}`)) {
return false;
}
return (
element.getAttribute("label") === USER_BUTTON_LABEL ||
element.getAttribute("aria-label") === USER_BUTTON_LABEL
);
}
);
return fallbackMatch || null;
}
function findTextElement(container, expectedText) {
const normalizedExpected = normalizeText(expectedText);
const elements = Array.from(container.querySelectorAll("*"));
return (
elements.find((element) => {
const text = normalizeText(element.textContent || "");
return text && text.includes(normalizedExpected);
}) || null
);
}
function resolveInsertionTarget(container, anchor, mount) {
const directAnchor = anchor ? getDirectChild(anchor, container) : null;
const fallbackTarget = findFirstInsertionTarget(container, mount);
const candidate = directAnchor || fallbackTarget;
if (!candidate) {
return null;
}
if (candidate === mount) {
return findNextValidSibling(container, mount);
}
return candidate.parentElement === container ? candidate : null;
}
function getDirectChild(element, container) {
let current = element;
while (current && current.parentElement !== container) {
current = current.parentElement;
}
return current && current.parentElement === container ? current : null;
}
function findFirstInsertionTarget(container, mount) {
return Array.from(container.children).find((child) => child !== mount) || null;
}
function findNextValidSibling(container, mount) {
let sibling = mount.nextElementSibling;
while (sibling) {
if (sibling.parentElement === container) {
return sibling;
}
sibling = sibling.nextElementSibling;
}
return null;
}
function findVisibleChildColor(container) {
const visibleChild = Array.from(container.querySelectorAll("*")).find(
(element) => isVisible(element) && !element.closest(`#${IDS.mount}`)
);
return visibleChild
? parseColor(window.getComputedStyle(visibleChild).color)
: null;
}
function lockScroll() {
document.documentElement.classList.add(CLASSES.modalOpen);
document.body?.classList.add(CLASSES.modalOpen);
}
function unlockScroll() {
document.documentElement.classList.remove(CLASSES.modalOpen);
document.body?.classList.remove(CLASSES.modalOpen);
}
return Object.freeze({
applyDialogTheme,
ensureStyles,
applyTheme,
findUserAnchor,
resolveInsertionTarget,
lockScroll,
unlockScroll,
});
}
globalThis.ArchPanelShellView = Object.freeze({
create,
});
})();

2530
src/content/styles.css Normal file

File diff suppressed because it is too large Load Diff

15
src/content/templates.js Normal file
View File

@@ -0,0 +1,15 @@
(() => {
function hcmTile(hcmTileId) {
return `
<div class="flat-grid-cell" id="${hcmTileId}">
<div id="c_75628d297d10409585cc1d40bb36f59d" class="app-nav-item" filmstrip="Arch Panel" page="undefined" index="0" type="subcluster" title="Arch Panel" group="groupNode_sales" destinationurl="#" targetframe="_self" isdesturlexist="true" role="presentation">
<svg viewBox="0 0 48 48" style="fill:currentColor" class="svg-nav suiicon svg-bkgd11" data-icon="navi_dashboard" role="presentation" focusable="false"><path class="svg-shortcut" d="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"></path><path class="svg-cluster" d="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"></path><path class="svg-icon18" d="M38 16H10v18.8c0 .7.5 1.2 1 1.2h26c.7 0 1.2-.5 1.2-1.2V16zm-9 3.2c0-.7.5-1.2 1-1.2h5c.5 0 1 .5 1 1.2v5.6c0 .7-.5 1.2-1 1.2h-5c-.5 0-1-.5-1-1.2v-5.6zM21 33c0 .6-.3 1-1 1h-7c-.7 0-1-.5-1-1.2V19.2c0-.7.5-1.2 1.2-1.2h6.6c.7 0 1.2.4 1.2 1v14zm1-13.8c0-.7.5-1.2 1-1.2h4c.5 0 1 .5 1 1.2v5.6c0 .7-.5 1.2-1 1.2h-4c-.5 0-1-.5-1-1.2v-5.6zM33 33H22v-2h11v2zm3-3H22v-2h14v2z"></path><path class="svg-icon14" d="M30 25v-5h1v4h4v1h-5zm4-5h1v3h-1v-3zm-2 1h1v2h-1v-2z"></path><path class="svg-icon03" d="M23 20h4v1h-4v-1zm3 4h-3v-1h3v1z"></path><path class="svg-icon04" d="M17 20c1 0 2 1 2 2h-2v-2zm-1 5c-1 0-2-1.2-2-2 0-1 1-2 2-2v2h2c0 1-1 2-2 2zm3 4h-5v-1h5v1zm0 3h-5v-1h5v1z"></path><path class="svg-icon05" d="M11 12h26c.7 0 1.2.6 1.2 1.2V15H10v-1.8c0-.6.5-1.2 1-1.2z"></path><path class="svg-outline" d="M38.1 35.76H9.92a1.16 1.16 0 0 1-1.15-1.17l.01-18.78h30.47V34.6a1.16 1.16 0 0 1-1.15 1.17zM23.9 19.31h2.53a1.15 1.15 0 0 1 1.13 1.17v3.45a1.15 1.15 0 0 1-1.13 1.17h-2.52a1.15 1.15 0 0 1-1.14-1.17v-3.45a1.15 1.15 0 0 1 1.14-1.17zm8.2.03h2.55a1.15 1.15 0 0 1 1.12 1.17v3.42a1.15 1.15 0 0 1-1.12 1.17H32.1a1.15 1.15 0 0 1-1.13-1.17v-3.42a1.15 1.15 0 0 1 1.13-1.17zm-18.6-.03h4.6a1.04 1.04 0 0 1 1.18 1v12.07a.9.9 0 0 1-1 1h-4.97a1.03 1.03 0 0 1-1-1.17V20.48a1.18 1.18 0 0 1 1.19-1.17z"></path><path class="svg-outline" d="M9.93 12.3H38.1a1.21 1.21 0 0 1 1.16 1.23l.02 2.28H8.78l-.01-2.28a1.21 1.21 0 0 1 1.16-1.23zM36 28.66H22m11 3.46H22"></path></svg>
<a id="c_75628d297d10409585cc1d40bb36f48d_0" class="app-nav-label flat-grid-nav-label" href="#">Arch Panel</a>
</div>
</div>`;
}
globalThis.ArchPanelTemplates = Object.freeze({
hcmTile,
});
})();

View File

@@ -0,0 +1,966 @@
(() => {
function create({
appState,
IDS,
DETAIL_SORT_KEYS,
hcmPage,
workbenchRepository,
comcipRepository,
readCachedDataset,
writeCachedDataset,
readCachedWorklistToken,
setStoredCalendarView,
refreshTaskTypeCache,
renderModal,
cleanString,
normalizeString,
toNumber,
toIsoDateString,
mapWithConcurrency,
getLocalDateKey,
getErrorMessage,
formatWholeNumber,
formatProgressPercent,
formatDate,
teamContainsUser,
stripTransientFields,
createExportPayload,
createDatasetSnapshot,
buildRampForecastComparisonRows,
parseForecastMonthNumber,
createForecastUpdatePayload,
normalizeTimeEntriesSummary,
getCalendarAnchorDate,
}) {
let cacheHydrationPromise = null;
async function refreshData() {
if (appState.status === "loading") {
return;
}
try {
appState.status = "loading";
appState.errorMessage = "";
updateLoadingProgress("Requesting current user", 4);
const user = await fetchCurrentUser();
updateLoadingProgress("Loading current resource user", 8);
const resourceCurrentUser = await comcipRepository
.fetchResourceCurrentUser(user.userEmail)
.catch(() => null);
updateLoadingProgress("Loading customer summary pages", 12);
const customers = await workbenchRepository.fetchAllCustomers(
user.userEmail
);
updateLoadingProgress(
`Loading workloads for ${formatWholeNumber(
customers.length
)} customers`,
24
);
let completedCustomers = 0;
const customersWithWorkloads = await mapWithConcurrency(
customers,
4,
async (customer, index) => {
updateLoadingProgress(
`Loading workloads ${index + 1}/${customers.length}: ${customer.name}`,
getSegmentProgress(24, 52, completedCustomers, customers.length)
);
const workloads = await workbenchRepository.fetchCustomerWorkloads(
customer
);
completedCustomers += 1;
updateLoadingProgress(
`Loaded workloads ${completedCustomers}/${customers.length}: ${customer.name}`,
getSegmentProgress(24, 52, completedCustomers, customers.length)
);
return {
customerId: customer.customerId,
name: customer.name,
workloads,
};
}
);
const allWorkloads = customersWithWorkloads.flatMap((customer) => {
return customer.workloads.map((workload) => ({
...workload,
customerId: customer.customerId,
customerName: customer.name,
}));
});
updateLoadingProgress(
`Loading actions and service requests for ${formatWholeNumber(
allWorkloads.length
)} workloads`,
56
);
let completedWorkloads = 0;
const enrichedWorkloads = await mapWithConcurrency(
allWorkloads,
6,
async (workload, index) => {
updateLoadingProgress(
`Loading actions and SRs ${index + 1}/${allWorkloads.length}: ${workload.name}`,
getSegmentProgress(56, 86, completedWorkloads, allWorkloads.length)
);
const [actions, serviceRequests] = await Promise.all([
workbenchRepository.fetchWorkloadActions(workload.workloadId),
workbenchRepository.fetchWorkloadServiceRequests(workload.workloadId),
]);
completedWorkloads += 1;
updateLoadingProgress(
`Loaded actions and SRs ${completedWorkloads}/${allWorkloads.length}: ${workload.name}`,
getSegmentProgress(56, 86, completedWorkloads, allWorkloads.length)
);
const hasSR = serviceRequests.some((serviceRequest) => {
return (
normalizeString(serviceRequest.status) ===
"SVC_PENDING_DELIVERY" &&
teamContainsUser(serviceRequest.team, user.userEmail)
);
});
const hasAction = actions.some((action) => {
return (
normalizeString(action.owner) === normalizeString(user.userEmail)
);
});
return {
...workload,
actions,
serviceRequests,
hasSR,
hasAction,
};
}
);
const customersMap = new Map(
customersWithWorkloads.map((customer) => [
customer.customerId,
{
customerId: customer.customerId,
name: customer.name,
workloads: [],
},
])
);
for (const workload of enrichedWorkloads) {
const customer = customersMap.get(workload.customerId);
if (customer) {
customer.workloads.push(stripTransientFields(workload));
}
}
updateLoadingProgress("Loading pending delivery service requests", 90);
const pendingServiceRequests = await comcipRepository
.fetchPendingServiceRequests()
.catch(() => []);
updateLoadingProgress("Refreshing time management task types", 94);
await refreshTaskTypeCache().catch(() => null);
updateLoadingProgress("Preparing dashboard cache", 96);
const finalCustomers = Array.from(customersMap.values());
const exportPayload = createExportPayload(
user,
finalCustomers,
pendingServiceRequests,
resourceCurrentUser
);
const dataset = createDatasetSnapshot(exportPayload);
await writeCachedDataset(exportPayload);
updateLoadingProgress("Dashboard ready", 100);
appState.dataset = dataset;
appState.status = "ready";
appState.loadingMessage = "";
appState.loadingProgress = 0;
appState.detailModal = null;
renderModal();
} catch (error) {
appState.status = "error";
appState.errorMessage = getErrorMessage(error);
appState.loadingMessage = "";
appState.loadingProgress = 0;
renderModal();
}
}
function updateLoadingProgress(message, progress) {
appState.loadingMessage = message;
appState.loadingProgress = Math.max(
0,
Math.min(Number(progress) || 0, 100)
);
if (!updateLoadingProgressDom()) {
renderModal();
window.requestAnimationFrame(() => {
updateLoadingProgressDom();
});
}
}
function updateLoadingProgressDom() {
const overlay = document.getElementById(IDS.overlay);
const progress = Math.max(0, Math.min(appState.loadingProgress || 0, 100));
const stateCopy = overlay?.querySelector(
".arch-panel-extension-state-copy"
);
const progressRoot = overlay?.querySelector(
".arch-panel-extension-loading-progress"
);
const progressBar = overlay?.querySelector(
".arch-panel-extension-loading-progress-fill"
);
const progressTrackValue = overlay?.querySelector(
".arch-panel-extension-loading-progress-value"
);
const shellNote = overlay?.querySelector(
".arch-panel-extension-shell-note"
);
if (!stateCopy || !progressRoot || !progressBar) {
return false;
}
stateCopy.textContent = appState.loadingMessage;
progressRoot.setAttribute("aria-valuenow", String(Math.round(progress)));
progressBar.style.width = `${progress}%`;
if (progressTrackValue) {
progressTrackValue.textContent = formatProgressPercent(progress);
}
if (shellNote) {
shellNote.textContent = appState.loadingMessage || "Loading";
}
return true;
}
function getSegmentProgress(start, end, current, total) {
const denominator = Math.max(Number(total) || 0, 1);
const ratio = Math.max(
0,
Math.min((Number(current) || 0) / denominator, 1)
);
return start + (end - start) * ratio;
}
async function hydrateDatasetFromCache() {
if (appState.dataset || cacheHydrationPromise) {
return cacheHydrationPromise;
}
appState.status = "loading";
appState.errorMessage = "";
appState.loadingMessage = "Loading cached snapshot";
appState.loadingProgress = 18;
renderModal();
cacheHydrationPromise = (async () => {
try {
const exportPayload = await readCachedDataset();
appState.loadingProgress = exportPayload ? 100 : 0;
if (exportPayload) {
appState.dataset = createDatasetSnapshot(exportPayload);
appState.status = "ready";
} else {
appState.status = "idle";
}
} catch (error) {
appState.status = "error";
appState.errorMessage = getErrorMessage(error);
} finally {
appState.loadingMessage = "";
appState.loadingProgress = 0;
cacheHydrationPromise = null;
renderModal();
}
})();
return cacheHydrationPromise;
}
async function fetchCurrentUser() {
const currentFrameToken = hcmPage.syncWorklistIframeToken({
force: true,
})?.token;
const worklistToken = currentFrameToken || (await readCachedWorklistToken());
if (!worklistToken) {
throw new Error(
"Unable to resolve Worklist token. Reload the FuseWelcome page so the Worklist iframe token can be captured before refreshing data."
);
}
appState.sessionAuthHeaders = {};
const user = await workbenchRepository.fetchWorklistCurrentUser(
worklistToken
);
appState.sessionAuthHeaders = {};
return user;
}
function findWorkloadById(workloadId) {
if (!appState.dataset || !workloadId) {
return null;
}
for (const customer of appState.dataset.customers || []) {
for (const workload of customer.workloads || []) {
if (workload.workloadId === workloadId) {
return {
...workload,
customerId: workload.customerId || customer.customerId,
customerName: workload.customerName || customer.name,
};
}
}
}
return null;
}
function applyForecastUpdateToDataset(workloadId, payload) {
if (!appState.dataset || !workloadId) {
return;
}
const previousDetailModal = appState.detailModal
? {
scope: appState.detailModal.scope,
value: appState.detailModal.value,
sortKey: appState.detailModal.sortKey,
sortDirection: appState.detailModal.sortDirection,
type: appState.detailModal.type,
}
: null;
const updatesByPeriod = new Map(
(payload || []).map((item) => [
`${toNumber(item.year)}-${toNumber(item.month)}`,
toNumber(item.amount),
])
);
function updateForecast(workload) {
if (!workload || workload.workloadId !== workloadId) {
return workload;
}
workload.forecast = (workload.forecast || []).map((forecastItem) => {
const month = parseForecastMonthNumber(forecastItem.month);
const year = toNumber(forecastItem.year);
const key = `${year}-${month}`;
if (!updatesByPeriod.has(key)) {
return forecastItem;
}
return {
...forecastItem,
adjustedConsumptionAmount: updatesByPeriod.get(key),
updatedBy: appState.dataset.user?.userEmail || forecastItem.updatedBy,
updatedDate: new Date().toISOString(),
};
});
return workload;
}
for (const customer of appState.dataset.customers || []) {
customer.workloads = (customer.workloads || []).map((workload) =>
updateForecast(workload)
);
}
for (const customer of appState.dataset.exportPayload?.customers || []) {
customer.workloads = (customer.workloads || []).map((workload) =>
updateForecast(workload)
);
}
appState.dataset = createDatasetSnapshot(appState.dataset.exportPayload);
if (
previousDetailModal?.scope &&
previousDetailModal.type !== "serviceRequests"
) {
restoreDetailModal(previousDetailModal);
}
void writeCachedDataset(appState.dataset.exportPayload);
}
function openDetailModal(scope, value) {
if (!appState.dataset) {
return;
}
const eligibleWorkloads = appState.dataset.summary.eligibleWorkloads;
let items = [];
let label = "";
let title = "";
if (scope === "stage") {
items = eligibleWorkloads.filter(
(workload) => workload.opportunityForecastTypeGroup === value
);
label = `Opportunity type: ${value}`;
title = `${value} workloads`;
} else if (scope === "sr") {
const flag = value === "true";
items = eligibleWorkloads.filter((workload) => workload.hasSR === flag);
label = "Status de SRs";
title = flag ? "Workloads com SR" : "Workloads sem SR";
} else if (scope === "action") {
const flag = value === "true";
items = eligibleWorkloads.filter((workload) => workload.hasAction === flag);
label = "Status de Consumption Plan/action";
title = flag ? "Workloads com action" : "Workloads sem action";
} else if (scope === "sr-hours") {
const pendingServiceRequests =
appState.dataset.summary.pendingServiceRequests || [];
const isAboveFour = value === "above-4";
items = pendingServiceRequests.filter((serviceRequest) => {
const hours = toNumber(serviceRequest.totalHoursWorked);
return isAboveFour ? hours > 4 : hours >= 0 && hours <= 4;
});
items.sort((left, right) => {
return toNumber(left.totalHoursWorked) - toNumber(right.totalHoursWorked);
});
label = "SRs por horas reportadas";
title = isAboveFour ? "SRs acima de 4h" : "SRs entre 0 e 4h";
} else if (scope === "calendar-day") {
const allWorkloads = appState.dataset.summary.allWorkloads || [];
items = allWorkloads.filter((workload) => {
return getLocalDateKey(workload.consumptionStartDate) === value;
});
label = "Workload start calendar";
title = `Workloads starting ${formatDate(value) || value}`;
}
appState.detailModal = {
label,
title,
items,
totalAcr:
scope === "sr-hours"
? 0
: items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
sortKey: DETAIL_SORT_KEYS.adjustedACR,
sortDirection: "desc",
scope,
value,
type: scope === "sr-hours" ? "serviceRequests" : "workloads",
};
renderModal();
}
function openActionFormModal(workloadId) {
appState.actionFormModal = {
workloadId,
errorMessage: "",
isSubmitting: false,
};
renderModal();
}
function openRampComparisonModal(workloadId) {
const workload = findWorkloadById(workloadId);
if (!workload) {
return;
}
appState.rampComparisonModal = {
workload,
rows: buildRampForecastComparisonRows([workload]),
};
renderModal();
}
function openForecastUpdateConfirmModal() {
if (!appState.rampComparisonModal || !appState.dataset) {
return;
}
const payload = createForecastUpdatePayload(
appState.rampComparisonModal.rows || [],
appState.dataset?.user?.userEmail || ""
);
appState.forecastUpdateConfirmModal = {
workloadId: appState.rampComparisonModal.workload.workloadId,
payload,
isSubmitting: false,
errorMessage: "",
};
renderModal();
}
async function submitForecastUpdate() {
const modal = appState.forecastUpdateConfirmModal;
if (!modal || modal.isSubmitting) {
return;
}
appState.forecastUpdateConfirmModal = {
...modal,
isSubmitting: true,
errorMessage: "",
};
renderModal();
try {
const workloadId = cleanString(modal.workloadId);
await workbenchRepository.updateWorkloadForecast(
workloadId,
modal.payload
);
applyForecastUpdateToDataset(workloadId, modal.payload);
const updatedWorkload = findWorkloadById(workloadId);
appState.forecastUpdateConfirmModal = null;
if (updatedWorkload) {
appState.rampComparisonModal = {
workload: updatedWorkload,
rows: buildRampForecastComparisonRows([updatedWorkload]),
};
}
renderModal();
} catch (error) {
appState.forecastUpdateConfirmModal = {
...modal,
isSubmitting: false,
errorMessage: getErrorMessage(error),
};
renderModal();
}
}
async function openTimeEntriesDrawer(srNumber) {
const normalizedSrNumber = cleanString(srNumber);
if (!normalizedSrNumber) {
return;
}
appState.timeEntriesDrawer = {
srNumber: normalizedSrNumber,
status: "loading",
errorMessage: "",
payload: null,
items: [],
};
renderModal();
try {
const response = await comcipRepository.fetchTimeEntriesSummary(
normalizedSrNumber
);
appState.timeEntriesDrawer = {
srNumber: normalizedSrNumber,
status: "ready",
errorMessage: "",
payload: response.payload,
items: normalizeTimeEntriesSummary(response.payload),
};
} catch (error) {
appState.timeEntriesDrawer = {
srNumber: normalizedSrNumber,
status: "error",
errorMessage: getErrorMessage(error),
payload: null,
items: [],
};
}
renderModal();
}
function shiftCalendarPeriod(delta) {
const nextDate =
appState.calendarView === "week"
? getShiftedCalendarWeekDate(delta)
: new Date(appState.calendarYear, appState.calendarMonth + delta, 1);
appState.calendarYear = nextDate.getFullYear();
appState.calendarMonth = nextDate.getMonth();
appState.calendarAnchorDateKey = getLocalDateKey(nextDate);
renderModal();
}
function goToCurrentCalendarPeriod() {
const today = new Date();
appState.calendarYear = today.getFullYear();
appState.calendarMonth = today.getMonth();
appState.calendarAnchorDateKey = getLocalDateKey(today);
renderModal();
}
function setCalendarView(view) {
const normalizedView = view === "week" ? "week" : "month";
if (appState.calendarView === normalizedView) {
return;
}
appState.calendarView = normalizedView;
setStoredCalendarView(normalizedView);
if (normalizedView === "week") {
const today = new Date();
const anchorDate =
today.getFullYear() === appState.calendarYear &&
today.getMonth() === appState.calendarMonth
? today
: new Date(appState.calendarYear, appState.calendarMonth, 1);
appState.calendarAnchorDateKey = getLocalDateKey(anchorDate);
appState.calendarYear = anchorDate.getFullYear();
appState.calendarMonth = anchorDate.getMonth();
} else {
const anchorDate = getCalendarAnchorDate(
appState.calendarYear,
appState.calendarMonth,
appState.calendarAnchorDateKey
);
appState.calendarYear = anchorDate.getFullYear();
appState.calendarMonth = anchorDate.getMonth();
}
renderModal();
}
function shiftCalendarMonth(delta) {
shiftCalendarPeriod(delta);
}
function goToCurrentCalendarMonth() {
goToCurrentCalendarPeriod();
}
function getShiftedCalendarWeekDate(delta) {
const anchorDate = getCalendarAnchorDate(
appState.calendarYear,
appState.calendarMonth,
appState.calendarAnchorDateKey
);
anchorDate.setDate(anchorDate.getDate() + delta * 7);
return anchorDate;
}
async function submitActionForm(form) {
if (!appState.dataset || !appState.actionFormModal) {
return;
}
if (!form.reportValidity()) {
return;
}
const formData = new FormData(form);
const parentId = cleanString(formData.get("parentId"));
const ownerEmail = cleanString(formData.get("ownerEmail"));
const startDate = toIsoDateString(formData.get("startDate"));
const endDate = toIsoDateString(formData.get("endDate"));
const payload = {
parentType: "WORKLOAD",
parentId,
action: {
ownerEmail,
role: "CLOUD_ARCHITECT",
name: "SUCCESS_PLAN_CONSUM_RVW",
team: cleanString(formData.get("team")),
tags: "",
startDate,
endDate,
status: "COMPLETE",
complexity: cleanString(formData.get("complexity")),
notes: [],
objective: "",
createdBy: cleanString(formData.get("createdBy")),
updatedBy: cleanString(formData.get("updatedBy")),
},
};
try {
appState.actionFormModal = {
...appState.actionFormModal,
isSubmitting: true,
errorMessage: "",
};
renderModal();
await workbenchRepository.createWorkloadAction(payload);
await applyCreatedActionToDataset(payload);
appState.actionFormModal = null;
renderModal();
} catch (error) {
appState.actionFormModal = {
...appState.actionFormModal,
isSubmitting: false,
errorMessage: getErrorMessage(error),
};
renderModal();
}
}
async function applyCreatedActionToDataset(payload) {
if (!appState.dataset) {
return;
}
const updatedCustomers = (appState.dataset.customers || []).map(
(customer) => ({
...customer,
workloads: (customer.workloads || []).map((workload) => {
if (workload.workloadId !== payload.parentId) {
return workload;
}
return {
...workload,
hasAction: true,
actions: [
...(workload.actions || []),
{
owner: payload.action.ownerEmail,
role: payload.action.role,
name: payload.action.name,
team: payload.action.team,
startDate: payload.action.startDate,
endDate: payload.action.endDate,
complexity: payload.action.complexity,
},
],
};
}),
})
);
const exportPayload = createExportPayload(
appState.dataset.user,
updatedCustomers,
appState.dataset.exportPayload?.pendingServiceRequests || [],
appState.dataset.exportPayload?.resourceCurrentUser || null
);
const nextDataset = createDatasetSnapshot(exportPayload);
const previousDetailModal = appState.detailModal
? {
scope: appState.detailModal.scope,
value: appState.detailModal.value,
sortKey: appState.detailModal.sortKey,
sortDirection: appState.detailModal.sortDirection,
}
: null;
appState.dataset = nextDataset;
await writeCachedDataset(exportPayload);
if (previousDetailModal?.scope) {
restoreDetailModal(previousDetailModal);
}
}
function restoreDetailModal(detailState) {
const eligibleWorkloads =
appState.dataset?.summary?.eligibleWorkloads || [];
let items = [];
let label = "";
let title = "";
if (detailState.scope === "stage") {
items = eligibleWorkloads.filter(
(workload) =>
workload.opportunityForecastTypeGroup === detailState.value
);
label = `Opportunity type: ${detailState.value}`;
title = `${detailState.value} workloads`;
} else if (detailState.scope === "sr") {
const flag = detailState.value === "true";
items = eligibleWorkloads.filter((workload) => workload.hasSR === flag);
label = "Status de SRs";
title = flag ? "Workloads com SR" : "Workloads sem SR";
} else if (detailState.scope === "action") {
const flag = detailState.value === "true";
items = eligibleWorkloads.filter((workload) => workload.hasAction === flag);
label = "Status de Consumption Plan/action";
title = flag ? "Workloads com action" : "Workloads sem action";
} else if (detailState.scope === "calendar-day") {
const allWorkloads = appState.dataset?.summary?.allWorkloads || [];
items = allWorkloads.filter((workload) => {
return getLocalDateKey(workload.consumptionStartDate) === detailState.value;
});
label = "Workload start calendar";
title = `Workloads starting ${
formatDate(detailState.value) || detailState.value
}`;
}
appState.detailModal = {
label,
title,
items,
totalAcr: items.reduce((sum, workload) => sum + workload.adjustedACR, 0),
sortKey: detailState.sortKey || DETAIL_SORT_KEYS.adjustedACR,
sortDirection: detailState.sortDirection || "desc",
scope: detailState.scope,
value: detailState.value,
type: "workloads",
};
}
function toggleDetailSort(sortKey) {
if (!appState.detailModal) {
return;
}
const nextDirection =
appState.detailModal.sortKey === sortKey &&
appState.detailModal.sortDirection === "asc"
? "desc"
: "asc";
appState.detailModal = {
...appState.detailModal,
sortKey,
sortDirection: nextDirection,
};
renderModal();
}
function getSortedDetailItems() {
if (!appState.detailModal) {
return [];
}
return sortWorkloads(
appState.detailModal.items,
appState.detailModal.sortKey,
appState.detailModal.sortDirection
);
}
function sortWorkloads(items, sortKey, sortDirection = "asc") {
const factor = sortDirection === "desc" ? -1 : 1;
return [...items].sort((left, right) => {
const comparison = compareWorkloadValues(
getSortableWorkloadValue(left, sortKey),
getSortableWorkloadValue(right, sortKey)
);
if (comparison !== 0) {
return comparison * factor;
}
return compareWorkloadValues(
getSortableWorkloadValue(left, DETAIL_SORT_KEYS.workload),
getSortableWorkloadValue(right, DETAIL_SORT_KEYS.workload)
);
});
}
function getSortableWorkloadValue(workload, sortKey) {
switch (sortKey) {
case DETAIL_SORT_KEYS.opportunityNumber:
return cleanString(workload.opportunityNumber);
case DETAIL_SORT_KEYS.customerName:
return cleanString(workload.customerName);
case DETAIL_SORT_KEYS.workload:
return `${cleanString(workload.name)}|${cleanString(
workload.description
)}`;
case DETAIL_SORT_KEYS.rampMonths:
return toNumber(workload.rampMonths);
case DETAIL_SORT_KEYS.adjustedACR:
return toNumber(workload.adjustedACR);
case DETAIL_SORT_KEYS.opportunityForecastTypeGroup:
return cleanString(workload.opportunityForecastTypeGroup);
case DETAIL_SORT_KEYS.hasSR:
return Boolean(workload.hasSR);
case DETAIL_SORT_KEYS.hasAction:
return Boolean(workload.hasAction);
default:
return cleanString(workload.name);
}
}
function compareWorkloadValues(left, right) {
if (typeof left === "number" || typeof right === "number") {
return toNumber(left) - toNumber(right);
}
if (typeof left === "boolean" || typeof right === "boolean") {
return Number(Boolean(left)) - Number(Boolean(right));
}
return String(left || "").localeCompare(String(right || ""), undefined, {
numeric: true,
sensitivity: "base",
});
}
return Object.freeze({
refreshData,
hydrateDatasetFromCache,
findWorkloadById,
openDetailModal,
openActionFormModal,
openRampComparisonModal,
openForecastUpdateConfirmModal,
submitForecastUpdate,
openTimeEntriesDrawer,
shiftCalendarPeriod,
goToCurrentCalendarPeriod,
setCalendarView,
shiftCalendarMonth,
goToCurrentCalendarMonth,
submitActionForm,
toggleDetailSort,
getSortedDetailItems,
});
}
globalThis.ArchPanelWorkbenchController = Object.freeze({
create,
});
})();