Implementação da visualização semanal do calendário de workloads
This commit is contained in:
@@ -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 `
|
||||
<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 >4h: ${formatPercent(aboveFourPercent)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arch-panel-extension-boolean-grid">
|
||||
${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"
|
||||
)}
|
||||
</div>
|
||||
${renderWorkloadStartCalendar(calendar)}
|
||||
</section>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -990,14 +1047,33 @@
|
||||
<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.monthLabel)}</h3>
|
||||
<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="Previous month"
|
||||
aria-label="${calendar.view === "week" ? "Previous week" : "Previous month"}"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
@@ -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
|
||||
</button>
|
||||
@@ -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"}"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</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"]
|
||||
.map((day) => `<span>${day}</span>`)
|
||||
.join("")}
|
||||
</div>
|
||||
<div class="arch-panel-extension-calendar-grid">
|
||||
${calendar.days.map((day) => renderCalendarDay(day)).join("")}
|
||||
<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) {
|
||||
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 `
|
||||
<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">
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user