diff --git a/content-script.js b/content-script.js index a6e9fdf..321ae48 100644 --- a/content-script.js +++ b/content-script.js @@ -28,6 +28,7 @@ const STORAGE_KEYS = { theme: "arch-panel-extension-theme", sidebarCollapsed: "arch-panel-extension-sidebar-collapsed", + calendarView: "arch-panel-extension-calendar-view", }; const CACHE = { @@ -135,6 +136,8 @@ sidebarCollapsed: getStoredSidebarCollapsed(), calendarYear: new Date().getFullYear(), calendarMonth: new Date().getMonth(), + calendarView: getStoredCalendarView(), + calendarAnchorDateKey: getLocalDateKey(new Date()), status: "idle", loadingMessage: "", loadingProgress: 0, @@ -353,17 +356,26 @@ } if (action === "calendar-prev") { - shiftCalendarMonth(-1); + shiftCalendarPeriod(-1); return; } if (action === "calendar-next") { - shiftCalendarMonth(1); + shiftCalendarPeriod(1); return; } if (action === "calendar-today") { - goToCurrentCalendarMonth(); + goToCurrentCalendarPeriod(); + return; + } + + if (action === "calendar-view") { + const calendarView = actionElement.getAttribute("data-calendar-view"); + + if (calendarView) { + setCalendarView(calendarView); + } return; } @@ -607,6 +619,14 @@ 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 detailValue = element.getAttribute("data-detail-value"); @@ -943,27 +963,64 @@ const calendar = buildWorkloadStartCalendar( summary.allWorkloads, appState.calendarYear, - appState.calendarMonth + appState.calendarMonth, + appState.calendarView, + appState.calendarAnchorDateKey ); return `
-
+ ${renderSrHoursIndicatorCard(summary)} + ${renderWorkloadStartCalendar(calendar)} +
+ `; + } + + 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 ` +
+
+
+

Horas apontadas

+
+
+
+
+ + +
+
+ SRs 0-4h: ${formatPercent(zeroToFourPercent)} + SRs >4h: ${formatPercent(aboveFourPercent)} +
+
+
${renderSrHoursIndicator( "SRs 0-4h", - "Horas apontadas entre 0 e 4", - summary.srHours.betweenZeroAndFour.length, + formatPercent(zeroToFourPercent), + zeroToFourCount, "0-4" )} ${renderSrHoursIndicator( "SRs >4h", - "Horas apontadas acima de 4", - summary.srHours.aboveFour.length, + formatPercent(aboveFourPercent), + aboveFourCount, "above-4" )}
- ${renderWorkloadStartCalendar(calendar)} - +
`; } @@ -990,14 +1047,33 @@

Workload start calendar

-

${escapeHtml(calendar.monthLabel)}

+

${escapeHtml(calendar.periodLabel)}

+
+ ${["month", "week"] + .map( + (view) => ` + + ` + ) + .join("")} +
@@ -1006,7 +1082,7 @@ type="button" class="arch-panel-extension-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 @@ -1014,25 +1090,35 @@ type="button" class="arch-panel-extension-calendar-nav" data-action="calendar-next" - aria-label="Next month" + aria-label="${calendar.view === "week" ? "Next week" : "Next month"}" > ›
-
+
${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] .map((day) => `${day}`) .join("")}
-
- ${calendar.days.map((day) => renderCalendarDay(day)).join("")} +
+ ${calendar.days + .map((day) => renderCalendarDay(day, calendar.view)) + .join("")}
`; } - function renderCalendarDay(day) { + function renderCalendarDay(day, view = "month") { + if (view === "week") { + return renderCalendarWeekDay(day); + } + const firstWorkload = day.workloads[0]; const calendarWorkloadLabel = 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 ` + + `; + } + + function renderCalendarWeekWorkload(workload) { + const label = workload.description || workload.name || "Workload"; + const customer = workload.customerName || ""; + + return ` + + ${escapeHtml(label)} + ${ + customer + ? `${escapeHtml( + customer + )}` + : "" + } + + `; + } + function renderWorkloadTable(workloads) { return `
@@ -3547,11 +3696,22 @@ }; } - function buildWorkloadStartCalendar(workloads, year, month) { + function buildWorkloadStartCalendar( + workloads, + year, + month, + view = "month", + anchorDateKey = "" + ) { const today = new Date(); + const normalizedView = view === "week" ? "week" : "month"; const monthStart = new Date(year, month, 1); - const calendarStart = new Date(monthStart); - calendarStart.setDate(monthStart.getDate() - monthStart.getDay()); + const anchorDate = getCalendarAnchorDate(year, month, anchorDateKey); + const calendarStart = + normalizedView === "week" + ? getWeekStartDate(anchorDate) + : getWeekStartDate(monthStart); + const dayCount = normalizedView === "week" ? 7 : 42; const workloadMap = new Map(); for (const workload of workloads || []) { @@ -3566,7 +3726,7 @@ workloadMap.set(dateKey, bucket); } - const days = Array.from({ length: 42 }, (_, index) => { + const days = Array.from({ length: dayCount }, (_, index) => { const date = new Date(calendarStart); date.setDate(calendarStart.getDate() + index); const dateKey = getLocalDateKey(date); @@ -3579,17 +3739,22 @@ operationalWeekLabel: formatOperationalWeekLabel( getOperationalWeekInfo(getCalendarRowWeekDate(date)) ), - isCurrentMonth: date.getMonth() === month, + isCurrentMonth: normalizedView === "week" || date.getMonth() === month, isToday: dateKey === getLocalDateKey(today), workloads: workloadsForDay, }; }); + const weekEnd = new Date(calendarStart); + weekEnd.setDate(calendarStart.getDate() + 6); + const monthLabel = new Intl.DateTimeFormat("en-US", { + month: "long", + year: "numeric", + }).format(monthStart); + const weekLabel = `${formatDate(calendarStart)} - ${formatDate(weekEnd)}`; return { - monthLabel: new Intl.DateTimeFormat("en-US", { - month: "long", - year: "numeric", - }).format(monthStart), + view: normalizedView, + periodLabel: normalizedView === "week" ? weekLabel : monthLabel, days, totalVisibleWorkloads: days.reduce( (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) { if (!appState.dataset) { return; @@ -3859,20 +4042,80 @@ appState.forecastUpdateConfirmModal = null; } - function shiftCalendarMonth(delta) { - const nextDate = new Date(appState.calendarYear, appState.calendarMonth + delta, 1); + 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 goToCurrentCalendarMonth() { + 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 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) { if (!appState.dataset || !appState.actionFormModal) { return; @@ -5459,6 +5702,43 @@ 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 { appearance: none; display: inline-flex; @@ -5522,6 +5802,10 @@ gap: 6px; } + .arch-panel-extension-calendar-grid.is-week { + min-height: 300px; + } + .arch-panel-extension-calendar-day { appearance: none; position: relative; @@ -5538,6 +5822,78 @@ 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 { 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) { return String(value || "").replace(/\s+/g, " ").trim(); } diff --git a/manifest.json b/manifest.json index 443dee8..5def2ab 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Arch Panel Injector", - "version": "1.0.7", + "version": "1.0.11", "description": "Insere o botao Arch panel no header do Oracle Workload Workbench.", "permissions": [ "cookies", @@ -44,3 +44,7 @@ + + + +