Implementação da visualização semanal do calendário de workloads

This commit is contained in:
Paulo Porto
2026-05-01 07:18:51 -03:00
parent 655d60b5ff
commit cc209d19cc
2 changed files with 413 additions and 32 deletions

View File

@@ -28,6 +28,7 @@
const STORAGE_KEYS = { const STORAGE_KEYS = {
theme: "arch-panel-extension-theme", theme: "arch-panel-extension-theme",
sidebarCollapsed: "arch-panel-extension-sidebar-collapsed", sidebarCollapsed: "arch-panel-extension-sidebar-collapsed",
calendarView: "arch-panel-extension-calendar-view",
}; };
const CACHE = { const CACHE = {
@@ -135,6 +136,8 @@
sidebarCollapsed: getStoredSidebarCollapsed(), sidebarCollapsed: getStoredSidebarCollapsed(),
calendarYear: new Date().getFullYear(), calendarYear: new Date().getFullYear(),
calendarMonth: new Date().getMonth(), calendarMonth: new Date().getMonth(),
calendarView: getStoredCalendarView(),
calendarAnchorDateKey: getLocalDateKey(new Date()),
status: "idle", status: "idle",
loadingMessage: "", loadingMessage: "",
loadingProgress: 0, loadingProgress: 0,
@@ -353,17 +356,26 @@
} }
if (action === "calendar-prev") { if (action === "calendar-prev") {
shiftCalendarMonth(-1); shiftCalendarPeriod(-1);
return; return;
} }
if (action === "calendar-next") { if (action === "calendar-next") {
shiftCalendarMonth(1); shiftCalendarPeriod(1);
return; return;
} }
if (action === "calendar-today") { if (action === "calendar-today") {
goToCurrentCalendarMonth(); goToCurrentCalendarPeriod();
return;
}
if (action === "calendar-view") {
const calendarView = actionElement.getAttribute("data-calendar-view");
if (calendarView) {
setCalendarView(calendarView);
}
return; return;
} }
@@ -607,6 +619,14 @@
return `[data-action="${action}"]`; return `[data-action="${action}"]`;
} }
if (action === "calendar-view") {
const calendarView = element.getAttribute("data-calendar-view");
return calendarView
? `[data-action="calendar-view"][data-calendar-view="${calendarView}"]`
: `[data-action="${action}"]`;
}
const detailScope = element.getAttribute("data-detail-scope"); const detailScope = element.getAttribute("data-detail-scope");
const detailValue = element.getAttribute("data-detail-value"); const detailValue = element.getAttribute("data-detail-value");
@@ -943,27 +963,64 @@
const calendar = buildWorkloadStartCalendar( const calendar = buildWorkloadStartCalendar(
summary.allWorkloads, summary.allWorkloads,
appState.calendarYear, appState.calendarYear,
appState.calendarMonth appState.calendarMonth,
appState.calendarView,
appState.calendarAnchorDateKey
); );
return ` return `
<section class="arch-panel-extension-operations-grid" aria-label="Pending SRs and workload calendar"> <section class="arch-panel-extension-operations-grid" aria-label="Pending SRs and workload calendar">
<div class="arch-panel-extension-sr-hour-grid"> ${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( ${renderSrHoursIndicator(
"SRs 0-4h", "SRs 0-4h",
"Horas apontadas entre 0 e 4", formatPercent(zeroToFourPercent),
summary.srHours.betweenZeroAndFour.length, zeroToFourCount,
"0-4" "0-4"
)} )}
${renderSrHoursIndicator( ${renderSrHoursIndicator(
"SRs >4h", "SRs >4h",
"Horas apontadas acima de 4", formatPercent(aboveFourPercent),
summary.srHours.aboveFour.length, aboveFourCount,
"above-4" "above-4"
)} )}
</div> </div>
${renderWorkloadStartCalendar(calendar)} </article>
</section>
`; `;
} }
@@ -990,14 +1047,33 @@
<header class="arch-panel-extension-panel-head"> <header class="arch-panel-extension-panel-head">
<div> <div>
<p class="arch-panel-extension-panel-label">Workload start calendar</p> <p class="arch-panel-extension-panel-label">Workload start calendar</p>
<h3 class="arch-panel-extension-panel-title">${escapeHtml(calendar.monthLabel)}</h3> <h3 class="arch-panel-extension-panel-title">${escapeHtml(calendar.periodLabel)}</h3>
</div> </div>
<div class="arch-panel-extension-calendar-actions"> <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 <button
type="button" type="button"
class="arch-panel-extension-calendar-nav" class="arch-panel-extension-calendar-nav"
data-action="calendar-prev" data-action="calendar-prev"
aria-label="Previous month" aria-label="${calendar.view === "week" ? "Previous week" : "Previous month"}"
> >
</button> </button>
@@ -1006,7 +1082,7 @@
type="button" type="button"
class="arch-panel-extension-calendar-today" class="arch-panel-extension-calendar-today"
data-action="calendar-today" data-action="calendar-today"
aria-label="Go to current month" aria-label="${calendar.view === "week" ? "Go to current week" : "Go to current month"}"
> >
Today Today
</button> </button>
@@ -1014,25 +1090,35 @@
type="button" type="button"
class="arch-panel-extension-calendar-nav" class="arch-panel-extension-calendar-nav"
data-action="calendar-next" data-action="calendar-next"
aria-label="Next month" aria-label="${calendar.view === "week" ? "Next week" : "Next month"}"
> >
</button> </button>
</div> </div>
</header> </header>
<div class="arch-panel-extension-calendar-weekdays"> <div class="arch-panel-extension-calendar-weekdays${
calendar.view === "week" ? " is-week" : ""
}">
${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] ${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
.map((day) => `<span>${day}</span>`) .map((day) => `<span>${day}</span>`)
.join("")} .join("")}
</div> </div>
<div class="arch-panel-extension-calendar-grid"> <div class="arch-panel-extension-calendar-grid${
${calendar.days.map((day) => renderCalendarDay(day)).join("")} calendar.view === "week" ? " is-week" : ""
}">
${calendar.days
.map((day) => renderCalendarDay(day, calendar.view))
.join("")}
</div> </div>
</article> </article>
`; `;
} }
function renderCalendarDay(day) { function renderCalendarDay(day, view = "month") {
if (view === "week") {
return renderCalendarWeekDay(day);
}
const firstWorkload = day.workloads[0]; const firstWorkload = day.workloads[0];
const calendarWorkloadLabel = const calendarWorkloadLabel =
firstWorkload?.description || firstWorkload?.name || "Workload"; firstWorkload?.description || firstWorkload?.name || "Workload";
@@ -1081,6 +1167,69 @@
`; `;
} }
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) { function renderWorkloadTable(workloads) {
return ` return `
<div class="arch-panel-extension-table-wrap is-main"> <div class="arch-panel-extension-table-wrap is-main">
@@ -3547,11 +3696,22 @@
}; };
} }
function buildWorkloadStartCalendar(workloads, year, month) { function buildWorkloadStartCalendar(
workloads,
year,
month,
view = "month",
anchorDateKey = ""
) {
const today = new Date(); const today = new Date();
const normalizedView = view === "week" ? "week" : "month";
const monthStart = new Date(year, month, 1); const monthStart = new Date(year, month, 1);
const calendarStart = new Date(monthStart); const anchorDate = getCalendarAnchorDate(year, month, anchorDateKey);
calendarStart.setDate(monthStart.getDate() - monthStart.getDay()); const calendarStart =
normalizedView === "week"
? getWeekStartDate(anchorDate)
: getWeekStartDate(monthStart);
const dayCount = normalizedView === "week" ? 7 : 42;
const workloadMap = new Map(); const workloadMap = new Map();
for (const workload of workloads || []) { for (const workload of workloads || []) {
@@ -3566,7 +3726,7 @@
workloadMap.set(dateKey, bucket); workloadMap.set(dateKey, bucket);
} }
const days = Array.from({ length: 42 }, (_, index) => { const days = Array.from({ length: dayCount }, (_, index) => {
const date = new Date(calendarStart); const date = new Date(calendarStart);
date.setDate(calendarStart.getDate() + index); date.setDate(calendarStart.getDate() + index);
const dateKey = getLocalDateKey(date); const dateKey = getLocalDateKey(date);
@@ -3579,17 +3739,22 @@
operationalWeekLabel: formatOperationalWeekLabel( operationalWeekLabel: formatOperationalWeekLabel(
getOperationalWeekInfo(getCalendarRowWeekDate(date)) getOperationalWeekInfo(getCalendarRowWeekDate(date))
), ),
isCurrentMonth: date.getMonth() === month, isCurrentMonth: normalizedView === "week" || date.getMonth() === month,
isToday: dateKey === getLocalDateKey(today), isToday: dateKey === getLocalDateKey(today),
workloads: workloadsForDay, workloads: workloadsForDay,
}; };
}); });
const weekEnd = new Date(calendarStart);
return { weekEnd.setDate(calendarStart.getDate() + 6);
monthLabel: new Intl.DateTimeFormat("en-US", { const monthLabel = new Intl.DateTimeFormat("en-US", {
month: "long", month: "long",
year: "numeric", year: "numeric",
}).format(monthStart), }).format(monthStart);
const weekLabel = `${formatDate(calendarStart)} - ${formatDate(weekEnd)}`;
return {
view: normalizedView,
periodLabel: normalizedView === "week" ? weekLabel : monthLabel,
days, days,
totalVisibleWorkloads: days.reduce( totalVisibleWorkloads: days.reduce(
(sum, day) => sum + day.workloads.length, (sum, day) => sum + day.workloads.length,
@@ -3598,6 +3763,24 @@
}; };
} }
function getCalendarAnchorDate(year, month, anchorDateKey) {
const anchorDate = parseDatePreservingDateOnly(anchorDateKey);
if (!Number.isNaN(anchorDate.getTime())) {
return anchorDate;
}
return new Date(year, month, 1);
}
function getWeekStartDate(value) {
const date = parseDatePreservingDateOnly(value);
const weekStart = Number.isNaN(date.getTime()) ? new Date() : new Date(date);
weekStart.setDate(weekStart.getDate() - weekStart.getDay());
return weekStart;
}
function openDetailModal(scope, value) { function openDetailModal(scope, value) {
if (!appState.dataset) { if (!appState.dataset) {
return; return;
@@ -3859,20 +4042,80 @@
appState.forecastUpdateConfirmModal = null; appState.forecastUpdateConfirmModal = null;
} }
function shiftCalendarMonth(delta) { function shiftCalendarPeriod(delta) {
const nextDate = new Date(appState.calendarYear, appState.calendarMonth + delta, 1); const nextDate =
appState.calendarView === "week"
? getShiftedCalendarWeekDate(delta)
: new Date(appState.calendarYear, appState.calendarMonth + delta, 1);
appState.calendarYear = nextDate.getFullYear(); appState.calendarYear = nextDate.getFullYear();
appState.calendarMonth = nextDate.getMonth(); appState.calendarMonth = nextDate.getMonth();
appState.calendarAnchorDateKey = getLocalDateKey(nextDate);
renderModal(); renderModal();
} }
function goToCurrentCalendarMonth() { function goToCurrentCalendarPeriod() {
const today = new Date(); const today = new Date();
appState.calendarYear = today.getFullYear(); appState.calendarYear = today.getFullYear();
appState.calendarMonth = today.getMonth(); appState.calendarMonth = today.getMonth();
appState.calendarAnchorDateKey = getLocalDateKey(today);
renderModal(); 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 getShiftedCalendarWeekDate(delta) {
const anchorDate = getCalendarAnchorDate(
appState.calendarYear,
appState.calendarMonth,
appState.calendarAnchorDateKey
);
anchorDate.setDate(anchorDate.getDate() + delta * 7);
return anchorDate;
}
function shiftCalendarMonth(delta) {
shiftCalendarPeriod(delta);
}
function goToCurrentCalendarMonth() {
goToCurrentCalendarPeriod();
}
async function submitActionForm(form) { async function submitActionForm(form) {
if (!appState.dataset || !appState.actionFormModal) { if (!appState.dataset || !appState.actionFormModal) {
return; return;
@@ -5459,6 +5702,43 @@
font-size: 12px; font-size: 12px;
} }
.arch-panel-extension-calendar-view-toggle {
display: inline-flex;
align-items: center;
overflow: hidden;
border: 1px solid var(--wb-border);
border-radius: 4px;
background: var(--wb-panel-soft);
}
.arch-panel-extension-calendar-view-button {
appearance: none;
min-height: 28px;
padding: 0 9px;
border: 0;
border-right: 1px solid var(--wb-border);
background: transparent;
color: var(--wb-text-secondary);
font: inherit;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
.arch-panel-extension-calendar-view-button:last-child {
border-right: 0;
}
.arch-panel-extension-calendar-view-button:hover {
background: var(--wb-row-hover);
color: var(--wb-text);
}
.arch-panel-extension-calendar-view-button.is-active {
background: #0b5cab;
color: #ffffff;
}
.arch-panel-extension-calendar-nav { .arch-panel-extension-calendar-nav {
appearance: none; appearance: none;
display: inline-flex; display: inline-flex;
@@ -5522,6 +5802,10 @@
gap: 6px; gap: 6px;
} }
.arch-panel-extension-calendar-grid.is-week {
min-height: 300px;
}
.arch-panel-extension-calendar-day { .arch-panel-extension-calendar-day {
appearance: none; appearance: none;
position: relative; position: relative;
@@ -5538,6 +5822,78 @@
cursor: pointer; cursor: pointer;
} }
.arch-panel-extension-calendar-day.is-week-day {
display: flex;
flex-direction: column;
min-height: 300px;
padding: 0;
overflow: hidden;
}
.arch-panel-extension-calendar-week-day-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 38px;
padding: 8px 9px;
border-bottom: 1px solid var(--wb-border);
background: color-mix(in srgb, var(--wb-panel) 72%, var(--wb-panel-soft));
}
.arch-panel-extension-calendar-day.is-week-day .arch-panel-extension-calendar-week-number {
position: static;
font-size: 9px;
opacity: 0.76;
}
.arch-panel-extension-calendar-week-workloads {
display: flex;
flex: 1;
flex-direction: column;
gap: 7px;
padding: 8px;
overflow: auto;
}
.arch-panel-extension-calendar-week-workload {
display: block;
border-left: 3px solid #d94f2b;
border-radius: 5px;
background: rgba(217, 79, 43, 0.14);
padding: 7px 8px;
color: var(--wb-text);
text-align: left;
}
.arch-panel-extension-calendar-week-workload-title,
.arch-panel-extension-calendar-week-workload-meta {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.arch-panel-extension-calendar-week-workload-title {
font-size: 11px;
font-weight: 700;
line-height: 1.25;
}
.arch-panel-extension-calendar-week-workload-meta {
margin-top: 3px;
color: var(--wb-text-secondary);
font-size: 10px;
line-height: 1.2;
white-space: nowrap;
}
.arch-panel-extension-calendar-week-empty {
margin: auto;
color: var(--wb-text-muted);
font-size: 11px;
font-weight: 600;
}
.arch-panel-extension-calendar-day:disabled { .arch-panel-extension-calendar-day:disabled {
cursor: default; cursor: default;
} }
@@ -6757,6 +7113,27 @@
} }
} }
function getStoredCalendarView() {
try {
return window.localStorage.getItem(STORAGE_KEYS.calendarView) === "week"
? "week"
: "month";
} catch {
return "month";
}
}
function setStoredCalendarView(value) {
try {
window.localStorage.setItem(
STORAGE_KEYS.calendarView,
value === "week" ? "week" : "month"
);
} catch {
// Ignore storage issues and keep the in-memory calendar view.
}
}
function normalizeText(value) { function normalizeText(value) {
return String(value || "").replace(/\s+/g, " ").trim(); return String(value || "").replace(/\s+/g, " ").trim();
} }

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Arch Panel Injector", "name": "Arch Panel Injector",
"version": "1.0.7", "version": "1.0.11",
"description": "Insere o botao Arch panel no header do Oracle Workload Workbench.", "description": "Insere o botao Arch panel no header do Oracle Workload Workbench.",
"permissions": [ "permissions": [
"cookies", "cookies",
@@ -44,3 +44,7 @@