UNPKG

express-ads

Version:

Ads distribution and management module for Express

1,609 lines (1,509 loc) 62.8 kB
/* * Copyright (c) 2015 Michel Gutierrez * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ angular.module('EASApp', ["ui.select"]); angular.module('EASApp').controller('EASCtrl', ['$scope', '$http', function ($scope,$http) { $scope.data = {}; $scope.context = { inventory: null, campaign: null, banner: null, addImageError: null, addImageUrl: "", imageFiles: [], startType: "now", endType: "never", start: Date.now(), end: new Date(Date.now()+7*24*60*60*1000).getTime(), selected: {}, selToggle: true, invStyleProp: null, invStyleValue: null, creatingBanner: false, bannerType: "image", bannerText: null, bannerTextId: null, addons: {}, } $scope.campaignUsage = {}; $scope.bannerUsage = {}; $scope.inventoryUsage = {}; $scope.local = { filterInventoryActive: true, filterInventoryUsed: false, filterInventoryCampaign: null, filterInventoryBanner: null, filterCampaignActive: true, filterCampaignUsed: false, filterCampaignInventory: null, filterCampaignBanner: null, filterBannerActive: true, filterBannerUsed: false, filterBannerInventory: null, filterBannerCampaign: null, tab: "campaign", } if(window.localStorage) { var local = window.localStorage.getItem("eas"); if(local) try { var stored = JSON.parse(local); angular.merge($scope.local,stored); } catch(e) {} } $scope.sizes = []; $scope.campTypes = [ {value: "background", label: "Background", vLabel: "Weight" }, {value: "click", label: "Clicks", vLabel: "Total clicks" }, {value: "impr", label: "Imprs", vLabel: "Total imprs" }, {value: "clickperminute", label: "Clicks per minute", vLabel: "Clicks per minute" }, {value: "clickperhour", label: "Clicks per hour", vLabel: "Clicks per hour" }, {value: "clickperday", label: "Clicks per day", vLabel: "Clicks per day" }, {value: "clickperweek", label: "Clicks per week", vLabel: "Clicks per week" }, {value: "clickpermonth", label: "Clicks per month", vLabel: "Clicks per month" }, {value: "imprperminute", label: "Imprs per minute", vLabel: "Imprs per minute" }, {value: "imprperhour", label: "Imprs per hour", vLabel: "Imprs per hour" }, {value: "imprperday", label: "Imprs per day", vLabel: "Imprs per day" }, {value: "imprperweek", label: "Imprs per week", vLabel: "Imprs per week" }, {value: "imprpermonth", label: "Imprs per month", vLabel: "Imprs per month" }, ]; var baseBannerTypes = { image: "Images", text: "Texts", }; $scope.nobanners = [{value:'hide',label:"Hide area"},{value:'blank',label:"Blank area"}]; $scope.startTypes = [ {value: "now", label: "Starts now" }, {value: "date", label: "Starts at" }, ]; $scope.endTypes = [ {value: "never", label: "Never ends" }, {value: "date", label: "Ends at" }, ]; $scope.countryTypes = [ {value: "any", label: "Any country" }, {value: "in", label: "Selected countries" }, {value: "out", label: "All but countries..." }, ]; $scope.browserTypes = [ {value: "any", label: "Any browser" }, {value: "in", label: "Selected browsers" }, {value: "out", label: "All but browsers..." }, ]; $scope.osTypes = [ {value: "any", label: "Any OS" }, {value: "in", label: "Selected OSes" }, {value: "out", label: "All but OSes..." }, ]; $scope.browserFamilies = { "bingbot": "bingbot", "firefox": "Firefox", "android": "Android", "chrome": "Chrome", "pale moon": "Pale Moon", "slurp": "Slurp", "yandexbot": "YandexBot", "mobile safari": "Mobile Safari", "ie": "IE", "phantomjs": "PhantomJS", "yandex browser": "Yandex Browser", "maxthon": "Maxthon", "other": "Other", "safari": "Safari", "applemail": "AppleMail", "chrome mobile": "Chrome Mobile", "ie mobile": "IE Mobile", "opera": "Opera", "firefox mobile": "Firefox Mobile", "iceweasel": "Iceweasel", "chromium": "Chromium", "googlebot": "Googlebot", "firefox beta": "Firefox Beta", "uc browser": "UC Browser", "qq browser mobile": "QQ Browser Mobile", "firefox alpha": "Firefox Alpha", "opera mini": "Opera Mini", "twitterbot": "TwitterBot", "opera mobile": "Opera Mobile", "iron": "Iron", "seamonkey": "SeaMonkey", "k-meleon": "K-Meleon", "up.browser": "UP.Browser", "avant": "Avant", "dolfin": "Dolfin", "chrome mobile ios": "Chrome Mobile iOS", "konqueror": "Konqueror", "camino": "Camino", "facebookbot": "FacebookBot" } $scope.osFamilies = { "other": "Other", "windows 7": "Windows 7", "windows": "Windows", "windows 8.1": "Windows 8.1", "windows xp": "Windows XP", "android": "Android", "windows 8": "Windows 8", "ubuntu": "Ubuntu", "mac os x": "Mac OS X", "linux": "Linux", "windows vista": "Windows Vista", "ios": "iOS", "linux mint": "Linux Mint", "windows phone": "Windows Phone", "fedora": "Fedora", "windows ce": "Windows CE", "windows rt 8.1": "Windows RT 8.1", "chrome os": "Chrome OS", "windows rt": "Windows RT", "windows 98": "Windows 98", "windows 95": "Windows 95", "windows 2000": "Windows 2000", "netbsd": "NetBSD", "debian": "Debian" } const periodTypes = { 'click': { type: 'click' }, 'impr': { type: 'impr' }, 'clickperminute': { type:'click', duration:60*1000 }, 'clickperhour': { type:'click', duration:60*60*1000 }, 'clickperday': { type:'click', duration:24*60*60*1000 }, 'clickperweek': { type: 'click', duration: 7*24*60*60*1000 }, 'clickpermonth': { type: 'click', duration : 30*24*60*60*1000 }, 'imprperminute': { type: 'impr', duration: 60*1000 }, 'imprperhour': { type: 'impr', duration: 60*60*1000 }, 'imprperday': { type: 'impr', duration: 24*60*60*1000 }, 'imprperweek': { type: 'impr', duration: 7*24*60*60*1000 }, 'imprpermonth': { type: 'impr', duration: 30*24*60*60*1000 } }; function UpdateSizes(addons) { var sizes = {text: "Text"}; if($scope.data.sizes) $scope.data.sizes.forEach(function(size) { sizes[size] = size; }); for(var a in (addons || {})) addons[a].sizes.forEach(function(size) { size = size.toLowerCase() if(!sizes[size]) sizes[size] = size[0].toUpperCase()+size.substring(1).toLowerCase(); }); Object.keys(sizes).sort().forEach(function(size) { sizes[size.toLowerCase()] = size[0].toUpperCase()+size.substring(1).toLowerCase(); $scope.sizes.push({value: size, label: sizes[size]}); }); } function UpdateBannerTypes(addons) { $scope.bannerTypes = []; var types = angular.merge({},baseBannerTypes); for(var a in (addons || {})) if(!types[a]) types[a] = addons[a].title; Object.keys(types).sort().forEach(function(type) { $scope.bannerTypes.push({value: type, label: types[type]}); }); } UpdateBannerTypes({}); function Call(apiUrl,apiParams,options,callback) { $scope.loading = true; if(!callback) { callback = options; options = undefined; } $http.post(easAPI + apiUrl,apiParams,options).then(function(data) { $scope.loading = false; if(!data.data.status) return callback(new Error(data.data.error)); else return callback(null,data.data.result || null); },function(err) { $scope.loading = false; console.info("Error",err); if(err.status==403) alert("Logged out ?"); else if(err.status==-1) alert("Server down ?"); callback(err); }); } $scope.campTypeValueLabel = function(type) { for(var i=0;i<$scope.campTypes.length;i++) if($scope.campTypes[i].value==type) return $scope.campTypes[i].vLabel; return ""; } $scope.bannerTypeValueLabel = function(type) { for(var i=0;i<$scope.bannerTypes.length;i++) if($scope.bannerTypes[i].value==type) return $scope.bannerTypes[i].label; return ""; } $scope.campTypeLabel = function(type) { for(var i=0;i<$scope.campTypes.length;i++) if($scope.campTypes[i].value==type) return $scope.campTypes[i].label; return ""; } $scope.sizeLabel = function(size) { for(var s in $scope.sizes) if($scope.sizes[s].value==size) return $scope.sizes[s].label; return ""; } $scope.campRemains = function(cid) { var campaign = $scope.data.ads.campaign[cid]; if(!campaign) return "-"; var periodType = periodTypes[campaign.type]; if(periodType) { var eventsCount = $scope.data.stats.period[periodType.type][cid] || 0; if(periodType.duration) { var timeRemaining = Math.max(0,($scope.data.stats.periodTime[cid]+periodType.duration)-$scope.data.now); return Math.max(0,campaign.value-eventsCount) + " " + periodType.type+"s in "+TimeToText(timeRemaining); } else { var timeRemaining = Math.max(0,campaign.end-$scope.data.now); return Math.max(0,campaign.value-eventsCount) + " " + periodType.type+"s in "+TimeToText(timeRemaining); } } } $scope.$watch('context.addImageUrl',function() { $scope.context.addImageError = null; }); $scope.$watch('local.tab',function() { $scope.context.selected = {}; $scope.context.selToggle = true; $(".tooltip").remove(); }); $scope.$watch('local',function() { if(window.localStorage) window.localStorage.setItem("eas",JSON.stringify($scope.local)); },true); function UpdateSchedule() { var campaign = $scope.context.campaign; var context = $scope.context; if(campaign) { if(context.startType=='date') campaign.start = new Date(context.start).getTime(); else if(context.startType=='now') campaign.start = 0; if(context.endType=='date') campaign.end = new Date(context.end).getTime(); else if(context.endType=='never') campaign.end = 0; } } $scope.$watch('context.startType+context.start+context.endType+context.end',UpdateSchedule); $scope.selectionToggle = function() { if(['campaign','banner','inventory'].indexOf($scope.local.tab)>=0) { for(var id in $scope.data.ads[$scope.local.tab]) $scope.context.selected[id] = $scope.context.selToggle; $scope.context.selToggle = !$scope.context.selToggle; } } $scope.selectedCount = function() { var count = 0; for(var id in $scope.context.selected) if($scope.context.selected[id]) count++; return count; } $scope.activeSelected = function(active) { if(['campaign','banner','inventory'].indexOf($scope.local.tab)>=0) { Call('/active-group',{type: $scope.local.tab, active: active, ids: $scope.context.selected},function(err,data) { $scope.getAds(); }); } } $scope.removeSelected = function() { if(['campaign','banner','inventory'].indexOf($scope.local.tab)>=0) { var count = 0; for(var id in $scope.context.selected) if($scope.context.selected[id]) count++; if(confirm("Do you really want to remove "+count+" item"+(count>1?"s":"")+" ?")) { Call('/remove-group',{type: $scope.local.tab, ids: $scope.context.selected},function(err,data) { $scope.getAds(); }); } } } $scope.mustHaveEnd = function() { return $scope.context.campaign && ( $scope.context.campaign.type=='click' || $scope.context.campaign.type=='impr' ); } $scope.$watch('context.campaign.type',function() { if($scope.mustHaveEnd()) { $scope.context.endType = 'date'; $scope.context.end = $scope.context.campaign.end || new Date(Date.now()+7*24*60*60*1000); } }); $scope.getAds = function(callback) { Call('/',{},function(err,data) { $(".tooltip").hide(); $("[vdh-tooltip]").tooltip('hide'); if(!err) { $scope.data = data; $scope.context.addons = angular.merge({},data.ads.addons); angular.extend($scope.osFamilies,data.osFamilies); angular.extend($scope.browserFamilies,data.browserFamilies); UpdateSizes(data.addons); UpdateBannerTypes(data.addons); $scope.context.selected = {} $scope.selToggle = true; $scope.calcInventoryUsage(); $scope.calcCampaignUsage(); $scope.calcBannerUsage(); if(callback) callback(); } }); } $scope.getAds(); $scope.floor = function(val) { return Math.floor(val); } $scope.prettyAds = function() { return JSON.stringify($scope.data,null,4); } $scope.pretty = function(data) { return JSON.stringify(data,null,4); } /* inventory */ $scope.newInventory = function() { $scope.context.inventory = { hid: "", active: true, size: "300x250", description: "", nobanner: "hide", classes: "", styles: {}, } } $scope.cancelInventory = function() { $scope.context.inventory = null; } $scope.saveInventory = function() { Call('/set-inventory',{inventory: $scope.context.inventory},function(err,data) { $scope.getAds(); }); $scope.context.inventory = null; } $scope.removeInventory = function(inv) { if(confirm("Are you sure you want to remove the area ?")) { $scope.context.inventory = null; Call('/remove-inventory',{iid: inv.id},function(err,data) { $scope.getAds(); }); } } $scope.changedInventory = function() { if(!$scope.context.inventory) return false; return !angular.equals($scope.context.inventory,$scope.data.ads.inventory[$scope.context.inventory.id]); } $scope.selectInventory = function(inv) { $scope.context.inventory = angular.copy(inv); ScrollTop(); } $scope.inventoryCount = function() { if(!$scope.data.ads) return 0; var count = 0; for(var i in $scope.data.ads.inventory) count++; return count; } $scope.inventoryHasStyle = function() { if(!$scope.context.inventory) return false; for(var i in $scope.context.inventory.styles) return true; return false; } $scope.addInventoryStyle = function() { $scope.context.inventory.styles[$scope.context.invStyleProp.trim()] = $scope.context.invStyleValue.trim(); $scope.context.invStyleProp = null; $scope.context.invStyleValue = null; } $scope.newInventoryStyle = function() { $scope.context.invStyleProp = ""; $scope.context.invStyleValue = ""; } $scope.removeInventoryStyle = function(prop) { delete $scope.context.inventory.styles[prop]; } /* campaign */ $scope.newCampaign = function() { $scope.context.campaign = { hid: "", active: true, type: "background", value: 1, cap: 0, pagecap: 0, paused: false, banners: [], description: "", start: 0, end: 0, } } $scope.cancelCampaign = function() { $scope.context.campaign = null; } $scope.saveCampaign = function() { Call('/set-campaign',{campaign: $scope.context.campaign},function(err,data) { $scope.getAds(); }); $scope.context.campaign = null; } $scope.removeCampaign = function(inv) { if(confirm("Are you sure you want to remove the campaign ?")) { $scope.context.campaign = null; Call('/remove-campaign',{iid: inv.id},function(err,data) { $scope.getAds(); }); } } $scope.changedCampaign = function() { if(!$scope.context.campaign) return false; return !angular.equals($scope.context.campaign,$scope.data.ads.campaign[$scope.context.campaign.id]); } $scope.selectCampaign = function(inv) { $scope.context.campaign = angular.copy(inv); $scope.context.startType = $scope.context.campaign.start ? "date" : "now"; $scope.context.endType = $scope.context.campaign.end ? "date" : "never"; ScrollTop(); } $scope.campaignCount = function() { if(!$scope.data.ads) return 0; var count = 0; for(var i in $scope.data.ads.campaign) count++; return count; } $scope.campaignStatus = function(campaign) { if(!campaign.active) return { title: "Paused", clazz: "fa-pause", } if(campaign.start && campaign.start>$scope.data.now) { return { title: "Waiting "+TimeToText(campaign.start-$scope.data.now), clazz: "fa-clock-o", } } if(campaign.end && campaign.end<$scope.data.now) return { title: "Complete", clazz: "fa-times", } if(campaign.active) { var usage = $scope.campaignUsage[campaign.id]; if(usage.bannersCount==0 || usage.inventoryCount==0) return { title: "Unused", clazz: "fa-play-circle-o", } else return { title: "Running ", clazz: "fa-play", } } } /* banner */ $scope.createBanner = function() { $scope.context.banner = { hid: "", active: true, type: $scope.context.bannerType, link: "", alt: "", cap: 0, pagecap: 0, description: "", images: {}, inventory: [], countryType: "any", countries: [], browserType: "any", browsers: [], osType: "any", oss: [], adbUsed: true, adbUnsure: true, adbUnused: true, } $scope.context.imageFiles = []; if($scope.context.bannerType=='image') $scope.context.banner.images = {} else $scope.context.banner.texts = {} Call('/set-banner',{banner: $scope.context.banner},function(err,data) { $scope.context.creatingBanner = false; if(data.banner) $scope.context.banner = data.banner; $scope.getAds(); }); } $scope.newBanner = function() { $scope.context.creatingBanner = true; } $scope.cancelBanner = function() { $scope.context.banner = null; } $scope.saveBanner = function() { Call('/set-banner',{banner: $scope.context.banner},function(err,data) { $scope.getAds(); }); $scope.context.banner = null; } $scope.removeBanner = function(inv) { if(confirm("Are you sure you want to remove the banner ?")) { $scope.context.banner = null; Call('/remove-banner',{iid: inv.id},function(err,data) { $scope.getAds(); }); } } $scope.changedBanner = function() { if(!$scope.context.banner) return false; return !angular.equals($scope.context.banner,$scope.data.ads.banner[$scope.context.banner.id]); } $scope.selectBanner = function(ban) { $scope.context.banner = angular.copy(ban); $scope.context.imageFiles = []; ScrollTop(); } $scope.bannerCount = function() { if(!$scope.data.ads) return 0; var count = 0; for(var i in $scope.data.ads.banner) count++; return count; } $scope.addBannerImage = function() { var imageUrl = ($scope.context.addImageUrl || "").trim(); if(!imageUrl) return; var bid = $scope.context.banner.id; Call('/add-banner-image',{bid: bid,url:imageUrl},function(err,data) { if(err) $scope.context.addImageError = err.data.error; else if($scope.context.banner) { $scope.context.banner.images = $scope.context.banner.images || {}; $scope.context.banner.images[data.image.id] = data.image; } }); } $scope.$watch('context.imageFiles',function() { if($scope.context.imageFiles.length==0) return; var imageFiles = $scope.context.imageFiles; $scope.context.imageFiles = []; var fd = new FormData(); fd.append('bid', $scope.context.banner.id); for(var i=0;i<imageFiles.length;i++) fd.append('files[]', imageFiles[i]); Call('/upload-banner-images',fd,{ transformRequest: angular.identity, headers: { 'Content-Type': undefined } }, function(err,data) { if(err) alert("Could not add file:",err); else if($scope.context.banner) { if(data.errors.length>0) alert(data.errors.join("\n")); $scope.context.banner.images = $scope.context.banner.images || {}; data.images.forEach(function(image) { $scope.context.banner.images[image.id] = image; }); } }); }); $scope.newBannerText = function() { Call('/make-id',{},function(err,data) { if(!err) { $scope.context.bannerTextId = data; $scope.context.bannerText = ''; } }); } $scope.selectBannerText = function(txt) { $scope.context.bannerTextId = txt.id; $scope.context.bannerText = txt.text; } $scope.addBannerText = function() { $scope.context.banner.texts[$scope.context.bannerTextId] = { id: $scope.context.bannerTextId, text: $scope.context.bannerText, } $scope.context.bannerText = null; $scope.context.bannerTextId = null; } $scope.addBannerImageUrl = function() { var imageUrl = ($scope.context.addImageUrl || "").trim(); if(!imageUrl) return; var bid = $scope.context.banner.id; Call('/add-banner-image-url',{bid: bid,url:imageUrl},function(err,data) { if(err) $scope.context.addImageError = err.data.error; else $scope.getAds(function() { $scope.selectBanner($scope.data.ads.banner[bid]); }); }); } $scope.removeBannerImage = function(img) { if(confirm("Are you sure you want to remove the banner image ?")) delete $scope.context.banner.images[img.id]; } $scope.removeBannerText = function(txt) { if(confirm("Are you sure you want to remove this text ?")) { delete $scope.context.banner.texts[txt.id]; if(txt.id==$scope.context.bannerTextId) { $scope.context.bannerText = null; $scope.context.bannerTextId = null; } } } $scope.bannerContentCount = function(type) { if(!$scope.context.banner) return 0; if(!$scope.context.banner[type]) return 0; var count=0; for(var cont in $scope.context.banner[type]) count++; return count; } $scope.bannerImagesCount = function() { return $scope.bannerContentCount('images'); } $scope.bannerTextsCount = function() { return $scope.bannerContentCount('texts'); } $scope.bannersArray = function() { if(!$scope.data || !$scope.data.ads || !$scope.data.ads.banner) return []; var arr=[]; for(var id in $scope.data.ads.banner) arr.push($scope.data.ads.banner[id]); return arr; } $scope.bannerStatus = function(banner) { if(!banner.active) return { title: "Paused", clazz: "fa-pause", } else { var usage = $scope.bannerUsage[banner.id]; if(usage.campaignsCount==0 || usage.inventoryCount==0) return { title: "Unused", clazz: "fa-play-circle-o", } else return { title: "Running ", clazz: "fa-play", } } } $scope.bannerType = function(banner) { if(banner.type=='image') return { clazz: 'fa-picture-o', title: 'Image', } if(banner.type=='text') return { clazz: 'fa-align-justify', title: 'Text', } return { clazz: 'fa-puzzle-piece', title: $scope.data.addons[banner.type]?$scope.data.addons[banner.type].title:"", } } $scope.inventoryArray = function() { if(!$scope.data || !$scope.data.ads || !$scope.data.ads.inventory) return []; var arr=[]; for(var id in $scope.data.ads.inventory) arr.push($scope.data.ads.inventory[id]); return arr; } $scope.calcCTR = function(object,id) { if(!object || !object.click || !object.impr) return "-"; if(!object.click[id]) return 0; if(object.impr[id]) return Math.round(object.click[id]*10000/object.impr[id])/100; else return "-"; } function TimeToText(value) { value=Math.floor(value/1000); var days=Math.floor(value/86400); value%=86400; var hours=Math.floor(value/3600); value%=3600; var minutes=Math.floor(value/60); var seconds=value%60; var str=""; if(days>3) str+=days+'d'; else if(days>0) { if(hours>0) str+=days+"d"+hours+"h" else str+=days+"d"; } else if(hours>3) str+=hours+"h"; else if(hours>0) { if(minutes>0) str+=hours+"h"+minutes+"m"; else str+=hours+"h"; } else { str+=minutes+':'; if(seconds<10) str+='0'; str+=seconds; } return str; } $scope.imageUrl = function(img) { return img.url || '/eas/images/'+img.id+'.png'; } $scope.capitalize = function(string) { return string.charAt(0).toUpperCase() + string.slice(1); } $scope.clearStats = function(type,id,which) { Call('/clear-stats',{type: type, id: id, which:which},function(err,data) { $scope.getAds(); }); } $scope.bannerImageTooltip = function(img) { return '<div><img style=\'max-width:400px;max-height:200px\' src=\''+$scope.imageUrl(img)+'\'/></div>'; } $scope.filterCampaign = function(cam) { if($scope.local.filterCampaignActive && !cam.active) return false; if($scope.local.filterCampaignUsed && ($scope.campaignUsage[cam.id].bannersCount==0 || $scope.campaignUsage[cam.id].inventoryCount==0)) return false; if($scope.data.ads && $scope.data.ads.inventory[$scope.local.filterCampaignInventory] && !$scope.campaignUsage[cam.id].inventory[$scope.local.filterCampaignInventory]) return false; if($scope.data.ads && $scope.data.ads.banner[$scope.local.filterCampaignBanner] && !$scope.campaignUsage[cam.id].banners[$scope.local.filterCampaignBanner]) return false; return true; } $scope.calcCampaignUsage = function() { if(!$scope.data || !$scope.data.ads || !$scope.data.ads.campaign) return; $scope.campaignUsage = {}; for(var camId in $scope.data.ads.campaign) { var cam = $scope.data.ads.campaign[camId]; var banners = {}; var inventory = {}; if(cam.active) { cam.banners.forEach(function(bid) { if(!$scope.data.ads.banner[bid]) return; if(!$scope.data.ads.banner[bid].active) return; banners[bid] = 1; }); for(var bid in banners) { var banner = $scope.data.ads.banner[bid]; banner.inventory.forEach(function(iid) { var inv = $scope.data.ads.inventory[iid]; if(!inv) return; if(!inv.active) return; if(banner.type=='text') { if(inv.size=='text') inventory[iid] = 1; } else if(banner.type=='image') { for(var imid in banner.images) { var image = banner.images[imid]; if(!image) continue; if(image.size==inv.size) inventory[iid] = 1; } } else if($scope.data.addons[banner.type]) { if($scope.data.addons[banner.type].sizes.indexOf(inv.size)>=0) inventory[iid] = 1; } }); } } var inventoryCount = 0; for(var iid in inventory) if($scope.data.ads.inventory[iid].active) inventoryCount++; var bannersCount = 0; for(var bid in banners) if($scope.data.ads.banner[bid].active) bannersCount++; $scope.campaignUsage[camId] = { banners: banners, bannersCount: bannersCount, inventory: inventory, inventoryCount: inventoryCount, } } var campaignInv = {}; var campaignCount = {}; for(var invId in $scope.data.stats.roll) { if(!$scope.data.ads.inventory[invId]) continue; var roll = $scope.data.stats.roll[invId]; for(var cid in roll.current) { if(!(cid in $scope.campaignUsage)) continue; campaignInv[cid] = campaignInv[cid] || {}; campaignInv[cid][invId] = (campaignInv[cid][invId] || 0) + roll.current[cid]; campaignCount[cid] = (campaignCount[cid] || 0) + roll.current[cid]; } for(var cid in roll.last) { if(!(cid in $scope.campaignUsage)) continue; campaignInv[cid] = campaignInv[cid] || {}; campaignInv[cid][invId] = (campaignInv[cid][invId] || 0) + roll.last[cid]; campaignCount[cid] = (campaignCount[cid] || 0) + roll.last[cid]; } } for(var cid in campaignInv) { $scope.campaignUsage[cid].inventoryUsagePercent = {}; for(var iid in campaignInv[cid]) $scope.campaignUsage[cid].inventoryUsagePercent[iid] = Math.round(campaignInv[cid][iid]*100/campaignCount[cid]); } } $scope.filterBanner = function(ban) { if($scope.local.filterBannerActive && !ban.active) return false; if($scope.local.filterBannerUsed && ($scope.bannerUsage[ban.id].campaignsCount==0 || $scope.bannerUsage[ban.id].inventoryCount==0)) return false; if($scope.data.ads && $scope.data.ads.inventory[$scope.local.filterBannerInventory] && !$scope.bannerUsage[ban.id].inventory[$scope.local.filterBannerInventory]) return false; if($scope.data.ads && $scope.data.ads.campaign[$scope.local.filterBannerCampaign] && !$scope.bannerUsage[ban.id].campaigns[$scope.local.filterBannerCampaign]) return false; return true; } $scope.calcBannerUsage = function() { if(!$scope.data || !$scope.data.ads || !$scope.data.ads.banner) return; $scope.bannerUsage = {}; for(var banId in $scope.data.ads.banner) { var banner = $scope.data.ads.banner[banId]; var campaigns = {}; var inventory = {}; if(banner.active) { banner.inventory.forEach(function(iid) { var inv = $scope.data.ads.inventory[iid]; if(!inv) return; if(!inv.active) return; if(banner.type=='image') { for(var imid in banner.images) { var image = banner.images[imid]; if(!image) continue; if(image.size==inv.size) inventory[iid] = 1; } } else if(banner.type=='text') { if(inv.size=='text') inventory[iid] = 1; } else if($scope.data.addons[banner.type]) { if($scope.data.addons[banner.type].sizes.indexOf(inv.size)>=0) inventory[iid] = 1; } }); for(var cid in $scope.data.ads.campaign) { var campaign = $scope.data.ads.campaign[cid]; if(!campaign) continue; if(!campaign.active) continue; if(campaign.banners.indexOf(banner.id)>=0) campaigns[cid] = 1; } } var inventoryCount = 0; for(var iid in inventory) if($scope.data.ads.inventory[iid].active) inventoryCount++; var campaignsCount = 0; for(var cid in campaigns) if($scope.data.ads.campaign[cid].active) campaignsCount++; $scope.bannerUsage[banId] = { campaigns: campaigns, campaignsCount: campaignsCount, inventory: inventory, inventoryCount: inventoryCount, } } } $scope.filterActiveInventory = function(inv) { return inv.active; } $scope.filterTypedInventory = function(type) { return function(inv) { if(type=='text') return inv.size=='text'; if(type=='image') return inv.size!='text'; return $scope.data.addons[type] && $scope.data.addons[type].sizes.indexOf(inv.size)>=0; } } $scope.filterInventory = function(inv) { if($scope.local.filterInventoryActive && !inv.active) return false; if($scope.local.filterInventoryUsed && ($scope.inventoryUsage[inv.id].campaignsCount==0 || $scope.inventoryUsage[inv.id].bannersCount==0)) return false; if($scope.data.ads && $scope.data.ads.campaign[$scope.local.filterInventoryCampaign] && !$scope.inventoryUsage[inv.id].campaigns[$scope.local.filterInventoryCampaign]) return false; if($scope.data.ads && $scope.data.ads.banner[$scope.local.filterInventoryBanner] && !$scope.inventoryUsage[inv.id].banners[$scope.local.filterInventoryBanner]) return false; return true; } $scope.calcInventoryUsage = function() { if(!$scope.data || !$scope.data.ads || !$scope.data.ads.inventory) return; var now = $scope.data.now; $scope.inventoryUsage = {}; for(var invId in $scope.data.ads.inventory) { var inv = $scope.data.ads.inventory[invId]; var uroll = { total: 0, campaignPercent: {}, }; var roll = $scope.data.stats.roll[invId]; if(roll) { var total = roll.currentCount + roll.lastCount; if(total>0) { uroll.total = total; uroll.perday = Math.round(total * ( 24*60*60*1000 / (now-roll.lastStart))); var campaigns0 = {} for(var cid in roll.current) { if(cid[0]=='_') continue; campaigns0[cid] = roll.current[cid]; } for(var cid in roll.last) { if(cid[0]=='_') continue; campaigns0[cid] = (campaigns0[cid] || 0) + roll.last[cid]; } var background = 0; var scheduled = 0; for(var cid in campaigns0) { var campaign = $scope.data.ads.campaign[cid]; uroll.campaignPercent[cid] = Math.round(campaigns0[cid]*100/total); if(campaign) { if(campaign.type=='background') background += campaigns0[cid]; else scheduled += campaigns0[cid]; } } uroll.backgroundPercent = Math.round(background*100/total); uroll.scheduledPercent = Math.round(scheduled*100/total) var missedBanner = (roll.current['_noBanner'] || 0) + (roll.last['_noBanner'] || 0); uroll.missedBannerPercent = Math.round(missedBanner*100/total) var missedCampaign = (roll.current['_noCampaign'] || 0) + (roll.last['_noCampaign'] || 0); uroll.missedCampaignPercent = Math.round(missedCampaign*100/total) } } var banners = {}; var campaigns = {}; if(inv.active) { for(var bid in $scope.data.ads.banner) { var banner = $scope.data.ads.banner[bid]; if(!banner) continue; if(!banner.active) continue; if(banner.inventory.indexOf(inv.id)>=0) { if(banner.type=='text') { if(inv.size=='text') banners[bid] = 1; } else if(banner.type=='image') { for(var iid in banner.images) { var image = banner.images[iid]; if(!image) continue; if(image.size==inv.size) banners[bid] = 1; } } else if($scope.data.addons[banner.type]) { if($scope.data.addons[banner.type].sizes.indexOf(inv.size)>=0) banners[bid] = 1; } } } for(var cid in $scope.data.ads.campaign) { var campaign = $scope.data.ads.campaign[cid]; if(!campaign) continue; if(!campaign.active) continue; for(var bid in banners) if(campaign.banners.indexOf(bid)>=0) campaigns[cid] = 1; } } var bannersCount = 0; for(var bid in banners) if($scope.data.ads.banner[bid].active) bannersCount++; var campaignsCount = 0; for(var cid in campaigns) if($scope.data.ads.campaign[cid].active) campaignsCount++; $scope.inventoryUsage[invId] = { banners: banners, bannersCount: bannersCount, campaigns: campaigns, campaignsCount: campaignsCount, roll: uroll, }; } } function CountProps(object) { var count = 0; for(var i in object) count++; return count; } $scope.campaignTooltip = function(cam) { var tooltip = '<div style="width: 400px;text-align:left">'; var usage = $scope.campaignUsage[cam.id]; var inventoryCount = CountProps(usage.inventory); var bannersCount = CountProps(usage.banners); if(bannersCount>0) { tooltip += '<div class="eas-tooltip-label">Banners:</div><div>'; var banners = []; for(var bid in usage.banners) banners.push($scope.data.ads.banner[bid].hid); tooltip += banners.join(", "); tooltip += '</div>'; } if(inventoryCount>0) { tooltip += '<div class="eas-tooltip-label">Inventory:</div><div>'; var inventory = []; for(var iid in usage.inventory) { var str = $scope.data.ads.inventory[iid].hid; if($scope.campaignUsage[cam.id].inventoryUsagePercent) str += " ("+($scope.campaignUsage[cam.id].inventoryUsagePercent[iid] || 0)+"%)"; inventory.push(str); } tooltip += inventory.join(", "); tooltip += '</div>'; } if(bannersCount==0 && inventoryCount==0) tooltip += '<div class="eas-tooltip-label">Not used</div>'; tooltip += '</div>'; return tooltip; } $scope.bannerTooltip = function(cam) { var tooltip = '<div style="width: 400px;text-align:left">'; var usage = $scope.bannerUsage[cam.id]; var inventoryCount = CountProps(usage.inventory); var campaignsCount = CountProps(usage.campaigns); if(campaignsCount>0) { tooltip += '<div class="eas-tooltip-label">Campaigns:</div><div>'; var campaigns = []; for(var cid in usage.campaigns) campaigns.push($scope.data.ads.campaign[cid].hid); tooltip += campaigns.join(", "); tooltip += '</div>'; } if(inventoryCount>0) { tooltip += '<div class="eas-tooltip-label">Inventory:</div><div>'; var inventory = []; for(var iid in usage.inventory) inventory.push($scope.data.ads.inventory[iid].hid); tooltip += inventory.join(", "); tooltip += '</div>'; } if(campaignsCount==0 && inventoryCount==0) tooltip += '<div class="eas-tooltip-label">Not used</div>'; tooltip += '</div>'; return tooltip; } $scope.inventoryStatus = function(inventory) { if(!inventory.active) return { title: "Paused", clazz: "fa-pause", } else { var usage = $scope.inventoryUsage[inventory.id]; if(usage.campaignsCount==0 || usage.bannersCount==0) return { title: "Unused", clazz: "fa-play-circle-o", } else return { title: "Running ", clazz: "fa-play", } } } $scope.inventoryTooltip = function(inv) { var tooltip = '<div style="width: 400px;text-align:left">'; var usage = $scope.inventoryUsage[inv.id]; var bannersCount = CountProps(usage.banners); var campaignsCount = CountProps(usage.campaigns); if(campaignsCount>0) { tooltip += '<div class="eas-tooltip-label">Campaigns:</div><div>'; var campaigns = []; for(var cid in usage.campaigns) { var campStr = $scope.data.ads.campaign[cid].hid; if(usage.roll.campaignPercent[cid]) campStr += ' ('+ usage.roll.campaignPercent[cid] + '%)'; campaigns.push(campStr); } tooltip += campaigns.join(", "); tooltip += '</div>'; } if(bannersCount>0) { tooltip += '<div class="eas-tooltip-label">Banners:</div><div>'; var banners = []; for(var bid in usage.banners) banners.push($scope.data.ads.banner[bid].hid); tooltip += banners.join(", "); tooltip += '</div>'; } if(usage.roll.total>0) { tooltip += '<div class="eas-tooltip-label">Usage:</div>'; tooltip += '<div>'+usage.roll.perday + " imprs per day</div>"; tooltip += '<div>Scheduled '+usage.roll.scheduledPercent+'%</div>'; tooltip += '<div>Background '+usage.roll.backgroundPercent+'%</div>'; tooltip += '<div>Missed no banner '+usage.roll.missedBannerPercent+'%</div>'; tooltip += '<div>Missed no campaign '+usage.roll.missedCampaignPercent+'%</div>'; } if(bannersCount==0 && campaignsCount==0 && usage.roll.total==0) tooltip += '<div class="eas-tooltip-label">Not used</div>'; tooltip += '</div>'; return tooltip; } $scope.instantStats = function(what,id,period) { if(!$scope.data.stats || !$scope.data.stats[period]) return {string:""}; var duration = $scope.data.stats[period].duration; var now = $scope.data.now; var instant = $scope.data.stats[period][what][id]; if(!instant) return {string:""}; var currentDuration = now - instant.lastEnd; var missingDuration = Math.max(0,duration - currentDuration); var lastDuration = instant.lastEnd - instant.lastStart; var imprs = instant.current.impr; var clicks = instant.current.click; if(lastDuration>0) { imprs += Math.round(instant.last.impr * (missingDuration/lastDuration)); clicks += Math.round(instant.last.click * (missingDuration/lastDuration)); } if(currentDuration>duration) { imprs = 0; clicks = 0; } var str = ""; var ctr = ""; if(imprs>0 || clicks>0) { str = clicks + "/" + imprs; if(imprs) { ctr = (Math.round(clicks*10000/imprs)/100)+"%"; str += " ("+ctr+")"; } } return { imprs: imprs, clicks: clicks, ctr: ctr, string: str, } } $scope.totalStats = function(what,id) { if(!$scope.data.stats || !$scope.data.stats.total) return {string:""}; var totalStats = $scope.data.stats.total; var total = totalStats[what]; var imprs = total.impr[id] || 0; var clicks = total.click[id] || 0; var str = clicks + "/" + imprs; if(clicks==0 && imprs==0) str = ""; var ctr = ""; if(imprs) { ctr = Math.round(clicks*10000/imprs)/100+"%"; str += " ("+ctr+")"; } return { imprs: imprs, clicks: clicks, ctr: ctr, string: str, }; } function ScrollTop() { window.scroll(0,0); } $scope.sorter = function(name,what) { $scope.local.sorter = $scope.local.sorter || {}; $scope.local.sorter[name] = $scope.local.sorter[name] || {}; return function(item) { var sorter = $scope.local.sorter[name]; var type = sorter.type || 'normal'; if(type=='normal') { var field = sorter.field || 'hid'; return item[field] || 0; } else if(type='stats') { if(!$scope.data.stats || !$scope.data.stats[sorter.field]) return 0; var total = $scope.data.stats[sorter.field][what]; if(!total) return 0; if(sorter.statsType==1) { switch(sorter.index%3) { case 0: return total.click[item.id] || 0; case 1: return total.impr[item.id] || 0; case 2: return total.impr[item.id] ? (total.click[item.id] || 0)/total.impr[item.id] : 0; } } else if(sorter.statsType==2) { var duration = $scope.data.stats[sorter.field].duration; var now = $scope.data.now; var instant = total[item.id]; if(!instant) return 0; var currentDuration = now - instant.lastEnd; var missingDuration = Math.max(0,duration - currentDuration); var lastDuration = instant.lastEnd - instant.lastStart; var imprs = instant.current.impr; var clicks = instant.current.click; if(lastDuration>0) { imprs += Math.round(instant.last.impr * (missingDuration/lastDuration)); clicks += Math.round(instant.last.click * (missingDuration/lastDuration)); } if(currentDuration>duration) { imprs = 0; clicks = 0; } switch(sorter.index%3) { case 0: return clicks || 0; case 1: return imprs || 0; case 2: return imprs ? (clicks || 0)/imprs : 0; } } } } } $scope.sorterDir = function(name) { $scope.local.sorter = $scope.local.sorter || {}; $scope.local.sorter[name] = $scope.local.sorter[name] || {}; var sorter = $scope.local.sorter[name]; var type = sorter.type || 'normal'; if(type=='normal') return !!sorter.direction; else if(type=='stats') return sorter.index<3; } $scope.sorterField = function(name,field) { $scope.local.sorter = $scope.local.sorter || {}; $scope.local.sorter[name] = $scope.local.sorter[name] || {}; var sorter = $scope.local.sorter[name]; sorter.type = 'normal'; if(sorter.field===field) sorter.direction = !(sorter.direction); else { sorter.field = field; sorter.direction = false; } } $scope.sorterStat = function(name,field,statsType) { statsType = statsType || 1; $scope.local.sorter = $scope.local.sorter || {}; $scope.local.sorter[name] = $scope.local.sorter[name] || {}; var sorter = $scope.local.sorter[name]; sorter.type = 'stats'; if(sorter.field===field) { sorter.direction = !(sorter.direction); sorter.index = ((sorter.index || 0) + 1) % 6; } else { delete sorter.direction; sorter.statsType = statsType; sorter.index = 0; sorter.field = field; } } $scope.gotoItem = function(type,id) { var item = $scope.data.ads[type][id]; if(item) { $scope.local.tab = type; $scope["select"+type[0].toUpperCase()+type.substring(1)](item); } } $scope.hasAddons = function() { if($scope.data.addons) for(var a in $scope.data.addons) return true; return false; } $scope.haveAddonSettingsChanged = function() { try { return !angular.equals($scope.context.addons,$scope.data.ads.addons); } catch(e) { return false; } } $scope.resetAddons = function() { $scope.context.addons = angular.merge({},$scope.data.ads.addons); } $scope.updateAddons = function() { var backup = $scope.context.addons; $scope.context.addons = {}; Call('/set-addons',{addons: backup},function(err,data) { if(err) $scope.context.addons = backup; else $scope.getAds(); }); } $scope.countries = [ // Taken from https://gist.github.com/unceus/6501985 {name: 'Afghanistan', code: 'AF'}, {name: 'Aland Islands', code: 'AX'}, {name: 'Albania', code: 'AL'}, {name: 'Algeria', code: 'DZ'}, {name: 'American Samoa', code: 'AS'}, {name: 'Andorra', code: 'AD'}, {name: 'Angola', code: 'AO'}, {name: 'Anguilla', code: 'AI'}, {name: 'Antarctica', code: 'AQ'}, {name: 'Antigua and Barbuda', code: 'AG'}, {name: 'Argentina', code: 'AR'}, {name: 'Armenia', code: 'AM'}, {name: 'Aruba', code: 'AW'}, {name: 'Australia', code: 'AU'}, {name: 'Austria', code: 'AT'}, {name: 'Azerbaijan', code: 'AZ'}, {name: 'Bahamas', code: 'BS'}, {name: 'Bahrain', code: 'BH'}, {name: 'Bangladesh', code: 'BD'}, {name: 'Barbados', code: 'BB'}, {name: 'Belarus', code: 'BY'}, {name: 'Belgium', code: 'BE'}, {name: 'Belize', code: 'BZ'}, {name: 'Benin', code: 'BJ'}, {name: 'Bermuda', code: 'BM'}, {name: 'Bhutan', code: 'BT'}, {name: 'Bolivia', code: 'BO'}, {name: 'Bosnia and Herzegovina', code: 'BA'}, {name: 'Botswana', code: 'BW'}, {name: 'Bouvet Island', code: 'BV'}, {name: 'Brazil', code: 'BR'}, {name: 'British Indian Ocean Territory', code: 'IO'}, {name: 'Brunei Darussalam', code: 'BN'}, {name: 'Bulgaria', code: 'BG'}, {name: 'Burkina Faso', code: 'BF'}, {name: 'Burundi', code: 'BI'}, {name: 'Cambodia', code: 'KH'}, {name: 'Cameroon', code: 'CM'}, {name: 'Canada', code: 'CA'}, {name: 'Cape Verde', code: 'CV'}, {name: 'Cayman Islands', code: 'KY'}, {name: 'Central African Republic', code: 'CF'}, {name: 'Chad', code: 'TD'}, {name: 'Chile', code: 'CL'}, {name: 'China', code: 'CN'}, {name: 'Christmas Island', code: 'CX'}, {name: 'Cocos (Keeling) Islands', code: 'CC'}, {name: 'Colombia', code: 'CO'}, {name: 'Comoros', code: 'KM'}, {name: 'Congo', code: 'CG'}, {name: 'Congo, The Democratic Republic of the', code: 'CD'}, {name: 'Cook Islands', code: 'CK'}, {name: 'Costa Rica', code: 'CR'}, {name: 'Cote D\'Ivoire', code: 'CI'}, {name: 'Croatia', code: 'HR'}, {name: 'Cuba', code: 'CU'}, {name: 'Cyprus', code: 'CY'}, {name: 'Czech Republic', code: 'CZ'}, {name: 'Denmark', code: 'DK'}, {name: 'Djibouti', code: 'DJ'}, {name: 'Dominica', code: 'DM'}, {name: 'Dominican Republic', code: 'DO'}, {name: 'Ecuador', code: 'EC'}, {name: 'Egypt', code: 'EG'}, {name: 'El Salvador', code: 'SV'}, {name: 'Equatorial Guinea', code: 'GQ'}, {name: 'Eritrea', code: 'ER'}, {name: 'Estonia', code: 'EE'}, {name: 'Ethiopia', code: 'ET'}, {name: 'Falkland Islands (Malvinas)', code: 'FK'}, {name: 'Faroe Islands', code: 'FO'}, {name: 'Fiji', code: 'FJ'}, {name: 'Finland', code: 'FI'}, {name: 'France', code: 'FR'}, {name: 'French Guiana', code: 'GF'}, {name: 'French Polynesia', code: 'PF'}, {name: 'French Southern Territories', code: 'TF'}, {name: 'Gabon', code: 'GA'}, {name: 'Gambia', code: 'GM'}, {name: 'Georgia', code: 'GE'}, {name: 'Germany', code: 'DE'}, {name: 'Ghana', code: 'GH'}, {name: 'Gibraltar', code: 'GI'}, {name: 'Greece', code: 'GR'}, {name: 'Greenland', code: 'GL'}, {name: 'Grenada', code: 'GD'}, {name: 'Guadeloupe', code: 'GP'}, {name: 'Guam', code: 'GU'}, {name: 'Guatemala', code: 'GT'}, {name: 'Guernsey', code: 'GG'}, {name: 'Guinea', code: 'GN'}, {name: 'Guinea-Bissau', code: 'GW'}, {name: 'Guyana', code: 'GY'}, {name: 'Haiti', code: 'HT'}, {name: 'Heard Island and Mcdonald Islands', code: 'HM'}, {name: 'Holy See (Vatican City State)', code: 'VA'}, {name: 'Honduras', code: 'HN'}, {name: 'Hong Kong', code: 'HK'}, {name: 'Hungary', code: 'HU'}, {name: 'Iceland', code: 'IS'}, {name: 'India', code: 'IN'}, {name: 'Indonesia', code: 'ID'}, {name: 'Iran, Islamic Republic Of', code: 'IR'},