diff --git a/blocks/gmo-program-details/gmo-program-details.css b/blocks/gmo-program-details/gmo-program-details.css index 5c69faa..9e6737d 100644 --- a/blocks/gmo-program-details/gmo-program-details.css +++ b/blocks/gmo-program-details/gmo-program-details.css @@ -29,6 +29,28 @@ body { } } +.export-asset-count-button{ + background-color: #FFFFFF; + font: normal normal normal 15px/18px Adobe Clean; + height: auto; + width: auto; + display: flex; + padding: 5px 10px 5px 10px; + align-items: center; + border: 1px solid #D3D3D3; + border-radius: 4px; + cursor: pointer; + &:hover { + background-color: #c5c8c9; + } + & > .icon { + width: 15px; + margin-left: 5px; + margin-right: 8px; + } + +} + .gmo-program-details-wrapper { margin-top: 50px; min-height: 400px; @@ -195,6 +217,11 @@ body { display: flex; flex-direction: row; justify-content: space-between; + position: relative; + & .export-asset-count-button{ + position:absolute; + right: 450px; + } } .tab.calendar { margin-top:10px; @@ -646,6 +673,9 @@ body { color: #505050; cursor: pointer; } + .collection-block { + width: fit-content; + } } .kpis-wrapper { @@ -690,7 +720,13 @@ body { margin-bottom: 5px; &:not(:last-child) { margin-right: 5px; + cursor: pointer; } + &.selected{ + background-color: #0096FF; + color: white; + } + } } @@ -771,26 +807,38 @@ body { } } } - .links-wrapper { + .links-wrapper, .collections-wrapper { display: flex; flex-direction: column; max-width: 90%; margin-right: 100px; margin-top: unset; - & .links { + .links, .collections { display: flex; justify-content: space-between; margin-top: 10px; } - & .campaign-link { + .campaign-link, .collection-link { font: normal normal normal 14px/21px Adobe Clean; letter-spacing: 0px; color: #0D66D0; + &:not(:last-child) { margin-right: 30px; } } } + .collections-wrapper { + margin-left: 100px; + } + .collection-link-wrapper { + display: flex; + flex-direction: column; + .collection-link { + display: block; + white-space: nowrap; + } + } } .total-assets-tooltip { @@ -849,6 +897,9 @@ body { position: sticky; position: -webkit-sticky; top: 0; + & > .showhide-deliverables { + margin-left: 10px; + } } } @@ -1005,11 +1056,25 @@ body { width: 200px; } +.header.column1 { + margin-left: 16px; +} + .column3 { width: 140px; } -.column4, .column5, .column6 { +.column4 { + width: 110px; + margin-right: 40px; +} + +.column5 { + width: 410px; + margin-right: 40px; +} + +.column6 { width: 110px; } @@ -1034,7 +1099,7 @@ body { .table-column { &:not(:last-child) { - margin-right: 85px; + margin-right: 45px; } } @@ -1052,3 +1117,20 @@ body { overflow: hidden; text-overflow: ellipsis; } + +.selected { + background-color: #0096FF; + .scope{ + color: white; + font-weight: bold; /* Optional: to make the text bold */ + } + +} + +.clear-selection { + font-weight: normal; + color: #0096FF; + cursor: pointer; + margin-left: 10px; +} + diff --git a/blocks/gmo-program-details/gmo-program-details.js b/blocks/gmo-program-details/gmo-program-details.js index 42efbb9..414c972 100644 --- a/blocks/gmo-program-details/gmo-program-details.js +++ b/blocks/gmo-program-details/gmo-program-details.js @@ -6,7 +6,7 @@ import { filterArray, getProductMapping, checkBlankString, dateFormat, statusMapping, getMappingArray, showLoadingOverlay, hideLoadingOverlay, div, - span, img + span, img, a } from '../../scripts/shared-program.js'; @@ -58,13 +58,20 @@ export default async function decorate(block) { ), ); + // export asset count button + const exportAssetCountButton = div( + { class: 'export-asset-count-button'}, + img({ class: 'icon icon-download' , 'data-direction': 'left', src: '/icons/download-button.svg'}), + span({ class: 'button-label'}, 'Export Asset Count'), + ); + // tab wrapper const tabWrapper = div( { class: 'tab-wrapper'}, div({ id: 'tab1toggle', class: 'tabBtn active', 'data-target': 'tab1'}, 'Overview'), div({ id: 'tab2toggle', class: 'tabBtn', 'data-target': 'tab2'}, 'Deliverables'), div({ id: 'tab3toggle', class: 'tabBtn', 'data-target': 'tab3'}, 'Calendar'), - ); + ); // overview tab const overviewTab = div( @@ -105,14 +112,19 @@ export default async function decorate(block) { div( { id: 'deliverable-type', class: 'channel-scope-wrapper'}, span({ class: 'h3'}, 'Deliverable Type'), + div({ class: 'description'}, 'Select a deliverable type to display platform the assets were created for.'), div({ class: 'tags-wrapper'}), ), div( { id: 'platforms', class: 'channel-scope-wrapper'}, span({ class: 'h3'}, 'Platforms'), + div({ class: 'description'}, 'Select a platform to display deliverable type the assets were created for.'), div({ class: 'tags-wrapper'}), ), ), + div( + exportAssetCountButton, + ), div( { class: 'infocards-wrapper'}, div( @@ -122,11 +134,13 @@ export default async function decorate(block) { div( { class: 'card audiences'}, div({ class: 'card-heading h3'}, 'Audiences'), - ) + ), ), ); + // deliverables tab + const expandCollapseTooltip = 'Expand/Collapse All Deliverable Tasks'; const deliverablesTab = div( { id: 'tab2', class: 'deliverables tab inactive'}, div( @@ -145,11 +159,13 @@ export default async function decorate(block) { { class: 'table-wrapper'}, div( { class: 'table-header' }, + img({ class: 'expand-deliverables showhide-deliverables', src: "/icons/AddCircle_18_N.svg", title: expandCollapseTooltip }), + img({ class: 'collapse-deliverables showhide-deliverables inactive', src: "/icons/RemoveCircle_18_N.svg", title: expandCollapseTooltip }), div({ class: 'header table-column column1' }, 'Deliverable Task Name'), div({ class: 'header table-column column2' }, 'Deliverable Type'), div({ class: 'header table-column column3' }, 'Platforms'), div({ class: 'header table-column column4' }, 'QA Files'), - div({ class: 'header table-column column5' }, 'Final Asset'), + div({ class: 'header table-column column5' }, 'Approved Collection Link(s)'), div({ class: 'header table-column column7' }, 'Status Update'), div({ class: 'header table-column column8' }, 'Completion Date'), div({ class: 'header table-column column9' }, 'Task Owner'), @@ -232,12 +248,17 @@ async function addProgramStats(block) { (encodedPath ? `${encodedSemi}path=${encodedPath}` : ''); const programData = await executeQuery(programQueryString); const program = programData.data.programList.items[0]; + const uniqueDeliverableTypes = getUniqueItems(programData.data.deliverableList.items, 'deliverableType'); const uniquePlatforms = getUniqueItems(programData.data.deliverableList.items, 'platforms'); const bodyWrapper = document.querySelector('.main-body-wrapper'); // for deliverable list const deliverableQueryString = `getProgramDeliverables${encodedSemi}programName=${encodedProgram}${encodedSemi}programID=${encodeURIComponent(programID)}`; + let imageTest = {imageUrl : '', imageAltText: '', assetCount: 0}; + imageTest = await searchAsset(programName, programName.campaignName, 'email', 'appier'); + console.log('Image Test:', imageTest); + // for thumbnails let imageObject = {imageUrl : '', imageAltText: '', assetCount: 0}; @@ -295,6 +316,7 @@ async function addProgramStats(block) { const targetMarketAreas = buildTargetMarketAreaList(p0TargetMarketArea,p1TargetMarketArea); const audiences = buildAudienceList(program); const artifactLinks = buildArtifactLinks(program); + const collections = buildProgramCollections(program); document.querySelector('.kpis-wrapper').appendChild(kpis); document.querySelector('.market-wrapper').appendChild(targetMarketAreas); @@ -309,13 +331,122 @@ async function addProgramStats(block) { // additional dom updates buildProductCard(program); - buildFieldScopes('deliverable-type', uniqueDeliverableTypes, block); - buildFieldScopes('platforms', uniquePlatforms, block); // deliverables tab const deliverables = executeQuery(deliverableQueryString); document.querySelector('.page-heading').appendChild(artifactLinks); document.querySelector('.total-assets > .description').textContent = totalassets; + if (collections) document.querySelector('.deliverables > .page-heading').appendChild(collections); + + //Create a map to store the associations + const deliverableTypeToPlatformsMap = new Map(); + // Create a map to store the associations + const platformToDeliverableTypesMap = new Map(); + + // Populate the map + programData.data.deliverableList.items.forEach(item => { + const deliverableType = item.deliverableType; //array of deliverableType + const platforms = item.platforms; // array of platform strings + // If deliverableType or platforms is null, skip the iteration + if (deliverableType === null || deliverableType === undefined || platforms === null || platforms === undefined) { + return; + } + //if deliverableType or Platforms has same value, show both buttons + if (deliverableType.value === 0 || platforms.value === 0) { + return; + } + platforms.forEach(async platform => { + let assetResult = {imageUrl : '', imageAltText: '', assetCount: 0}; + assetResult = await searchAsset(programName, programName.campaignName, deliverableType, platform); + let assetCount = assetResult.assetCount; + + // deliverableType, platform, assetCount + if (!deliverableTypeToPlatformsMap.has(deliverableType)) { + // Case: If the deliverable type is not in the map, create a new entry with the platform added to the new map. + const deliverableTypesPlatformAssetCountMap = new Map(); + deliverableTypesPlatformAssetCountMap.set(platform, assetCount); + // Create a new entry for each deliverable type with the new map. + deliverableTypeToPlatformsMap.set(deliverableType, deliverableTypesPlatformAssetCountMap); + } else { + // Case: If the deliverable type is in the map, add the platform to the existing set. + const deliverableTypesPlatformAssetCountMap = deliverableTypeToPlatformsMap.get(deliverableType); + deliverableTypesPlatformAssetCountMap.set(platform, assetCount); + deliverableTypeToPlatformsMap.set(deliverableType, deliverableTypesPlatformAssetCountMap); + } + + if (!platformToDeliverableTypesMap.has(platform)) { + // Case: If the platform is not in the map, create a new entry with the deliverableType added to the new map. + const platformdeliverableTypesAssetCountMap = new Map(); + platformdeliverableTypesAssetCountMap.set(deliverableType, assetCount); + // Create a new entry for each platform with the new map. + platformToDeliverableTypesMap.set(platform, platformdeliverableTypesAssetCountMap); + } else { + // Case: If the platform is in the map, add the deliverableType to the existing set. + const platformdeliverableTypesAssetCountMap = platformToDeliverableTypesMap.get(platform); + platformdeliverableTypesAssetCountMap.set(deliverableType, assetCount); + platformToDeliverableTypesMap.set(platform, platformdeliverableTypesAssetCountMap); + } + }); + + }); + + // Attach event listener to the button + const exportAssetCountButton = document.querySelector('.export-asset-count-button'); + exportAssetCountButton.addEventListener('click', () => { + createCSV(deliverableTypeToPlatformsMap); + }); + + function createCSV(deliverableTypeToPlatformsMap) { + // Define the CSV header + let header = `Program name: ${program.programName}\n\n`; + let csvContent = `data:text/csv;charset=utf-8,${header}Deliverable Type,Platforms (by Deliverable Type),Deliverable Types - Asset Counts,Platform - Asset Counts\n`; + let grandTotalAssetCount = 0; + + // Iterate over the deliverableTypeToPlatformsMap to extract data + deliverableTypeToPlatformsMap.forEach((platformsMap, deliverableType) => { + let totalAssetCount = 0; + + platformsMap.forEach(assetCount => { + totalAssetCount += assetCount; + }); + + // Add the deliverable type row + csvContent += `${deliverableType},All platforms,${totalAssetCount},\n`; + + // Add the platform rows + platformsMap.forEach((assetCount, platform) => { + csvContent += `,${platform},,${assetCount}\n`; + }); + + // Add to grand total + grandTotalAssetCount += totalAssetCount; + }); + + // add a blank row + csvContent += '\n'; + + // Add the grand total asset count row and bold the text + csvContent += `Total Unique Asset Count,,${grandTotalAssetCount},\n`; + + // Encode the CSV content + const encodedUri = encodeURI(csvContent); + + // Create a link element to download the CSV file + const link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", "marketing-dashboard-hcv-count.csv"); + + // Append the link to the document body and trigger the download + document.body.appendChild(link); + link.click(); + + // Remove the link from the document + document.body.removeChild(link); + } + + buildProductCard(program); + buildFieldScopes('deliverable-type', uniqueDeliverableTypes, block, deliverableTypeToPlatformsMap); + buildFieldScopes('platforms', uniquePlatforms, block, platformToDeliverableTypesMap); const table = buildTable(await deliverables).then(async (rows) => { return rows; @@ -345,6 +476,18 @@ async function addProgramStats(block) { switchTab(event.target); }); + // enable expand/collapse all deliverables + block.querySelectorAll('.expand-deliverables, .collapse-deliverables').forEach((button) => { + button.addEventListener('click', (event) => { + const clickedBtn = event.currentTarget; + document.querySelector('.showhide-deliverables.inactive').classList.toggle('inactive'); + clickedBtn.classList.toggle('inactive'); + + const expand = clickedBtn.classList.contains('expand-deliverables'); + document.querySelectorAll('.row.collapsible').forEach((group) => toggleGroup(group, expand)); + }); + }) + // decorate any new icons decorateIcons(block); @@ -352,6 +495,21 @@ async function addProgramStats(block) { hideLoadingOverlay(bodyWrapper); } +function toggleGroup(group, expand) { + if (expand) { + group.querySelector('.icon-next').classList.add('inactive'); + group.querySelector('.icon-collapse').classList.remove('inactive'); + } else { + group.querySelector('.icon-next').classList.remove('inactive'); + group.querySelector('.icon-collapse').classList.add('inactive'); + } + Array.from(group.children).forEach((child) => { + if (child.classList.contains('row')) { + child.classList.toggle('inactive', !expand); + } + }); +}; + function enableBackBtn(block, blockConfig) { block.querySelector('.back-button').addEventListener('click', () => { const host = location.origin + getBaseConfigPath(); @@ -360,6 +518,27 @@ function enableBackBtn(block, blockConfig) { }) } +function buildProgramCollections(program) { + const programCollections = program.programLevelcollectionLink; + if (programCollections) { + const collectionsElem = div( + { id: 'collections-wrapper', class: 'collections-wrapper' }, + div({ class: 'h3' }, 'Program Collection'), + ); + const collectionsLinksWrapper = div({ class: 'collections' }); + + programCollections.forEach((collection) => { + const collectionData = parseCollectionLink(collection); + const collectionLink = a({ class: 'collection-link', href: collectionData.link, target: '_blank' }, collectionData.name); + collectionsLinksWrapper.appendChild(collectionLink); + }); + collectionsElem.appendChild(collectionsLinksWrapper); + return collectionsElem; + } else { + return null; + } +} + function buildDriverField(driverName) { const driverSpan = document.createElement('span'); driverSpan.classList.add('driver-text'); @@ -452,7 +631,7 @@ function switchTab(tab) { } -async function buildFieldScopes(scopeTypeId, scopes, block) { +async function buildFieldScopes(scopeTypeId, scopes, block, associationMap) { if (scopes.length == 0) { block.querySelector(`#${scopeTypeId}.channel-scope-wrapper`).classList.add('inactive'); return; @@ -460,12 +639,115 @@ async function buildFieldScopes(scopeTypeId, scopes, block) { const scopesParent = block.querySelector(`#${scopeTypeId}.channel-scope-wrapper .tags-wrapper`); scopes.forEach(async (scope) => { if (scope == null || scope == undefined || scope == '') return; - const tag = document.createElement('div'); + const tag = document.createElement('button'); tag.classList.add('scope-tag'); tag.textContent = await lookupType(scope, scopeTypeId); - scopesParent.appendChild(tag); - }); -} + tag.id = scope; + scopesParent.appendChild(tag); + + let isSelected = false; + + tag.addEventListener('click', async () => { + if (isSelected) { + let headingDiv1 = document.getElementById((scopeTypeId === 'deliverable-type') ? 'platforms' : 'deliverable-type'); + // Fetch all .scope-tag class from headingDiv1 + let alternativeTags = headingDiv1.querySelectorAll('.scope-tag'); + + // Clear the selection + const allTags = document.querySelectorAll('.scope-tag'); + allTags.forEach(t => { + t.style.display = 'inline-block'; + t.classList.remove('selected'); // Remove the selected class + }); + const associatedItems = associationMap.get(scope); + + // Reset the associated buttons + if (associatedItems) { + associatedItems.forEach(async (_count, key) => { + const associatedTag = Array.from(alternativeTags).find(t => t.id.includes(key)); + if (associatedTag) { + let alternateTextContent = await lookupType(associatedTag.id, (scopeTypeId === 'deliverable-type') ? 'platforms' : 'deliverable-type'); + associatedTag.textContent = `${alternateTextContent}`; + associatedTag.style.display = 'inline-block'; + associatedTag.style.pointerEvents = 'auto'; // Make the clickable + } + }); + } + + tag.textContent = await lookupType(scope, scopeTypeId); + isSelected = false; + + // Reset the heading text content + let headingDiv = document.getElementById((scopeTypeId === 'deliverable-type') ? 'platforms' : 'deliverable-type'); + // Fetch h3 class from headingDiv + let heading = headingDiv.querySelector('span.h3'); + + // Remove the appended heading.textContent + heading.textContent = heading.textContent.split(' (')[0]; + + // Remove the "Clear selection" hyperlink text next to the headingDiv + let clearSelection = document.getElementById('clear-selection'); + clearSelection.remove(); + } else { + // Hide all buttons + const allTags = document.querySelectorAll('.scope-tag'); + allTags.forEach(t => t.style.display = 'none'); + + let associatedHeadingDiv = document.getElementById((scopeTypeId === 'deliverable-type') ? 'platforms' : 'deliverable-type'); + // Fetch all .scope-tag class from associatedHeadingDiv + let alternativeTags = associatedHeadingDiv.querySelectorAll('.scope-tag'); + + // Get the associated items + const associatedItems = associationMap.get(scope); + + // Update the clicked button's text content to include the length + tag.textContent = `${await lookupType(scope, scopeTypeId)}`; + tag.style.display = 'inline-block'; + + // selectedTextContent = await lookupType(scope, scopeTypeId); + let totalAssociatedAssetCount = 0; + + // Show the associated buttons + if (associatedItems) { + associatedItems.forEach(async (count, key) => { + totalAssociatedAssetCount = totalAssociatedAssetCount + count; + const associatedTag = Array.from(alternativeTags).find(t => t.id.includes(key)); + let alternateTextContent = await lookupType(associatedTag.id, (scopeTypeId === 'deliverable-type') ? 'platforms' : 'deliverable-type'); + if (associatedTag) { + associatedTag.textContent = `${await lookupType(scope, scopeTypeId)}: ${alternateTextContent} (${count})`; + associatedTag.style.display = 'inline-block'; + associatedTag.style.pointerEvents = 'none'; // Make the tag non-clickable + + } + }); + } + // Add the selected class to the clicked button + tag.classList.add('selected'); + isSelected = true; + let alternateHeadingDiv = document.getElementById((scopeTypeId === 'deliverable-type') ? 'platforms' : 'deliverable-type'); + // Fetch h3 class from headingDiv + let alternateHeading = alternateHeadingDiv.querySelector('span.h3'); + + // Append heading.textContent with number 3 + alternateHeading.textContent = `${alternateHeading.textContent} (${totalAssociatedAssetCount} ${totalAssociatedAssetCount > 1 ? 'assets' : 'asset'})`; + + let headingDiv = document.getElementById((scopeTypeId)); + + // Add a "Clear selection" hyperlink text next to the headingDiv + let clearSelection = document.createElement('a'); + clearSelection.id = 'clear-selection'; + clearSelection.classList.add('clear-selection'); + clearSelection.textContent = 'Clear selection'; + clearSelection.addEventListener('click', () => { + tag.click(); + }); + // Append the "Clear selection" hyperlink text next to the headingDiv as its first child + headingDiv.firstChild.appendChild(clearSelection); + + } + }); + }); + } function buildKPIList(program) { let kpiList = document.createElement('ul'); @@ -701,23 +983,24 @@ async function buildHeaderRow(category, headerType, isInactive, matchCount) { return headerRow; } -async function buildTableRow(deliverableJson, kpi, createHidden) { +async function buildTableRow(deliverable, kpi, createHidden) { //look up friendly name for deliverable type - const typeLabel = await lookupType(deliverableJson.deliverableType, 'deliverable-type'); + const typeLabel = await lookupType(deliverable.deliverableType, 'deliverable-type'); const dataRow = document.createElement('div'); dataRow.classList.add('row', 'datarow'); if (createHidden) dataRow.classList.add('inactive'); - const status = (deliverableJson.deliverableStatusUpdate == null) ? "Not Available" : deliverableJson.deliverableStatusUpdate + "%"; - const statusPct = (deliverableJson.deliverableStatusUpdate == null) ? "0%" : deliverableJson.deliverableStatusUpdate + "%"; + const status = (deliverable.deliverableStatusUpdate == null) ? "Not Available" : deliverable.deliverableStatusUpdate + "%"; + const statusPct = (deliverable.deliverableStatusUpdate == null) ? "0%" : deliverable.deliverableStatusUpdate + "%"; + const assetLinks = createAssetLink(deliverable); dataRow.innerHTML = ` -