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

714
src/views/dashboard-view.js Normal file
View File

@@ -0,0 +1,714 @@
(() => {
function create(dependencies) {
const {
appState,
buildWorkloadStartCalendar,
hasRampForecastMismatch,
renderOpportunityLink,
renderWorkloadLink,
escapeHtml,
cleanString,
formatWholeNumber,
formatCurrency,
formatPercent,
formatProgressPercent,
formatDate,
} = dependencies;
function renderMainContent() {
if (appState.status === "loading") {
return `
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-state-card">
<p class="arch-panel-extension-state-eyebrow">Loading data</p>
<h2 class="arch-panel-extension-state-title">Building the workload dataset</h2>
<p class="arch-panel-extension-state-copy">${escapeHtml(appState.loadingMessage)}</p>
<div
class="arch-panel-extension-loading-progress"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="${Math.round(appState.loadingProgress || 0)}"
>
<div class="arch-panel-extension-loading-progress-meta">
<span>Overall progress</span>
</div>
<div class="arch-panel-extension-loading-progress-track">
<div
class="arch-panel-extension-loading-progress-fill"
style="width: 0%;"
></div>
<span class="arch-panel-extension-loading-progress-value">
${formatProgressPercent(appState.loadingProgress || 0)}
</span>
</div>
</div>
</section>
</main>
`;
}
if (appState.status === "error") {
return `
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-state-card is-error">
<p class="arch-panel-extension-state-eyebrow">Request failed</p>
<h2 class="arch-panel-extension-state-title">Unable to assemble the dashboard</h2>
<p class="arch-panel-extension-state-copy">${escapeHtml(appState.errorMessage)}</p>
<div class="arch-panel-extension-state-actions">
<button type="button" class="arch-panel-extension-utility-button" data-action="refresh">
Retry
</button>
</div>
</section>
</main>
`;
}
if (!appState.dataset) {
return `
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-state-card">
<p class="arch-panel-extension-state-eyebrow">Ready</p>
<h2 class="arch-panel-extension-state-title">Start the live data extraction</h2>
<p class="arch-panel-extension-state-copy">
No cached snapshot is available yet. Use Refresh data to fetch the current user, customers, workloads, actions and service requests.
</p>
<div class="arch-panel-extension-state-actions">
<button type="button" class="arch-panel-extension-utility-button" data-action="refresh">
Refresh data
</button>
</div>
</section>
</main>
`;
}
const { user, summary } = appState.dataset;
return `
<aside class="arch-panel-extension-sidebar${appState.sidebarCollapsed ? " is-collapsed" : ""}" aria-label="Dataset summary">
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Current user</p>
${renderCurrentUserSidebar(user)}
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Exports</p>
<button type="button" class="arch-panel-extension-sidebar-button" data-action="download-json">
Download JSON
</button>
<button type="button" class="arch-panel-extension-sidebar-button" data-action="download-html">
Download HTML
</button>
<button type="button" class="arch-panel-extension-sidebar-button" data-action="refresh">
Refresh data
</button>
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Coverage</p>
<div class="arch-panel-extension-sidebar-data">
<span>Customers: ${formatWholeNumber(summary.totalCustomers)}</span>
<span>Workloads: ${formatWholeNumber(summary.totalWorkloads)}</span>
<span>Pending SRs: ${formatWholeNumber(summary.pendingServiceRequests.length)}</span>
</div>
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Last refresh</p>
<div class="arch-panel-extension-sidebar-data">
<strong>${escapeHtml(summary.generatedAtLabel || "-")}</strong>
<span>Updated by Refresh data</span>
</div>
</div>
<div class="arch-panel-extension-sidebar-group">
<p class="arch-panel-extension-sidebar-label">Extension version</p>
<div class="arch-panel-extension-sidebar-data">
<span>v${escapeHtml(getExtensionVersion())}</span>
</div>
</div>
</aside>
<main class="arch-panel-extension-main-shell">
<section class="arch-panel-extension-summary-row" aria-label="Overview metrics">
${renderMetricCard("Total clients", formatWholeNumber(summary.totalCustomers), "Loaded from listSummary")}
${renderMetricCard("Total workloads", formatWholeNumber(summary.totalWorkloads), "Excluding Won workloads")}
${renderMetricCard("Total (ACR)", formatCurrency(summary.totalEligibleAcr), "Across eligible workloads")}
${renderMetricCard("Pending SRs", formatWholeNumber(summary.pendingServiceRequests.length), "SVC_PENDING_DELIVERY")}
</section>
<section class="arch-panel-extension-indicator-grid" aria-label="Opportunity indicators">
${summary.stageIndicators.map((indicator) => renderStageIndicator(indicator)).join("")}
</section>
<section class="arch-panel-extension-status-grid" aria-label="SR and action status indicators">
${renderBooleanIndicator(
"Status de SRs",
"Workloads com SR",
summary.srStatus.trueCount,
"Workloads sem SR",
summary.srStatus.falseCount,
"sr"
)}
${renderBooleanIndicator(
"Status de Consumption Plan/action",
"Workloads com action",
summary.actionStatus.trueCount,
"Workloads sem action",
summary.actionStatus.falseCount,
"action"
)}
</section>
${renderSrHoursAndCalendarSection(summary)}
</main>
`;
}
function renderCurrentUserSidebar(user) {
const displayName = cleanString(user?.name || user?.userEmail);
const userEmail = cleanString(user?.userEmail);
const avatar = cleanString(user?.avatar);
const initials = getUserInitials(displayName || userEmail);
return `
<div class="arch-panel-extension-current-user-card">
${
avatar
? `<img
class="arch-panel-extension-current-user-avatar"
src="${escapeHtml(avatar)}"
alt="${escapeHtml(displayName ? `${displayName} avatar` : "User avatar")}"
loading="lazy"
referrerpolicy="no-referrer"
>`
: `<span class="arch-panel-extension-current-user-avatar is-fallback" aria-hidden="true">${escapeHtml(initials)}</span>`
}
<div class="arch-panel-extension-sidebar-data">
<strong>${escapeHtml(displayName || userEmail)}</strong>
<span>${escapeHtml(userEmail)}</span>
</div>
</div>
`;
}
function getUserInitials(value) {
const parts = cleanString(value)
.split(/\s+/)
.filter(Boolean)
.slice(0, 2);
if (!parts.length) {
return "U";
}
return parts.map((part) => part.charAt(0).toUpperCase()).join("");
}
function getExtensionVersion() {
try {
return chrome.runtime.getManifest()?.version || "0.0.0";
} catch {
return "0.0.0";
}
}
function renderMetricCard(label, value, detail) {
return `
<article class="arch-panel-extension-summary-card">
<p class="arch-panel-extension-summary-label">${escapeHtml(label)}</p>
<p class="arch-panel-extension-summary-value">${escapeHtml(value)}</p>
<p class="arch-panel-extension-summary-detail">${escapeHtml(detail)}</p>
</article>
`;
}
function renderStageIndicator(indicator) {
return `
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="stage"
data-detail-value="${escapeHtml(indicator.label)}"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(indicator.label)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(indicator.count)}</p>
<p class="arch-panel-extension-summary-detail">Total (ACR): ${formatCurrency(indicator.totalAcr)}</p>
</button>
`;
}
function renderBooleanIndicator(
title,
trueLabel,
trueCount,
falseLabel,
falseCount,
scope
) {
const total = trueCount + falseCount;
const truePercent = total > 0 ? (trueCount / total) * 100 : 0;
const falsePercent = total > 0 ? (falseCount / total) * 100 : 0;
return `
<article class="arch-panel-extension-panel">
<header class="arch-panel-extension-panel-head">
<div>
<p class="arch-panel-extension-panel-label">${escapeHtml(title)}</p>
</div>
</header>
<div class="arch-panel-extension-progress">
<div class="arch-panel-extension-progress-track">
<span
class="arch-panel-extension-progress-segment is-success"
style="width: ${truePercent}%;"
></span>
<span
class="arch-panel-extension-progress-segment is-danger"
style="width: ${falsePercent}%;"
></span>
</div>
<div class="arch-panel-extension-progress-labels">
<span>${escapeHtml(trueLabel)}: ${formatPercent(truePercent)}</span>
<span>${escapeHtml(falseLabel)}: ${formatPercent(falsePercent)}</span>
</div>
</div>
<div class="arch-panel-extension-boolean-grid">
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="${escapeHtml(scope)}"
data-detail-value="true"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(trueLabel)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(trueCount)}</p>
<p class="arch-panel-extension-summary-detail">${formatPercent(truePercent)}</p>
</button>
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="${escapeHtml(scope)}"
data-detail-value="false"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(falseLabel)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(falseCount)}</p>
<p class="arch-panel-extension-summary-detail">${formatPercent(falsePercent)}</p>
</button>
</div>
</article>
`;
}
function renderSrHoursAndCalendarSection(summary) {
const calendar = buildWorkloadStartCalendar(
summary.allWorkloads,
appState.calendarYear,
appState.calendarMonth,
appState.calendarView,
appState.calendarAnchorDateKey
);
return `
<section class="arch-panel-extension-operations-grid" aria-label="Pending SRs and workload calendar">
${renderSrHoursIndicatorCard(summary)}
${renderWorkloadStartCalendar(calendar)}
</section>
`;
}
function renderSrHoursIndicatorCard(summary) {
const zeroToFourCount = summary.srHours.betweenZeroAndFour.length;
const aboveFourCount = summary.srHours.aboveFour.length;
const total = zeroToFourCount + aboveFourCount;
const zeroToFourPercent = total > 0 ? (zeroToFourCount / total) * 100 : 0;
const aboveFourPercent = total > 0 ? (aboveFourCount / total) * 100 : 0;
return `
<article class="arch-panel-extension-panel">
<header class="arch-panel-extension-panel-head">
<div>
<p class="arch-panel-extension-panel-label">Horas apontadas</p>
</div>
</header>
<div class="arch-panel-extension-progress">
<div class="arch-panel-extension-progress-track">
<span
class="arch-panel-extension-progress-segment is-success"
style="width: ${zeroToFourPercent}%;"
></span>
<span
class="arch-panel-extension-progress-segment is-danger"
style="width: ${aboveFourPercent}%;"
></span>
</div>
<div class="arch-panel-extension-progress-labels">
<span>SRs 0-4h: ${formatPercent(zeroToFourPercent)}</span>
<span>SRs &gt;4h: ${formatPercent(aboveFourPercent)}</span>
</div>
</div>
<div class="arch-panel-extension-boolean-grid">
${renderSrHoursIndicator(
"SRs 0-4h",
formatPercent(zeroToFourPercent),
zeroToFourCount,
"0-4"
)}
${renderSrHoursIndicator(
"SRs >4h",
formatPercent(aboveFourPercent),
aboveFourCount,
"above-4"
)}
</div>
</article>
`;
}
function renderSrHoursIndicator(label, detail, count, value) {
return `
<button
type="button"
class="arch-panel-extension-summary-card arch-panel-extension-summary-card-button"
data-action="open-detail"
data-detail-scope="sr-hours"
data-detail-value="${escapeHtml(value)}"
aria-label="Open ${escapeHtml(label)} service requests"
>
<p class="arch-panel-extension-summary-label">${escapeHtml(label)}</p>
<p class="arch-panel-extension-summary-value">${formatWholeNumber(count)}</p>
<p class="arch-panel-extension-summary-detail">${escapeHtml(detail)}</p>
</button>
`;
}
function renderWorkloadStartCalendar(calendar) {
return `
<article class="arch-panel-extension-panel arch-panel-extension-calendar-card">
<header class="arch-panel-extension-panel-head">
<div>
<p class="arch-panel-extension-panel-label">Workload start calendar</p>
<h3 class="arch-panel-extension-panel-title">${escapeHtml(calendar.periodLabel)}</h3>
</div>
<div class="arch-panel-extension-calendar-actions">
<div class="arch-panel-extension-calendar-view-toggle" aria-label="Calendar view">
${["month", "week"]
.map(
(view) => `
<button
type="button"
class="arch-panel-extension-calendar-view-button${
calendar.view === view ? " is-active" : ""
}"
data-action="calendar-view"
data-calendar-view="${view}"
aria-pressed="${calendar.view === view ? "true" : "false"}"
>
${view === "month" ? "Month" : "Week"}
</button>
`
)
.join("")}
</div>
<button
type="button"
class="arch-panel-extension-calendar-nav"
data-action="calendar-prev"
aria-label="${calendar.view === "week" ? "Previous week" : "Previous month"}"
>
</button>
<span>${formatWholeNumber(calendar.totalVisibleWorkloads)} starts</span>
<button
type="button"
class="arch-panel-extension-calendar-today"
data-action="calendar-today"
aria-label="${calendar.view === "week" ? "Go to current week" : "Go to current month"}"
>
Today
</button>
<button
type="button"
class="arch-panel-extension-calendar-nav"
data-action="calendar-next"
aria-label="${calendar.view === "week" ? "Next week" : "Next month"}"
>
</button>
</div>
</header>
<div class="arch-panel-extension-calendar-weekdays${
calendar.view === "week" ? " is-week" : ""
}">
${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
.map((day) => `<span>${day}</span>`)
.join("")}
</div>
<div class="arch-panel-extension-calendar-grid${
calendar.view === "week" ? " is-week" : ""
}">
${calendar.days
.map((day) => renderCalendarDay(day, calendar.view))
.join("")}
</div>
</article>
`;
}
function renderCalendarDay(day, view = "month") {
if (view === "week") {
return renderCalendarWeekDay(day);
}
const firstWorkload = day.workloads[0];
const calendarWorkloadLabel =
firstWorkload?.description || firstWorkload?.name || "Workload";
const className = [
"arch-panel-extension-calendar-day",
day.isCurrentMonth ? "" : "is-muted",
day.isToday ? "is-today" : "",
day.workloads.length ? "has-workload" : "",
]
.filter(Boolean)
.join(" ");
return `
<button
type="button"
class="${className}"
title="${escapeHtml(
day.workloads
.map((workload) => workload.description || workload.name)
.filter(Boolean)
.join(" | ")
)}"
data-action="open-detail"
data-detail-scope="calendar-day"
data-detail-value="${escapeHtml(day.dateKey)}"
${day.workloads.length ? "" : "disabled"}
>
<span class="arch-panel-extension-calendar-date">${day.dayNumber}</span>
${
day.isWeekStart
? `<span class="arch-panel-extension-calendar-week-number">${escapeHtml(
day.operationalWeekLabel
)}</span>`
: ""
}
${
day.workloads.length
? `<span class="arch-panel-extension-calendar-workload">${escapeHtml(
calendarWorkloadLabel
)}</span><span class="arch-panel-extension-calendar-count">${formatWholeNumber(
day.workloads.length
)}</span>`
: ""
}
</button>
`;
}
function renderCalendarWeekDay(day) {
const className = [
"arch-panel-extension-calendar-day",
"is-week-day",
day.isToday ? "is-today" : "",
day.workloads.length ? "has-workload" : "",
]
.filter(Boolean)
.join(" ");
const title = day.workloads
.map((workload) => workload.description || workload.name)
.filter(Boolean)
.join(" | ");
return `
<button
type="button"
class="${className}"
title="${escapeHtml(title)}"
data-action="open-detail"
data-detail-scope="calendar-day"
data-detail-value="${escapeHtml(day.dateKey)}"
${day.workloads.length ? "" : "disabled"}
>
<span class="arch-panel-extension-calendar-week-day-head">
<span class="arch-panel-extension-calendar-date">${day.dayNumber}</span>
${
day.isWeekStart
? `<span class="arch-panel-extension-calendar-week-number">${escapeHtml(
day.operationalWeekLabel
)}</span>`
: ""
}
</span>
<span class="arch-panel-extension-calendar-week-workloads">
${
day.workloads.length
? day.workloads.map((workload) => renderCalendarWeekWorkload(workload)).join("")
: `<span class="arch-panel-extension-calendar-week-empty">No starts</span>`
}
</span>
</button>
`;
}
function renderCalendarWeekWorkload(workload) {
const label = workload.description || workload.name || "Workload";
const customer = workload.customerName || "";
return `
<span class="arch-panel-extension-calendar-week-workload">
<span class="arch-panel-extension-calendar-week-workload-title">${escapeHtml(label)}</span>
${
customer
? `<span class="arch-panel-extension-calendar-week-workload-meta">${escapeHtml(
customer
)}</span>`
: ""
}
</span>
`;
}
function renderWorkloadTable(workloads) {
return `
<div class="arch-panel-extension-table-wrap is-main">
<table class="arch-panel-extension-table">
<colgroup>
<col class="arch-panel-extension-col-opportunity">
<col class="arch-panel-extension-col-customer">
<col class="arch-panel-extension-col-workload">
<col class="arch-panel-extension-col-acr">
<col class="arch-panel-extension-col-type">
<col class="arch-panel-extension-col-flag">
<col class="arch-panel-extension-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>
${workloads.map((workload) => renderWorkloadTableRow(workload)).join("")}
</tbody>
</table>
</div>
`;
}
function renderWorkloadTableRow(
workload,
includeConsumptionStartDate = false,
includeRampMonths = false
) {
const hasRampMismatch =
includeRampMonths && hasRampForecastMismatch(workload);
return `
<tr>
<td>${renderOpportunityLink(workload)}</td>
<td>${escapeHtml(workload.customerName || "")}</td>
<td>${renderWorkloadLink(workload, hasRampMismatch)}</td>
${
includeConsumptionStartDate
? `<td>${escapeHtml(formatDate(workload.consumptionStartDate) || "-")}</td>`
: ""
}
${
includeRampMonths
? `<td class="is-numeric">${renderRampMonthsButton(workload)}</td>`
: ""
}
<td class="is-numeric">${formatCurrency(workload.adjustedACR)}</td>
<td>${escapeHtml(workload.opportunityForecastTypeGroup)}</td>
<td>${renderBooleanBadge(workload.hasSR)}</td>
<td>${renderHasActionCell(workload)}</td>
</tr>
`;
}
function renderRampMonthsButton(workload) {
return `
<button
type="button"
class="arch-panel-extension-row-link arch-panel-extension-inline-link-button"
data-action="open-ramp-comparison"
data-workload-id="${escapeHtml(workload.workloadId || "")}"
aria-label="Abrir comparação de rampa para ${escapeHtml(
workload.name || "workload"
)}"
>
${formatWholeNumber(workload.rampMonths || 0)}
</button>
`;
}
function renderBooleanBadge(value) {
const isTrue = Boolean(value);
const variantClass = isTrue ? "is-success" : "is-danger";
const label = isTrue ? "Sim" : "Não";
return `
<span class="arch-panel-extension-badge ${variantClass}">
${label}
</span>
`;
}
function renderHasActionCell(workload) {
if (workload.hasAction) {
return renderBooleanBadge(true);
}
return `
<div class="arch-panel-extension-action-cell">
${renderBooleanBadge(false)}
<button
type="button"
class="arch-panel-extension-add-action-button"
data-action="open-action-form"
data-workload-id="${escapeHtml(workload.workloadId || "")}"
aria-label="Criar action para ${escapeHtml(workload.name || "workload")}"
title="Criar action"
>
</button>
</div>
`;
}
function renderInsightRow(label, value, detail) {
return `
<div class="arch-panel-extension-insight">
<div>
<p class="arch-panel-extension-insight-label">${escapeHtml(label)}</p>
<p class="arch-panel-extension-insight-detail">${escapeHtml(detail)}</p>
</div>
<p class="arch-panel-extension-insight-value">${escapeHtml(value)}</p>
</div>
`;
}
return {
renderMainContent,
renderWorkloadTableRow,
};
}
globalThis.ArchPanelDashboardView = Object.freeze({
create,
});
})();