Files
arch-panel-extension-browser/src/views/dashboard-view.js

732 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
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">
<button
type="button"
class="arch-panel-extension-sidebar-toggle"
data-action="toggle-sidebar"
aria-label="${appState.sidebarCollapsed ? "Show sidebar" : "Hide sidebar"}"
aria-expanded="${appState.sidebarCollapsed ? "false" : "true"}"
title="${appState.sidebarCollapsed ? "Show sidebar" : "Hide sidebar"}"
>
<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">
${
appState.sidebarCollapsed
? `<path d="M7.22 15.78a.75.75 0 0 1 0-1.06L11.94 10 7.22 5.28a.75.75 0 0 1 1.06-1.06l5.25 5.25a.75.75 0 0 1 0 1.06l-5.25 5.25a.75.75 0 0 1-1.06 0Z"></path>`
: `<path d="M12.78 4.22a.75.75 0 0 1 0 1.06L8.06 10l4.72 4.72a.75.75 0 1 1-1.06 1.06l-5.25-5.25a.75.75 0 0 1 0-1.06l5.25-5.25a.75.75 0 0 1 1.06 0Z"></path>`
}
</svg>
</button>
<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,
});
})();