diff --git a/blocks/gmo-program-details/gmo-program-details.css b/blocks/gmo-program-details/gmo-program-details.css
index 801d8c9d..36a77d6f 100644
--- a/blocks/gmo-program-details/gmo-program-details.css
+++ b/blocks/gmo-program-details/gmo-program-details.css
@@ -102,6 +102,37 @@ body {
}
}
+.header-row3 .icon-release-tier {
+ width: 16px;
+ height: 16px;
+ background-image: url('/icons/launch.svg');
+ background-size: contain;
+ background-repeat: no-repeat;
+ margin-right: 8px;
+ margin-left: 16px;
+}
+
+.header-row3 .icon-productGroup {
+ width: 16px;
+ height: 16px;
+ background-image: url('/icons/folderOpenOutline.svg');
+ background-size: contain;
+ background-repeat: no-repeat;
+ margin-right: 8px;
+ margin-left: 16px;
+}
+
+.header-row3 .release-tier {
+ font: normal normal normal 14px/21px Adobe Clean;
+ margin-left: 8px;
+}
+
+.header-row3 .productGroup {
+ font: normal normal normal 14px/21px Adobe Clean;
+ margin-left: 8px;
+}
+
+
.details-header-wrapper .header-row3 {
display: flex;
align-items: center;
@@ -136,7 +167,7 @@ body {
justify-content: space-between;
flex-wrap: wrap;
align-content: center;
- width: 160px;
+ width: 240px;
height: 50px;
font: normal normal normal 14px/17px Adobe Clean;
border-bottom: 2px solid #EAEAEA;
@@ -156,7 +187,425 @@ body {
flex-direction: row;
justify-content: space-between;
}
+.tab.calendar {
+ margin-top:10px;
+ & > .no-data-msg {
+ height: 100px;
+ margin-top: 15px;
+ display: flex;
+ align-items: center;
+ }
+ & > .control-wrapper {
+ height: 32px;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ margin-bottom: 15px;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ & .inc-dec-wrapper {
+ font: normal normal normal 14px/17px Adobe Clean;
+ display: flex;
+ flex-direction: row;
+ width: 140px;
+
+ & .year-switch {
+ cursor: pointer;
+ display: flex;
+ flex-direction: row;
+ width: 64px;
+ box-shadow: 0px 1px 4px #00000026;
+ border-radius: 4px;
+ & .year-toggle {
+ display: flex;
+ justify-content: center;
+ align-content: center;
+ height: 32px;
+ width: 32px;
+ flex-wrap: wrap;
+ }
+ & img {
+ height: 15px;
+ &.left {
+ transform: rotate(180deg);
+ }
+ }
+ &.disabled {
+ background-color: #c6c6c6;
+ cursor: default;
+ }
+ }
+ & .current-year {
+ line-height: 32px;
+ margin-left: 20px;
+ &.single {
+ margin-left: unset;
+ }
+ }
+ }
+ & .right-controls {
+ width: 240px;
+ display: flex;
+ & .today-button {
+ width: 66px;
+ height: 32px;
+ font: normal normal normal 14px/32px Adobe Clean;
+ border-radius: 4px;
+ box-shadow: 0px 1px 4px #00000026;
+ display: flex;
+ justify-content: center;
+ margin-right: 5px;
+ cursor: pointer;
+ }
+ }
+ & .filter-dropdown-wrapper {
+ position: relative;
+ display: inline-block;
+ font: normal normal normal 14px/17px Adobe Clean;
+ letter-spacing: 0px;
+ height: 32px;
+ background-color: #FFF;
+ width: 70%;
+ border: 1px solid #D3D3D3;
+ border-radius: 4px;
+ cursor: pointer;
+ & > .filter-dropdown-button {
+ height: 32px;
+ line-height: 32px;
+ padding-left: 10px;
+ display: flex;
+ justify-content: space-between;
+ & > .label {
+ overflow: hidden;
+ }
+ & > .icon {
+ padding-top: 4px;
+ height: 16px;
+ }
+ }
+ & > .filter-dropdown-content {
+ display: none;
+ position: absolute;
+ background-color: #f9f9f9;
+
+ max-height: 200px;
+ overflow-y: auto;
+ border: 1px solid #ccc;
+ z-index: 2;
+ width: 100%;
+ }
+ &.active .filter-dropdown-content {
+ display: block;
+ }
+ & .filter-option, & .filter-label {
+ display: block;
+ overflow-x: hidden;
+
+ text-decoration: none;
+ color: #333;
+ }
+ & .filter-option {
+ padding: 10px 0px 10px 30px;
+ &:hover {
+ background-color: #ddd;
+ }
+ }
+ & .filter-label {
+ padding: 10px 0px 10px 10px;
+ font-weight: bold;
+ }
+ }
+ }
+ & > .calendar-wrapper {
+ overflow-y: auto;
+ overflow-x: hidden;
+ background-color: white;
+ font: normal normal bold 14px/21px Adobe Clean;
+ display: flex;
+ flex-direction: column;
+ border-bottom: 3px solid #F4F4F4;
+ position: relative;
+ min-height: 500px;
+ & .header-wrapper {
+ height: 24px;
+ & .quarter-header {
+ display: flex;
+ flex-direction: row;
+ height: 24px;
+ }
+ & .quarter {
+ width: 25%;
+ display: flex;
+ justify-content: center;
+ background-color: #F4F4F4;
+
+ &:not(:first-child) {
+ border-left: 3px solid white;
+ }
+ }
+ }
+ &.quarter-view, &.multiyear {
+ overflow-x: unset;
+ }
+ & .calendar-background {
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ &.quarter-view {
+ width: 200%;
+ }
+ }
+ & .month-wrapper, & .quarterview-wrapper {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ & .month {
+ width: 8.3%;
+ display: flex;
+ justify-content: flex-start;
+ flex-direction: column;
+ align-items: center;
+ border-left: 3px solid #F4F4F4;
+ height:100%;
+ &:last-child {
+ border-right: 3px solid #F4F4F4;
+ }
+ & .label {
+ padding: 3px;
+ margin-top: 1px;
+ }
+ &.current .label {
+ background-color: #96d7ff;
+ color: #2680EB;
+ border-radius: 15px;
+ padding-right: 15px;
+ padding-left: 15px;
+ z-index: 1;
+
+ }
+ & .calendar-indicator {
+ width: 1px;
+ height: 100%;
+ margin-top: -13%;
+ background: linear-gradient(
+ to bottom, #2680EB 50%, transparent 50%
+ );
+ background-size: 1px 5px;
+ }
+ }
+ }
+ & .calendar-content-wrapper {
+ position: relative;
+ padding-top: 65px;
+ padding-left: 5px;
+ width: 100%;
+ top: 0;
+ left: 0;
+ right: 0;
+ padding-bottom: 20px;
+ }
+ & .calendar-group {
+ width: 550px;
+ background-color: lightcyan;
+ border-radius: 7px;
+ &:not(:last-child) {
+ margin-bottom: 20px;
+ }
+ & .group-header {
+ position:relative;
+ height: 40px;
+ width: 100%;
+ color: white;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap; /* Prevent wrapping */
+ align-content: center;
+ justify-content: space-between;
+ border-radius: 7px 7px 0px 0px;
+ &.content-hidden {
+ border-radius: 7px;
+ }
+ & .left-block {
+ display: flex;
+ flex-direction: row;
+ align-items: center; /* Vertically center the items */
+ flex-wrap: nowrap; /* Prevent wrapping within left-block */
+ align-content: center;
+ border-radius: 7px 7px 0px 0px;
+ }
+ & .group-arrow {
+ transform: rotate(180deg);
+ filter: invert(99%) sepia(0%) saturate(0%) hue-rotate(227deg) brightness(113%) contrast(100%);
+ margin-right: 15px;
+ margin-top: 5px;
+ }
+ }
+ & .group-heading {
+ line-height: 24px;
+ height: 24px;
+ margin-left: 5px;
+ white-space: nowrap; /* Prevents the text from wrapping to a new line */
+ overflow: hidden; /* Hides the overflow text */
+ text-overflow: ellipsis; /* Adds an ellipsis (...) to indicate truncated text */
+ max-width: 100%; /* Adjusts the width to leave space for the group count */
+ display: inline-block; /* Ensures the element respects the max-width */
+ vertical-align: middle; /* Aligns it properly with other inline elements */
+ }
+ & .group-count {
+ position: absolute;
+ top: 50%; /* Align it vertically */
+ right: 10px; /* Align it horizontally to the right */
+ transform: translateY(-50%); /* Center the element vertically */
+ display: inline-flex;
+ align-items: center; /* Vertically center the content */
+ justify-content: center; /* Center the content horizontally */
+ min-width: 24px; /* Set minimum width instead of fixed width */
+ margin-left: 8px;
+ margin-right: 5px; /* Add margin if necessary to prevent overlapping */
+ height: 24px;
+ border-radius: 10px;
+ line-height: 24px;
+ background-color: black;
+ background: rgba(0,0,0,0.2);
+ }
+ & .group-controls {
+ margin-left: 15px;
+ }
+ & .group-collapse {
+ transform: rotate(90deg);
+ }
+ & .group-content {
+ display: flex;
+ flex-direction: column;
+
+ padding-left: 5px;
+ padding-right: 5px;
+ & .item {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ background-color: white;
+ margin-bottom: 10px;
+ border-radius: 4px;
+ height: 64px;
+ min-width: 190px;
+ font-size: 12px;
+ &:first-child {
+ margin-top: 10px;
+ }
+ & .content-row {
+ height: 32px;
+ line-height: 32px;
+ &.bottom {
+ display: flex;
+ line-height: unset;
+ align-items: center;
+ & div:not(last-child) {
+ margin-right: 4px;
+ }
+ }
+ }
+ & .start-date, & .link {
+ border-radius: 5px;
+ border: 1px solid #a9a9a9;
+ height: 12px;
+ padding: 4px;
+ }
+ & .color-tab {
+ border-radius: 4px 0px 0px 4px;
+ background-color: blue;
+ width: 3px;
+ height: 100%;
+ }
+ & > .item-content {
+ display: flex;
+ flex-direction: column;
+ width: 96%;
+ line-height: 12px;
+ margin-left: 2px;
+ }
+ & .name {
+ max-width: 62%;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ text-wrap: nowrap;
+ }
+ }
+ & .info {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ }
+ & .thumbnail {
+ height: 32px;
+ width: 32px;
+ background-color: lightgray;
+ border-radius: 4px;
+ margin-right: 5px;
+ overflow: hidden;
+ }
+ & .link {
+ margin-right: 10px;
+ display: flex;
+ & > img {
+ margin-top: 1px;
+ margin-left: 5px;
+ transform: rotate(180deg);
+ }
+ }
+ & .item-status {
+ height: 5px;
+ width: 5px;
+ background-color: green;
+ border-radius: 50%;
+ position: relative;
+ margin-top: 13px;
+ margin-left: 10px;
+ }
+ }
+ &.color1 {
+ background-color: #33AB840D;
+ border: 1px solid #12805C;
+ & .group-header {
+ background-color: #33AB84;
+ }
+ }
+ &.color2 {
+ background-color: #37c8f029;
+ border: 1px solid #2680EB;
+ & .group-header {
+ background-color: #378EF0;
+ }
+ }
+ &.color3 {
+ background-color: #cf3edc2d;
+ border: 1px solid #A228AD;
+ & .group-header {
+ background-color: #CF3EDC;
+ }
+ }
+ &.color4 {
+ background-color: #9c64e126;
+ border: 1px solid #7A42BF;
+ & .group-header {
+ background-color: #9D64E1;
+ }
+ }
+ group5 {
+ background-color: #168a4a;
+ border: 1px solid #24a25d;
+ & .group-header {
+ background-color: #137941;
+ }
+ }
+ }
+ }
+}
.overview-wrapper {
display: flex;
flex-direction: column;
@@ -177,6 +626,7 @@ body {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
+ line-clamp: 3;
overflow: hidden;
}
}
@@ -213,7 +663,7 @@ body {
& .tags-wrapper {
display: flex;
- flex-wrap: wrap; /* Ensure tags wrap properly */
+ flex-wrap: wrap;
margin-top: 10px;
}
@@ -222,12 +672,12 @@ body {
color: #505050;
border: 1px solid #D3D3D3;
border-radius: 4px;
- padding: 0 10px; /* Ensure no padding at the bottom */
+ padding: 0 10px;
line-height: 32px;
- height: 32px; /* Ensure consistent height */
+ height: 32px;
display: flex;
- align-items: center; /* Vertically center the text */
- margin-bottom: 5px; /* Add margin at the bottom */
+ align-items: center;
+ margin-bottom: 5px;
&:not(:last-child) {
margin-right: 5px;
}
@@ -341,32 +791,32 @@ body {
.total-assets-tooltip .tooltiptext {
visibility: hidden;
- white-space: nowrap; /* Ensure text appears on one line */
- background-color: #D3D3D3; /* Match the background color */
- color: black; /* Match the text color */
- text-align: left; /* Left-align the text */
+ white-space: nowrap;
+ background-color: #D3D3D3;
+ color: black;
+ text-align: left;
border-radius: 4px;
- padding: 5px 10px; /* Adjust padding as needed */
+ padding: 5px 10px;
position: absolute;
z-index: 1;
- top: 50%; /* Center vertically */
- left: 100%; /* Position to the right of the element */
- transform: translateY(-50%); /* Center vertically */
- margin-left: 10px; /* Adjust margin to match the screenshot */
+ top: 50%;
+ left: 100%;
+ transform: translateY(-50%);
+ margin-left: 10px;
opacity: 0;
transition: opacity 0.3s;
- box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); /* Add shadow for better visibility */
+ box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
}
.total-assets-tooltip .tooltiptext::after {
content: "";
position: absolute;
- top: 50%; /* Center the arrow vertically */
- left: -5px; /* Adjust to position the arrow */
- transform: translateY(-50%); /* Center vertically */
+ top: 50%;
+ left: -5px;
+ transform: translateY(-50%);
border-width: 5px;
border-style: solid;
- border-color: transparent #D3D3D3 transparent transparent; /* Arrow color matches tooltip background */
+ border-color: transparent #D3D3D3 transparent transparent;
}
.total-assets-tooltip:hover .tooltiptext {
@@ -414,11 +864,11 @@ body {
&.datarow {
display: flex;
align-items: center;
- height: auto; /* Set to auto to accommodate varying heights */
+ height: auto;
font-weight: 200;
& .property {
- line-height: normal; /* Adjust line-height to normal */
- padding: 10px 0; /* Add padding for better spacing */
+ line-height: normal;
+ padding: 10px 0;
}
&:first-child, &:nth-child(2) {
border-top: 2px solid #F4F4F4;
@@ -588,6 +1038,7 @@ body {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
+ line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
}
diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js
index 738f03b0..36c5f29a 100644
--- a/blocks/gmo-program-details/gmo-program-details.js
+++ b/blocks/gmo-program-details/gmo-program-details.js
@@ -10,14 +10,24 @@ const programName = queryVars.programName;
const programID = queryVars.programID;
const deliverableMappings = getMappingArray('deliverableType');
const platformMappings = getMappingArray('platforms');
+const taskStatusMappings = getMappingArray('taskStatus');
+const startDateProp = 'taskPlannedStartDate';
+const endDateProp = 'taskPlannedEndDate';
+let viewStart, viewEnd, calendarDeliverables;
+
+// Thumbnail cache array object to store the image objects using cacheKey = `${programName}-${campaignName}-${deliverableType}`;
+const thumbnailCache = {};
export default async function decorate(block) {
const encodedSemi = encodeURIComponent(';');
const encodedProgram = encodeURIComponent(programName);
+ const encodedPath = queryVars.path ? `${encodeURIComponent(queryVars.path)}` : '';
blockConfig = readBlockConfig(block);
+ // Including path in the query if present
+ const programQueryString = `getProgramDetails${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}` +
+ (encodedPath ? `${encodedSemi}path=${encodedPath}` : '');
- const programQueryString = `getProgramDetails${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}`;
const deliverableQueryString = `getProgramDeliverables${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}`;
// Immediately render a placeholder header
@@ -88,12 +98,14 @@ export default async function decorate(block) {
const audiences = buildAudienceList(program).outerHTML;
const artifactLinks = buildArtifactLinks(program).outerHTML;
+ const currentYear = new Date().getFullYear();
+
// Inject the additional HTML content
block.querySelector('.main-body-wrapper').innerHTML += `
Overview
Deliverables
-
Calendar
+
Calendar
@@ -177,6 +189,32 @@ export default async function decorate(block) {
+
+
+
+
+
+
+
+
+
+
+
+
${currentYear}
+
+
+
+
+
`;
// Wait for deliverables data
@@ -227,7 +265,7 @@ export default async function decorate(block) {
});
decorateIcons(block);
-
+ buildCalendar(await deliverables, block, "year", await deliverableMappings);
}
function enableBackBtn(block, blockConfig) {
@@ -260,6 +298,18 @@ function buildHeader(program, queryVars) {
driverField=buildDriverField(driver).outerHTML;
}
+ const releaseTier = `
+ `;
+
+ const productGroup = `
+ `;
+
headerWrapper.innerHTML = `
@@ -271,6 +321,8 @@ function buildHeader(program, queryVars) {
`
@@ -659,22 +711,673 @@ function attachListener(htmlElement) {
function extractQueryVars() {
const urlStr = window.location.href;
- const pnRegex = /.*programName=(.*?)&programID=(.*)/;
+ const pnRegex = /[?&]programName=([^&]+)&programID=([^&]+)(&path=([^&]+))?/;
const match = urlStr.match(pnRegex);
if (match && match[1] && match[2]) {
- const pName = decodeURIComponent(match[1]);
- let pID = decodeURIComponent(match[2])
+ const pName = decodeURIComponent(match[1]); // Removed the replace method
+ let pID = decodeURIComponent(match[2]);
+ let pPath = match[4] ? decodeURIComponent(match[4]) : null;
if (pID.endsWith('#')) {
pID = pID.slice(0, -1);
}
return {
programName: pName,
- programID: pID
- }
+ programID: pID,
+ path: pPath
+ };
} else {
return {
programName: 'Program Name Not Available',
- programID: 'Program ID Not Available'
+ programID: 'Program ID Not Available',
+ path: null
+ };
+ }
+}
+
+// program calendar view
+async function buildCalendar(dataObj, block, type, mappingArray, period) {
+ if (!calendarDeliverables) calendarDeliverables = dataObj.data.deliverableList.items;
+ if (!deliverableMappings) deliverableMappings = await mappingArray;
+ if (!taskStatusMappings) taskStatusMappings = await taskStatusMappings;
+
+ const programLaunch = document.querySelector('span.campaign-date').textContent;
+ const programLaunchDate = new Date(programLaunch);
+
+ // multiple of 3 for width of column when viewing in quarter mode. can change this
+ // by adjusting the multiple below, and also the % width of the calendar background
+ // when drawing a 'quarter' calendar.
+
+ const columnWidth = (type === "year") ? 8.315 : (8.315 * 3);
+ const currentDate = new Date();
+ const currentYear = currentDate.getFullYear();
+
+ // if there are no deliverables, display msg to user and end construction.
+ if (calendarDeliverables.length === 0) {
+ const calendarTab = document.querySelector('.calendar.tab');
+ calendarTab.innerHTML = `
+ Required Data is Unavailable for this view.
+ `;
+ return;
+ }
+
+ // get start of the view
+ viewStart = getTimeBounds(calendarDeliverables, "start", startDateProp);
+ viewStart = (!(isValidDate(viewStart)) || viewStart <= 0) ? programLaunchDate : viewStart;
+ const viewStartYear = viewStart.getUTCFullYear();
+
+ const displayYear = period ? period.year : viewStartYear;
+ const displayQuarter = period ? period.quarter : 1;
+
+ const yearIndicator = block.querySelector('.inc-dec-wrapper .current-year');
+ yearIndicator.dataset.year = displayYear;
+ yearIndicator.textContent = displayYear;
+
+ // get end of the view
+ viewEnd = getTimeBounds(calendarDeliverables, "end", endDateProp);
+ if (!(isValidDate(viewEnd)) || viewEnd <= 0) {
+ viewEnd = new Date(viewStart);
+ viewEnd.setMonth(viewStart.getMonth() + 1);
+ }
+ const viewEndYear = viewEnd.getUTCFullYear();
+
+ // get array of all years to be included
+ let years = calendarYears(viewStartYear, viewEndYear);
+
+ // disable increment/decrement if only one year in view
+ if (years.length === 1) {
+ document.querySelector('.inc-dec-wrapper > .year-switch').classList.add('disabled');
+ }
+
+ // build the calendar background here as we already know the period and style
+ let calendarEl;
+ if (type === "year") {
+ calendarEl = buildYearCal(years);
+ } else {
+ // if the last item ends in December we need another year to catch the Q1 n+1 finish
+ if (viewEnd.getUTCMonth = 11) {
+ years.push(viewEndYear + 1);
+ }
+ calendarEl = buildQuarterCal(years);
+ }
+ // get unique deliverable types
+ const uniqueGroups = getUniqueItems(calendarDeliverables, "deliverableType");
+
+ // set up the content wrapper
+ const contentWrapper = document.createElement('div');
+ contentWrapper.classList.add('calendar-content-wrapper');
+ if (type === "quarter") {
+ contentWrapper.classList.add('quarter-view');
+ contentWrapper.dataset.view = "quarter";
+ } else {
+ contentWrapper.dataset.view = "year";
+ }
+
+ var groupIndex = 1;
+ for (const group of uniqueGroups) {
+ const groupType = await lookupType(group, 'deliverable-type');
+ // find all members of this group
+ const matchedItems = calendarDeliverables.filter(item => item.deliverableType === group);
+
+ // find the earliest date- this is how we set the position for the group against the calendar
+ let earliestStartDate = getTimeBounds(matchedItems, "start", startDateProp);
+ earliestStartDate = (!(isValidDate(earliestStartDate)) || earliestStartDate <= 0) ? new Date(viewStart) : earliestStartDate;
+ let latestEndDate = getTimeBounds(matchedItems, "end", endDateProp);
+ latestEndDate = (!(isValidDate(latestEndDate)) || latestEndDate <= 0) ? new Date(viewEnd) : latestEndDate;
+ const startMonth = (earliestStartDate.getUTCMonth()); // getMonth returns 0-11 but this is desirable
+ const startDay = (earliestStartDate.getUTCDate() - 1); // if at start of month, we don't want to add any more margin
+ const endMonth = (latestEndDate.getUTCMonth());
+ const endDay = (latestEndDate.getUTCDate() - 1);
+
+ const groupStartYear = earliestStartDate.getUTCFullYear();
+ const groupEndYear = latestEndDate.getUTCFullYear();
+
+ // accounting for different years in view
+ const startYearOffset = ((groupStartYear - viewStartYear) * 12);
+ const endYearOffset = ((groupEndYear - viewStartYear) * 12);
+ const totalDaysInMonth = new Date(Date.UTC(groupStartYear, startMonth, 0)).getUTCDate();
+ const totalDaysInEndMonth = new Date(Date.UTC(groupEndYear, endMonth, 0)).getUTCDate();
+
+ const percentOfStartMonth = (startDay / totalDaysInMonth);
+ const percentOfEndMonth = (endDay / totalDaysInEndMonth);
+ const dayMargin = (percentOfStartMonth * columnWidth);
+ const endDayMargin = (percentOfEndMonth * columnWidth);
+ let startPosition = (((startYearOffset + startMonth) * columnWidth) + dayMargin).toFixed(2);
+ let endPosition = (((endYearOffset + endMonth) * columnWidth) + endDayMargin).toFixed(2);
+
+ if (type === "quarter") {
+ startPosition = parseFloat(startPosition) + parseFloat(columnWidth);
+ endPosition = parseFloat(endPosition) + parseFloat(columnWidth);
+ }
+
+ if (endMonth > 9) endPosition = endPosition - 0.35;
+ const widthOfGroup = (endPosition - startPosition); // width of group = start position + (day duration)
+ // calculate the duration of the group as that helps set the width of its members
+ const groupDuration = Math.floor((latestEndDate.getTime() - earliestStartDate.getTime()) / (1000 * 60 * 60 * 24));
+
+ const itemWrapper = document.createElement('div');
+ itemWrapper.classList.add('group-content');
+
+ for (const item of matchedItems) {
+ const itemStartDate = (item[startDateProp]) ? new Date(item[startDateProp]) : viewStart;
+ const itemEndDate = (item[endDateProp]) ? new Date(item[endDateProp]) : viewEnd;
+
+ const itemEndDateStr = itemEndDate ? itemEndDate.toLocaleDateString().split(',')[0] : null;
+ const itemDuration = Math.floor((itemEndDate.getTime() - itemStartDate.getTime()) / (1000 * 60 * 60 * 24));
+ const itemDurationPct = ((itemDuration / groupDuration) * 100).toFixed(2);
+
+ let daysDifference = Math.floor((itemStartDate.getTime() - earliestStartDate.getTime()) / (1000 * 60 * 60 * 24));
+ const startPctDiff = ((daysDifference / groupDuration) * 100).toFixed(2);
+ let itemEl = document.createElement('div');
+ itemEl.classList.add('item');
+ itemEl.style.marginLeft = startPctDiff + '%';
+
+ // Find the corresponding color code from the taskStatusMappings array
+ const itemStatusMapping = await getTaskStatusMapping(item.taskStatus);
+ const { text: statusText = 'Unknown Status', 'color-code': colorCode = 'green' } = itemStatusMapping;
+
+ // Create a placeholder for the thumbnail
+ itemEl.innerHTML = `
+
+
+
+
+
+
${item.deliverableName}
+
+
+
+
+
+ ${itemEndDateStr ? '
End Date: ' + itemEndDateStr + '
' : ''}
+
+
+
+ `;
+ itemEl.style.width = itemDurationPct + '%';
+
+ // Call the new function to fetch and add the thumbnail, ensuring sequential execution
+ await addThumbnailToItem(itemEl, item.programName, item.campaignName,item.deliverableType);
+ itemWrapper.appendChild(itemEl);
+
+ };
+
+ //await lookupType(category, 'deliverable-type');
+ const groupEl = document.createElement('div');
+ groupEl.classList.add('calendar-group', `color${groupIndex}`);
+ groupEl.style.marginLeft = startPosition + '%';
+ groupEl.style.width = widthOfGroup + '%';
+ groupEl.innerHTML = `
+
+ `;
+ groupEl.appendChild(itemWrapper);
+ groupEl.querySelectorAll('.group-controls').forEach((arrow) => {
+ arrow.addEventListener('click', showHideGroup);
+ });
+
+ contentWrapper.appendChild(groupEl);
+ groupIndex +=1;
+
+ };
+
+ calendarEl.appendChild(contentWrapper);
+ block.querySelector('.calendar.tab').appendChild(calendarEl);
+
+ // populate "filter" dropdown
+ const filterDropdown = document.createElement('div');
+ filterDropdown.classList.add('filter-dropdown-content');
+ const uniqueYears = getUniqueYears(calendarDeliverables);
+ const yearOptionLabel = document.createElement('div');
+ yearOptionLabel.classList.add('filter-label');
+ yearOptionLabel.textContent = 'Year';
+ const quarterOptionLabel = document.createElement('div');
+ quarterOptionLabel.classList.add('filter-label');
+ quarterOptionLabel.textContent = 'Quarter';
+ filterDropdown.appendChild(yearOptionLabel);
+
+ // when choosing 'Quarter' the top left controls change to control the quarter in focus
+ // its kind of a zoomed in view.
+ uniqueYears.forEach((year) => {
+ const yearOption = document.createElement('div');
+ yearOption.classList.add('filter-option');
+ yearOption.dataset.year = year;
+ yearOption.textContent = year;
+ yearOption.addEventListener('click', (event) => filterDropdownSelection(event, viewStartYear, years.length));
+ filterDropdown.appendChild(yearOption);
+ });
+ filterDropdown.appendChild(quarterOptionLabel);
+ const quarters = [ 1, 2, 3, 4 ];
+ quarters.forEach((quarter) => {
+ const quarterOption = document.createElement('div');
+ quarterOption.classList.add('filter-option');
+ quarterOption.dataset.period = quarter;
+ quarterOption.textContent = quarter;
+ //quarterOption.addEventListener('click', filterDropdownSelection);
+ quarterOption.addEventListener('click', (event) => filterDropdownSelection(event, viewStartYear, years.length));
+ filterDropdown.appendChild(quarterOption);
+ })
+
+ const filterDropdownWrapper = block.querySelector('.filter-dropdown-wrapper');
+ filterDropdownWrapper.appendChild(filterDropdown);
+ filterDropdownWrapper.querySelector('.filter-dropdown-button').addEventListener('click', (event) => toggleDropdown(event.target));
+
+ // scroll to the right
+ const calendarWrapper = document.querySelector('.calendar-wrapper')
+ const scrollPct = calculateScroll(type, viewStartYear, displayYear, displayQuarter, years.length);
+ document.addEventListener('DOMContentLoaded', scrollOnInit(calendarWrapper, scrollPct));
+
+ // indicator that shows current day/month
+ const currentMonth = currentDate.getMonth() + 1;
+ // calculate the percentage completion of the current month for the indicator offset
+ const totalDaysInMonth = new Date((new Date(currentYear, currentMonth, 1)) - 1).getDate();
+ const percentOfMonth = (currentDate.getUTCDate() / totalDaysInMonth).toFixed(2) * 100;
+ const monthEl = block.querySelector(`.month-wrapper[data-year='${currentYear}'] .month[data-num='${currentMonth}']`);
+ monthEl.classList.add('current');
+ const lineEl = document.createElement('div');
+ lineEl.classList.add('calendar-indicator');
+ // use direct style for offset
+ lineEl.style.marginRight = ((-2 * percentOfMonth) + 100) + '%';
+ monthEl.appendChild(lineEl);
+
+ // close dropdown listener for clicks outside open dropdown
+ document.querySelector('.gmo-program-details.block').addEventListener('click', dismissDropdown);
+ block.querySelectorAll('.year-switch > .year-toggle').forEach((control) => {
+ control.removeEventListener('click', changePeriod);
+ if (years.length > 1) {
+ control.addEventListener('click', changePeriod);
+ }
+ });
+ block.querySelector('.right-controls .today-button').addEventListener('click', () => {
+ const calendarWrapper = document.querySelector('.calendar-wrapper')
+ // determine scroll pct for years. always use type = year because "today" doesn't care about quarters.
+ const yearScrollPct = calculateScroll("year", viewStartYear, currentYear, displayQuarter, years.length);
+ // determine how far through the current year we are
+ const now = new Date();
+ now.setHours(0,0,0,0);
+ const dateDiff = Math.floor((now - new Date(now.getFullYear(), 0, 0)) / (1000 * 60 * 60 * 24));
+ const dayPct = (((dateDiff) * ((1 / years.length) / 365)) * 100);
+ // get total scroll percent by adding year + day scroll pct
+ const totalScroll = (parseFloat(yearScrollPct) + parseFloat(dayPct)).toFixed(2);
+ // scroll to position
+ scrollToPosition(calendarWrapper, totalScroll);
+ });
+}
+
+// calendar view supporting functions
+
+// Helper function to get task status mapping
+async function getTaskStatusMapping(taskStatus) {
+ const taskStatusArray = await taskStatusMappings;
+ return taskStatusArray.find(mapping => mapping.value === taskStatus) || {};
+}
+
+async function addThumbnailToItem(itemEl, programName, campaignName, deliverableType) {
+ // Create a unique key for the cache based on the parameters, only add the campaignName in cacheKey when it is not null or empty
+ const cacheKey = campaignName ? `${programName}-${campaignName}-${deliverableType}` : `${programName}-${deliverableType}`;
+
+ // Check if the imageObject is already cached
+ let imageObject = thumbnailCache[cacheKey];
+
+ // If not cached, make the API call and store the result in the cache
+ if (!imageObject) {
+ try {
+ imageObject = await searchAsset(programName, campaignName, deliverableType);
+ thumbnailCache[cacheKey] = imageObject; // Store the result in the cache
+ } catch (error) {
+ console.error("Failed to load thumbnail image:", error);
+ return; // Exit the function if the API call fails
}
}
+
+ // Use the cached or newly fetched imageObject
+ if (imageObject && imageObject.imageUrl) {
+ const thumbnailDiv = itemEl.querySelector('.thumbnail');
+ const imgElement = document.createElement('img');
+ imgElement.src = imageObject.imageUrl;
+ imgElement.alt = imageObject.imageAltText;
+ imgElement.loading = 'lazy';
+ thumbnailDiv.appendChild(imgElement);
+ } else {
+ console.error("Image Object does not have a valid imageUrl");
+ }
+}
+
+function getTimeBounds(items, whichEnd, property) {
+ const desiredDate = items.reduce((dateCompare, currentItem) => {
+ const currentItemDate = new Date(currentItem[property]); // Ensure UTC
+ if (whichEnd === "start") {
+ return currentItemDate < dateCompare ? currentItemDate : dateCompare;
+ } else {
+ return currentItemDate > dateCompare ? currentItemDate : dateCompare;
+ }
+ }, new Date(items[0][property]));
+ return desiredDate;
+}
+
+function showHideGroup(event) {
+ const arrow = event.target;
+ const group = arrow.closest('.calendar-group');
+ group.querySelector('.group-content').classList.toggle('inactive');
+ group.querySelector('.group-expand').classList.toggle('inactive');
+ group.querySelector('.group-collapse').classList.toggle('inactive');
+ group.querySelector('.group-header').classList.toggle('content-hidden')
+}
+
+function changePeriod(event) {
+ const arrow = event.target;
+ const direction = arrow.dataset.direction;
+ const wrapper = arrow.closest('.inc-dec-wrapper');
+ const yearEl = wrapper.querySelector('.current-year');
+ const contentWrapper = document.querySelector('.calendar-content-wrapper');
+ const view = contentWrapper.dataset.view;
+ const currentYear = parseInt(yearEl.dataset.year);
+ const maxYear = viewEnd.getUTCFullYear(); // Use the maximum year from the viewEnd variable
+ const minYear = viewStart.getUTCFullYear(); // Use the minimum year from the viewStart variable
+
+ let newPeriod, newYear, newQuarter;
+
+ if (view === "quarter") {
+ const currentQuarter = parseInt(yearEl.dataset.quarter);
+ newQuarter = (direction == 'right') ? (currentQuarter + 1) : (currentQuarter - 1);
+ newYear = currentYear;
+ if (newQuarter > 4) {
+ newQuarter = 1;
+ newYear = currentYear + 1;
+ }
+ if (newQuarter < 1) {
+ newQuarter = 4;
+ newYear = currentYear - 1;
+ }
+ } else {
+ newYear = (direction == 'right') ? (currentYear + 1) : (currentYear - 1);
+ newQuarter = 1;
+ }
+
+ // Prevent the year from going beyond the maximum or minimum year
+ if (newYear > maxYear || newYear < minYear) {
+ return;
+ }
+
+ newPeriod = { 'year': newYear, 'quarter': newQuarter };
+ refreshCalendar(newPeriod, view);
+}
+
+function getUniqueYears(items) {
+ const yearsSet = new Set();
+ items.forEach(item => {
+ const startDate = item[startDateProp];
+ if (startDate) {
+ const year = startDate.split('-')[0];
+ yearsSet.add(year);
+ }
+ });
+ if (yearsSet.size === 0) {
+ const startYear = viewStart.getFullYear();
+ const endYear = viewEnd.getFullYear();
+ for (let year = startYear; year <= endYear; year++) {
+ yearsSet.add(year);
+ }
+ }
+ const years = Array.from(yearsSet);
+ years.sort((a, b) => parseInt(a) - parseInt(b));
+ return years;
+}
+
+// handle clicking on the year button
+function toggleDropdown(element) {
+ const dropdown = element.closest('.filter-dropdown-wrapper');
+ const iconChevronDown = dropdown.querySelector('.icon-chevronDown');
+ const iconChevronUp = dropdown.querySelector('.icon-chevronUp');
+
+ iconChevronDown.classList.toggle('inactive');
+ iconChevronUp.classList.toggle('inactive');
+ dropdown.classList.toggle('active');
+}
+
+// handle clicks outside the dropdown
+function dismissDropdown(event) {
+ const isInsideDropdown = event ? event.target.closest('.filter-dropdown-wrapper') : false;
+ if (!isInsideDropdown) {
+ const dropdown = document.querySelector('.filter-dropdown-wrapper');
+ dropdown.querySelector('.icon-chevronDown').classList.remove('inactive');
+ dropdown.querySelector('.icon-chevronUp').classList.add('inactive');
+ dropdown.classList.remove('active');
+ }
+}
+
+function filterDropdownSelection(event, viewStartYear, numYears) {
+ // if we're hopping views, redraw the calendar
+ // if not just scroll
+ const calendarContentEl = document.querySelector('.calendar-content-wrapper');
+ const currentView = calendarContentEl.dataset.view;
+
+ const optionEl = event.target;
+ let year, quarter, view;
+ if (("period") in optionEl.dataset) {
+ // quarter view
+ quarter = optionEl.dataset.period;
+ year = document.querySelector('.inc-dec-wrapper .current-year').dataset.year;
+ view = "quarter";
+ } else {
+ // year view
+ view = "year";
+ year = optionEl.dataset.year;
+ quarter = 1;
+ }
+
+ const period = { 'year': year, 'quarter': quarter }
+
+ if (currentView === view) {
+ // scroll over
+ const scrollPct = calculateScroll(view, viewStartYear, year, quarter, numYears);
+ const calendarWrapper = document.querySelector('.calendar-wrapper');
+ scrollToPosition(calendarWrapper, scrollPct);
+ } else {
+ const viewStr = view.charAt(0).toUpperCase() + view.slice(1);
+ document.querySelector('.filter-dropdown-button > .label').textContent = `Selected View: ${viewStr}`;
+ refreshCalendar(period, view);
+ }
+ dismissDropdown();
+}
+
+// retrieve the year via js when refreshing in quarter view
+function refreshCalendar(period, view) {
+ const block = document.querySelector('.gmo-program-details.block');
+ const yearEl = block.querySelector('.inc-dec-wrapper .current-year');
+ yearEl.dataset.year = period.year;
+ yearEl.dataset.quarter = period.quarter;
+
+ if (view === "year") {
+ yearEl.textContent = period.year;
+ } else {
+ yearEl.textContent = `Q${period.quarter} ${period.year}`;
+ }
+
+
+ // trick to remove event listeners
+ block.querySelector('.filter-dropdown-wrapper').outerHTML += '';
+ block.querySelector('.right-controls .today-button').outerHTML += '';
+
+ block.querySelector('.calendar-wrapper').remove();
+ block.querySelector('.filter-dropdown-content').remove();
+
+ buildCalendar(calendarDeliverables, block, view, deliverableMappings, period);
+}
+
+function calendarYears(startYear, endYear) {
+ let years = [];
+ for (let year = startYear; year <= endYear; year++) {
+ years.push(year);
+ }
+ return years;
+}
+
+function buildYearCal(years) {
+ const calendarEl = document.createElement('div');
+ calendarEl.classList.add('calendar-wrapper');
+ if (years.length > 1) calendarEl.classList.add('multiyear');
+ const backgroundEl = document.createElement('div');
+ backgroundEl.classList.add('calendar-background');
+ backgroundEl.style.width = (years.length * 100) + '%';
+
+ years.forEach((year) => {
+ const yearWrapper = document.createElement('div');
+ yearWrapper.dataset.year = year;
+ yearWrapper.classList.add('year-wrapper');
+ yearWrapper.style.width = (100 / years.length) + '%';
+ const calendarHeader = document.createElement('div');
+ calendarHeader.classList.add('header-wrapper');
+ const quartersHeader = document.createElement('div');
+ quartersHeader.classList.add('quarter-header');
+ quartersHeader.innerHTML = `
+ Q1 ${year}
+ Q2 ${year}
+ Q3 ${year}
+ Q4 ${year}
+ `;
+ calendarHeader.appendChild(quartersHeader);
+
+ const monthsWrapper = document.createElement('div');
+ monthsWrapper.classList.add('month-wrapper');
+ monthsWrapper.dataset.year = year;
+ monthsWrapper.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ yearWrapper.appendChild(calendarHeader);
+ yearWrapper.appendChild(monthsWrapper);
+ backgroundEl.appendChild(yearWrapper);
+ });
+ calendarEl.appendChild(backgroundEl);
+ return calendarEl;
+}
+
+
+function buildQuarterCal(years) {
+ const calendarEl = document.createElement('div');
+ calendarEl.classList.add('calendar-wrapper', 'quarter-view');
+ if (years.length > 1) calendarEl.classList.add('multiyear');
+ const backgroundEl = document.createElement('div');
+ backgroundEl.classList.add('calendar-background');
+ // this is wider for 'quarter' view- see equivalent in 'year' view.
+ backgroundEl.style.width = (years.length * 300) + '%';
+
+ years.forEach((year) => {
+ const yearWrapper = document.createElement('div');
+ yearWrapper.dataset.year = year;
+ yearWrapper.classList.add('year-wrapper');
+ yearWrapper.style.width = (100 / years.length) + '%';
+ const calendarHeader = document.createElement('div');
+ calendarHeader.classList.add('header-wrapper');
+ const quartersHeader = document.createElement('div');
+ quartersHeader.classList.add('quarter-header');
+ quartersHeader.innerHTML = `
+ Fiscal Q1 ${year}
+ Fiscal Q2 ${year}
+ Fiscal Q3 ${year}
+ Fiscal Q4 ${year}
+ `;
+ calendarHeader.appendChild(quartersHeader);
+
+ const monthsWrapper = document.createElement('div');
+ monthsWrapper.classList.add('month-wrapper');
+ monthsWrapper.dataset.year = year;
+ monthsWrapper.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ yearWrapper.appendChild(calendarHeader);
+ yearWrapper.appendChild(monthsWrapper);
+ backgroundEl.appendChild(yearWrapper);
+ });
+ calendarEl.appendChild(backgroundEl);
+ return calendarEl;
+}
+
+function scrollOnInit(element, scrollPct) {
+ // Observer to detect when the element becomes visible
+ const observer = new IntersectionObserver((entries, observer) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ resizeGroups();
+ scrollToPosition(element, scrollPct);
+ observer.disconnect(); // Stop observing after scrolling
+ }
+ });
+ });
+
+ // Start observing the element
+ observer.observe(element);
+}
+
+function getHorizontalOverflow(element) {
+ return element.scrollWidth - element.clientWidth;
+}
+
+function resizeGroups() {
+ const groups = document.querySelectorAll('.calendar-group');
+ groups.forEach((group) => {
+ const overflow = getHorizontalOverflow(group);
+ if (overflow) {
+ group.querySelector('.group-header').style.paddingRight = overflow + 'px';
+ group.style.paddingRight = overflow + 'px';
+ }
+ })
+}
+
+function scrollToPosition(element, scrollPct) {
+ const maxScrollLeft = element.scrollWidth;
+ const scrollAmt = (maxScrollLeft) * (scrollPct / 100);
+ element.scrollTo({
+ left: scrollAmt, // Replace with desired position
+ behavior: 'smooth' // Optional: for smooth scrolling
+ });
+}
+
+function calculateScroll(type, viewStartYear, displayYear, displayQuarter, numYears) {
+ const yearDiff = displayYear - viewStartYear;
+ const yearWidthOffsetPct = (((yearDiff / numYears)) * 100);
+
+ if (type === "quarter") {
+ return ((yearWidthOffsetPct) + ((displayQuarter - 1) * ((1 / numYears) / 4)) * 100).toFixed(2);
+ } else {
+ return (yearWidthOffsetPct).toFixed(2);
+ }
+}
+
+function isValidDate(dateObj) {
+ return dateObj instanceof Date && !isNaN(dateObj);
}
diff --git a/blocks/gmo-program-list/gmo-program-list.js b/blocks/gmo-program-list/gmo-program-list.js
index 927093f4..7a9bd3cf 100644
--- a/blocks/gmo-program-list/gmo-program-list.js
+++ b/blocks/gmo-program-list/gmo-program-list.js
@@ -181,6 +181,7 @@ async function buildCampaignList(campaigns, numPerPage) {
const programName = campaign.node.programName;
const campaignName = campaign.node.campaignName;
const programID = campaign.node.programID ? campaign.node.programID : "";
+ const path = campaign.node._path;
campaignRow.classList.add('campaign-row');
if ((index + 1) > numPerPage) campaignRow.classList.add('hidden');
@@ -190,9 +191,9 @@ async function buildCampaignList(campaigns, numPerPage) {
const campaignIconLink = document.createElement('a');
let campaignDetailsLink = host + `/${detailsPage}?programName=${programName}&`;
- campaignDetailsLink += `programID=${programID}`
+ campaignDetailsLink += `programID=${programID}`;
+ campaignDetailsLink += `&path=${path}`;
campaignIconLink.href = campaignDetailsLink;
-
const campaignIcon = document.createElement('div');
campaignIcon.classList.add('campaign-icon');
campaignIcon.dataset.programname = programName;
@@ -203,7 +204,6 @@ async function buildCampaignList(campaigns, numPerPage) {
const campaignNameWrapper = document.createElement('div');
campaignNameWrapper.classList.add('campaign-name-wrapper', 'vertical-center');
-
campaignNameWrapper.innerHTML = `
${checkBlankString(programName)}
diff --git a/icons/folderOpenOutline.svg b/icons/folderOpenOutline.svg
new file mode 100644
index 00000000..ff67e983
--- /dev/null
+++ b/icons/folderOpenOutline.svg
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/icons/launch.svg b/icons/launch.svg
new file mode 100644
index 00000000..e6209ba5
--- /dev/null
+++ b/icons/launch.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/scripts/graphql.js b/scripts/graphql.js
index f2e5ca5d..58012272 100644
--- a/scripts/graphql.js
+++ b/scripts/graphql.js
@@ -47,6 +47,8 @@ export async function graphqlAllCampaignsFilter(first,cursor,filter) {
const encodedCursor = encodeURIComponent(cursor);
const encodedFilter = encodeURIComponent(JSON.stringify(filter));
const graphqlEndpoint = `${baseApiUrl}/${projectId}/${queryName}${encodedSemiColon}first=${encodedFirst}${encodedSemiColon}cursor=${encodedCursor}${encodedSemiColon}filter=${encodedFilter}`;
+ //Performance logging
+ const startTime = performance.now();
const jwtToken = await getBearerToken();
try {
@@ -57,6 +59,11 @@ export async function graphqlAllCampaignsFilter(first,cursor,filter) {
},
};
const response = await fetch(`${graphqlEndpoint}`, options);
+ //Performance logging
+ const endTime = performance.now();
+ const executionTime = endTime - startTime;
+ console.debug(`getAllCampaigns Execution Time: ${executionTime} ms`);
+
// Handle response codes
if (response.status === 200) {
const responseBody = await response.json();
@@ -170,6 +177,8 @@ export async function executeQuery(queryString) {
const baseApiUrl = `${await getGraphqlEndpoint()}/graphql/execute.json`;
const projectId = 'gmo';
const queryEndpoint = `${baseApiUrl}/${projectId}/${queryString}`;
+ //Performance logging
+ const startTime = performance.now();
const jwtToken = await getBearerToken();
return fetch(queryEndpoint, {
@@ -181,6 +190,11 @@ export async function executeQuery(queryString) {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
+ //Performance logging
+ const endTime = performance.now();
+ const executionTime = endTime - startTime;
+ console.debug(`executeQuery for ${queryString} Execution Time: ${executionTime} ms`);
+
return response.json();
}).then(data => {
return data; // Make sure to return the data so that the promise resolves with it