(function () { var $injector = angular.injector(['ng']); $injector.invoke(function ($http, $rootScope) { $rootScope.$apply(function () { $http.post('/rest/user/login', { }).then(function successCallback(res) { angular.module("app").constant("USER", res.data.user); angular.bootstrap(document, ['app']); }); }); }); })(); var app = angular.module('app', ['ui.bootstrap', 'amChartsDirective', 'ngRoute', 'pascalprecht.translate']); app.filter('html', ['$sce', function ($sce) { return function (text) { return $sce.trustAsHtml(text); }; }]); app.filter('useFilter', function ($filter) { return function () { if (typeof arguments[0] == 'undefined') { return ''; }; var filterName = [].splice.call(arguments, 1, 1)[0]; return $filter(filterName).apply(null, arguments); }; }) app.filter('useFilter2', function ($filter) { return function () { if (typeof arguments[0] == 'undefined') { return '--'; }; var filterName = [].splice.call(arguments, 1, 1)[0]; return $filter(filterName).apply(null, arguments); }; }) app.filter('exists', function ($filter) { return function (v) { if (v === 0 || v == '0.00') { return '--'; } if (!v) { return ''; }; return v; }; }) app.filter('secondsToHHmmss', function ($filter) { return function (seconds) { if (seconds) { return $filter('date')(new Date(0, 0, 0).setSeconds(seconds), 'HH:mm:ss'); } return '00:00:00'; }; }) app.filter('mcdate', function ($filter) { return function (datum) { var d = new Date(datum); return d.getDate() + '/' + (d.getMonth() + 1) + '/' + d.getFullYear() + ' - ' + d.getHours() + ':' + d.getMinutes(); }; }) app.filter('minzero', function ($filter) { return function (int) { return (int < 0) ? 0 : int; }; }) app.filter('path', function () { return function (url) { return url = url.substring(url.indexOf("/")); }; }) app.filter('truefalse', function () { return function (b) { return (b) ? 'Yes' : 'No'; }; }) app.filter('string', function () { return function (b) { return b; }; }) app.filter('objToArray', function () { return function (input, attribute) { if (!angular.isObject(input)) return input; var array = []; for (var objectKey in input) { array.push(input[objectKey]); } return array; } }) app.factory('h', function ($rootScope, $http) { return { siteId: function () { if ($rootScope.user.site) { if ($rootScope.user.site._id) { return $rootScope.user.site._id.$oid; } } }, login: function (cb) { $http.post('/rest/user/login', { }).then(function successCallback(res) { $rootScope.user = res.data.user; cb(); }); }, colors: function () { return angular.extend([], ['#E91E63', '#3F51B5', '#00BCD4', '#8BC34A', '#FF9800', '#795548', '#607D8B', '#00CC00', '#0000CC', '#DDDDDD', '#999999', '#333333', '#990000'], $rootScope.user.reseller.config.chartcolors) }, metrics: function (key) { var allMetrics = { analytics: { sessions: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, users: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, pageviews: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, pageviewsPerSession: { typefilter: 'number', filtercomparator: 2, 'datemerge': 'avg' }, avgSessionDuration: { typefilter: 'secondsToHHmmss', filtercomparator: 0, 'datemerge': 'avg' }, percentNewSessions: { typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, bounceRate: { typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg', 'reversesign': true }, goalCompletionsAll: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, goalValueAll: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, goalConversionRateAll: { typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, goalAbandonRateAll: { typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, uniquePageviews: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, avgTimeOnPage: { typefilter: 'secondsToHHmmss', filtercomparator: 0, 'datemerge': 'avg' }, entrances: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, exitRate: { typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, pageValue: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, exits: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, ROAS: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, costPerTransaction: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, totalValue: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, adCost: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, CPC: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, adClicks: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, transactions: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, uniquePurchases: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, totalValue: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, revenuePerTransaction: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, itemQuantity: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, itemsPerPurchase: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, }, copernica: { Placeholder: { name: '', typefilter: 'number', filtercomparator: 0 }, subject: { name: 'Subject', typefilter: 'number', filtercomparator: 0 }, type: { name: 'Type', typefilter: 'string' }, destinations: { name: 'Destinations', typefilter: 'number', filtercomparator: 0 }, abuses: { name: 'Abuses', typefilter: 'number', filtercomparator: 0 }, clicks: { name: 'Clicks', typefilter: 'number', filtercomparator: 0 }, uniqueclicks: { name: 'Unique Clicks', typefilter: 'number', filtercomparator: 0 }, deliveries: { name: 'Deliveries', typefilter: 'number', filtercomparator: 0 }, errors: { name: 'Errors', typefilter: 'number', filtercomparator: 0 }, impressions: { name: 'Impressions', typefilter: 'number', filtercomparator: 0 }, uniqueimpressions: { name: 'Unique Impressions', typefilter: 'number', filtercomparator: 0 }, retries: { name: 'Retries', typefilter: 'number', filtercomparator: 0 }, unsubscribes: { name: 'Unsubscribes', typefilter: 'number', filtercomparator: 0 }, }, facebookads: { clicks: { name: 'Klikken', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, impressions: { name: 'Impressies', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, cpc: { name: 'cpc', typefilter: 'number', filtercomparator: 2, 'before': '€', 'datemerge': 'avg' }, cpm: { name: 'cpm', typefilter: 'number', filtercomparator: 2, 'before': '€', 'datemerge': 'avg' }, ctr: { name: 'Ctr', typefilter: 'number', filtercomparator: 2, 'after': '%', 'datemerge': 'avg' }, Conversions: { name: 'Conversions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, Cost: { name: 'Cost', typefilter: 'number', filtercomparator: 0, 'before': '€', 'datemerge': 'sum' }, CostPerConversion: { name: 'spend / conversie', typefilter: 'number', filtercomparator: 0, 'before': '€', 'datemerge': 'avg' }, ConversionRate: { name: 'Conversie ratio', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, AveragePosition: { name: 'Gemiddelde positie', typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, ConversionValue: { name: 'Totale conversiewaarde', typefilter: 'number', filtercomparator: 0, 'before': '€', 'datemerge': 'sum' }, relevance_score: { name: 'Relevantie Score', typefilter: 'number', filtercomparator: 0, hide: { 'panel': true }, 'datemerge': 'avg' }, spend: { name: 'Kosten', typefilter: 'number', filtercomparator: 2, 'before': '€', 'datemerge': 'sum' }, }, facebookinsights: { fans: { name: 'Fans', typefilter: 'number', filtercomparator: 0 }, interacties: { name: 'Interactions', typefilter: 'number', filtercomparator: 0 }, bereik: { name: 'Bereik', typefilter: 'number', filtercomparator: 0 }, }, googlexads: { Clicks: { name: 'Clicks', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, Impressions: { name: 'Impressions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, Date: { name: 'Date', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, AverageCpc: { name: 'Avg cost / Click', typefilter: 'number', filtercomparator: 2, 'before': '€', 'datemerge': 'avg' }, Ctr: { name: 'Ctr', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, Conversions: { name: 'Conversions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, Cost: { name: 'Cost', typefilter: 'number', filtercomparator: 2, 'before': '€', 'datemerge': 'sum' }, CostPerConversion: { name: 'Cost / conversion', typefilter: 'number', filtercomparator: 2, 'before': '€', 'datemerge': 'avg' }, ConversionRate: { name: 'Conversion rate', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, AbsoluteTopImpressionPercentage: { name: 'AbsoluteTopImpressionPercentage', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, ConversionValue: { name: 'Conversion value', typefilter: 'number', filtercomparator: 0, 'before': '€', 'datemerge': 'sum' }, SearchImpressionShare: { name: 'Zoekimpressie aandeel', typefilter: 'number', filtercomparator: 2, 'after': '%', 'datemerge': 'avg' }, SearchAbsoluteTopImpressionShare: { name: 'Top Positie', typefilter: 'number', filtercomparator: 2, 'after': '%', 'datemerge': 'avg' }, roas: { name: 'ROAS', typefilter: 'number', filtercomparator: 2, 'after': '%', 'datemerge': 'avg' }, }, instagram: { impressions: { name: 'Fans', typefilter: 'number', filtercomparator: 0 }, reach: { name: 'Interactions', typefilter: 'number', filtercomparator: 0 }, follower_count: { name: 'Bereik', typefilter: 'number', filtercomparator: 0 }, profile_views: { name: 'Bereik', typefilter: 'number', filtercomparator: 0 }, website_clicks: { name: 'Bereik', typefilter: 'number', filtercomparator: 0 }, email_contacts: { name: 'Bereik', typefilter: 'number', filtercomparator: 0 }, get_directions_clicks: { name: 'Bereik', typefilter: 'number', filtercomparator: 0 }, phone_call_clicks: { name: 'Bereik', typefilter: 'number', filtercomparator: 0 }, text_message_clicks: { name: 'Bereik', typefilter: 'number', filtercomparator: 0 }, }, linkedin: { Placeholder: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, shareCount: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, likeCount: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, engagement: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, clickCount: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, impressionCount: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, commentCount: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, networksize: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, networksizeexternal: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, networksizeinternal: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, }, linkedinxads: { impressions: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, clicks: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, oneClickLeads: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, costInLocalCurrency: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, externalWebsiteConversions: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, costInLocalCurrency: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, networksize: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, networksizeexternal: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, networksizeinternal: { typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, }, mailchimp: { Placeholder: { name: '', typefilter: 'number', filtercomparator: 0 }, member_count: { name: 'Aanmeldingen', typefilter: 'number', filtercomparator: 0 }, list_rating: { name: 'Rating', typefilter: 'number', filtercomparator: 0 }, click_rate: { name: 'Klik Ratio', typefilter: 'number', filtercomparator: 0, 'after': '%' }, open_rate: { name: 'Open Ratio', typefilter: 'number', filtercomparator: 0, 'after': '%' }, emails_sent: { name: 'Email verstuurd', typefilter: 'number', filtercomparator: 0 }, send_time: { name: 'Verstuurd op', typefilter: 'mcdate', filtercomparator: 0 }, unsubscribed: { name: 'Afmeldingen', typefilter: 'number', filtercomparator: 0 }, unsubscribe_count: { name: 'Afmeldingen', typefilter: 'number', filtercomparator: 0 }, cleaned_count: { name: 'Verwijderd', typefilter: 'number', filtercomparator: 0 }, bounces: { name: 'Bounce', typefilter: 'number', filtercomparator: 0 }, total_orders: { name: 'Orders', typefilter: 'number', filtercomparator: 0 }, recipient_clicks: { name: 'klikken', typefilter: 'number', filtercomparator: 0 }, total_revenue: { name: 'Totale opbrangest', typefilter: 'number', filtercomparator: 0, 'after': '€' }, }, mybusiness: { Placeholder: { name: '', typefilter: 'number', filtercomparator: 0 }, QUERIES_DIRECT: { name: 'Directe zoekopdrachten', typefilter: 'number', filtercomparator: 0 }, QUERIES_INDIRECT: { name: 'Indirecte zoekopdrachten', typefilter: 'number', filtercomparator: 0 }, VIEWS_MAPS: { name: 'Google maps', typefilter: 'number', filtercomparator: 0 }, VIEWS_SEARCH: { name: 'Google zoeken', typefilter: 'number', filtercomparator: 0 }, QUERIES_DIRECT: { name: 'Bedrijf zoekopdacht', typefilter: 'number', filtercomparator: 0 }, QUERIES_INDIRECT: { name: 'Categorie zoekopdracht', typefilter: 'number', filtercomparator: 0 }, PHOTOS_VIEWS_MERCHANT: { name: 'Van eigenaars', typefilter: 'number', filtercomparator: 0 }, PHOTOS_VIEWS_CUSTOMERS: { name: 'Van bezoekers', typefilter: 'number', filtercomparator: 0 }, PHOTOS_COUNT_MERCHANT: { name: 'Van eigenaar', typefilter: 'number', filtercomparator: 0 }, PHOTOS_COUNT_CUSTOMERS: { name: 'Van bezoekers', typefilter: 'number', filtercomparator: 0 }, Date: { name: 'Datum', typefilter: 'number', filtercomparator: 0 }, AverageCpc: { name: 'Gemiddelde kosten per klik', typefilter: 'number', filtercomparator: 2, 'before': '€' }, Ctr: { name: 'Ctr', typefilter: 'number', filtercomparator: 0, 'after': '%' }, Conversions: { name: 'Conversions', typefilter: 'number', filtercomparator: 0 }, Cost: { name: 'Cost', typefilter: 'number', filtercomparator: 0, 'before': '€' }, CostPerConversion: { name: 'Kosten per conversie', typefilter: 'number', filtercomparator: 0, 'before': '€' }, ConversionRate: { name: 'Conversie ratio', typefilter: 'number', filtercomparator: 0, 'after': '%' }, AveragePosition: { name: 'Gemiddelde positie', typefilter: 'number', filtercomparator: 1 }, ConversionValue: { name: 'Totale conversiewaarde', typefilter: 'number', filtercomparator: 0, 'before': '€' }, }, searchconsole: { clicks: { typefilter: 'number', filtercomparator: 0, 'reverse': true, 'datemerge': 'sum' }, ctr: { typefilter: 'number', filtercomparator: 2, 'after': '%', 'reverse': true, 'datemerge': 'avg' }, impressions: { typefilter: 'number', filtercomparator: 0, 'reverse': true, 'datemerge': 'sum' }, Date: { typefilter: 'number', filtercomparator: 0 }, position: { typefilter: 'number', filtercomparator: 1, 'reverse': false, 'reversesign': true, 'datemerge': 'avg' }, }, youtube: { commentCount: { name: 'Clicks', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, subscriberCount: { name: 'Impressions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, videoCount: { name: 'Date', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, viewCount: { name: 'Avg cost / Click', typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, views: { name: 'Ctr', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, shares: { name: 'Conversions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, likes: { name: 'Cost', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, dislikes: { name: 'Cost / conversion', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, comments: { name: 'Conversion rate', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, subscribersGained: { name: 'AbsoluteTopImpressionPercentage', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, subscribersLost: { name: 'Conversion value', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, estimatedMinutesWatched: { name: 'Zoekimpressie aandeel', typefilter: 'number', filtercomparator: 1, 'datemerge': 'sum' }, annotationCloseRate: { name: 'Top Positie', typefilter: 'number', filtercomparator: 2, 'after': '%', 'datemerge': 'avg' }, annotationClickThroughRate: { name: 'ROAS', typefilter: 'number', filtercomparator: 2, 'after': '%', 'datemerge': 'avg' }, averageViewDuration: { name: 'ROAS', typefilter: 'secondsToHHmmss', filtercomparator: 0, 'datemerge': 'avg' }, }, analytics4: { totalUsers: { name: 'totalUsers', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, screenPageViews: { name: 'screenPageViews', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, screenPageViewsPerSession: { name: 'screenPageViewsPerSession', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, sessions: { name: 'Sessions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, newUsers: { name: 'Users', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, pageviews: { name: 'Pageviews', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, pageviewsPerSession: { name: 'Pageviews / session', typefilter: 'number', filtercomparator: 2, 'datemerge': 'avg' }, averageSessionDuration: { name: 'Average session duration', typefilter: 'secondsToHHmmss', filtercomparator: 0, 'datemerge': 'avg' }, percentNewSessions: { name: 'Percentrage new sessions', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, bounceRate: { name: 'Bounce Ratio', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, goalCompletionsAll: { name: 'Goals', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, goalValueAll: { name: 'Goalvalue', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, goalConversionRateAll: { name: 'Goalconversion ratio', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, goalAbandonRateAll: { name: 'Doel verlatings ratio', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, uniquePageviews: { name: 'Unique Pageviews', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, avgTimeOnPage: { name: 'Avg time on page', typefilter: 'secondsToHHmmss', filtercomparator: 0, 'datemerge': 'avg' }, entrances: { name: 'Entrances', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, exitRate: { name: 'Exit rate', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, pageValue: { name: 'Pagevalue', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, exits: { name: 'Exits', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, ROAS: { name: 'ROAS', typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, costPerTransaction: { name: 'Cost per transaction', typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, totalValue: { name: 'Total value', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, adCost: { name: 'Ad Cost', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, CPC: { name: 'CPC', typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, adClicks: { name: 'Ad Clicks', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, transactions: { name: 'Transacions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, uniquePurchases: { name: 'Unique Purchases', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, totalValue: { name: 'Total Value', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, revenuePerTransaction: { name: 'Revenue per Transaction', typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, itemQuantity: { name: 'Item Quentity', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, itemsPerPurchase: { name: 'items / purchase', typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, eventCount: { name: 'items / purchase', typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, eventCountPerUser: { name: 'items / purchase', typefilter: 'number', filtercomparator: 0, 'datemerge': 'avg' }, mobileDeviceBranding: { name: 'Mobile device model', typefilter: 'text', filtercomparator: 3 }, itemsPurchased: { name: 'Items purchased', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, itemRevenue: { name: 'Item revenue', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, conversions: { name: 'Conversions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, eventValue: { name: 'Event value', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' }, sessionConversionRate: { name: 'Session conversion rate', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' }, userEngagementDuration: { name: 'User engagement duration', typefilter: 'secondsToHHmmss', filtercomparator: 0, 'datemerge': 'sum' }, engagementRate: { name: 'User engagement rate', typefilter: 'number', filtercomparator: 2, 'after': '%', 'datemerge': 'avg' }, } } return allMetrics[key]; } } }); app.directive('bullet', function ($compile) { return { restrict: 'E', scope: { type: '=' }, template: '', }; }); app.directive("clientscript", function () { var updateScripts = function (element) { return function (script) { element.empty(); var scriptTag = angular.element(document.createElement("script")); scriptTag.text(script) element.append(scriptTag); }; }; return { restrict: "EA", scope: { scripts: "=" }, link: function (scope, element) { scope.$watch("scripts", updateScripts(element)); } }; }) app.filter('maxserp', function () { return function (text, length, end) { if (text == 100 || text === undefined) { return '--'; } return text; } }) app.directive('change', function () { return { restrict: 'E', replace: true, scope: { type: '@' }, template: '{{change}} ', link: function (scope) { scope.$watch('type', function () { if (scope.type == '0') { scope.change = '--'; } else if (scope.type < 1) { scope.change = scope.type + ' '; scope.class = 'fa fa-long-arrow-down text-warning'; } else { scope.change = scope.type + ' '; scope.class = 'fa fa-long-arrow-up text-success'; } }); } }; }); app.directive('infiniteScroll', [ '$rootScope', '$window', '$timeout', function ($rootScope, $window, $timeout) { return { link: function (scope, elem, attrs) { var checkWhenEnabled, handler, scrollDistance, scrollEnabled; $window = angular.element($window); scrollDistance = 0; if (attrs.infiniteScrollDistance != null) { scope.$watch(attrs.infiniteScrollDistance, function (value) { return scrollDistance = parseInt(value, 10); }); } scrollEnabled = true; checkWhenEnabled = false; if (attrs.infiniteScrollDisabled != null) { scope.$watch(attrs.infiniteScrollDisabled, function (value) { scrollEnabled = !value; if (scrollEnabled && checkWhenEnabled) { checkWhenEnabled = false; return handler(); } }); } handler = function () { var elementBottom, remaining, shouldScroll, windowBottom; windowBottom = $window.height() + $window.scrollTop(); elementBottom = elem.offset().top + elem.height(); remaining = elementBottom - windowBottom; shouldScroll = remaining <= $window.height() * scrollDistance; if (shouldScroll && scrollEnabled) { if ($rootScope.$$phase) { return scope.$eval(attrs.infiniteScroll); } else { return scope.$apply(attrs.infiniteScroll); } } else if (shouldScroll) { return checkWhenEnabled = true; } }; $window.on('scroll', handler); scope.$on('$destroy', function () { return $window.off('scroll', handler); }); return $timeout((function () { if (attrs.infiniteScrollImmediateCheck) { if (scope.$eval(attrs.infiniteScrollImmediateCheck)) { return handler(); } } else { return handler(); } }), 0); } }; } ]) app.filter('cleanurl', function () { return function (url) { return url.replace(/(^\w+:|^)\/\//, ''); }; }); app.filter('mstotime', function () { return function (bytes) { return (bytes > 1000) ? Math.round(bytes / 10) / 100 + 's' : Math.round(bytes) + 'ms'; }; }); app.filter('filename', function () { return function (url) { url = url.split(/[?#]/)[0].replace(/\/$/, ""); return url.substring(url.lastIndexOf('/') + 1) }; }); app.directive('ngGauge', gaugeMeterDirective) .provider('ngGauge', gaugeMeterProviderFn); gaugeMeterProviderFn.$inject = []; function gaugeMeterProviderFn() { var defaultOptions = { size: 200, value: undefined, min: 0, max: 100, cap: 'butt', thick: 6, type: 'full', foregroundColor: 'rgba(0, 150, 136, 1)', backgroundColor: 'rgba(0, 0, 0, 0.1)', duration: 1500, fractionSize: null, labelOnly: false, }; this.setOptions = function (customOptions) { if (!(customOptions && angular.isObject(customOptions))) throw new Error('Invalid option type specified in the ngGaugeProvider'); defaultOptions = angular.merge(defaultOptions, customOptions); }; var ngGauge = { getOptions: function () { return angular.extend({}, defaultOptions); } }; this.$get = function () { return ngGauge; }; } gaugeMeterDirective.$inject = ['ngGauge']; function gaugeMeterDirective(ngGauge) { var tpl = '
' + '{{prepend}}' + '{{value | number}}' + '{{value | number: fractionSize}}' + '{{append}}' + '{{ label }}' + '
'; var Gauge = function (element, options) { this.element = element.find('canvas')[0]; this.text = element.find('span'); this.legend = element.find('b'); this.unit = element.find('u'); this.context = this.element.getContext('2d'); this.options = options; this.init(); }; Gauge.prototype = { init: function () { this.setupStyles(); this.create(null, null); }, setupStyles: function () { this.context.canvas.width = this.options.size; this.context.canvas.height = this.options.size; this.context.lineCap = this.options.cap; this.context.lineWidth = this.options.thick; var lfs = this.options.size * 0.30, llh = this.options.size; this.text.css({ display: 'inline-block', fontWeight: 'normal', width: '100%', position: 'absolute', textAlign: 'center', overflow: 'hidden', textOverflow: 'ellipsis', fontSize: lfs + 'px', lineHeight: llh + 'px' }); this.unit.css({ textDecoration: 'none', fontSize: '0.6em', fontWeight: 200, opacity: 0.8 }); var fs = this.options.labelOnly ? lfs * 0.8 : this.options.size / 13; var lh = this.options.labelOnly ? llh : (5 * fs) + parseInt(this.options.size); this.legend.css({ display: 'inline-block', width: '100%', position: 'absolute', textAlign: 'center', overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: 'normal', fontSize: fs + 'px', lineHeight: lh + 'px' }); }, create: function (nv, ov) { var self = this, type = this.getType(), bounds = this.getBounds(type), duration = this.getDuration(), min = this.getMin(), max = this.getMax(), value = this.clamp(this.getValue(), min, max), start = bounds.head, unit = (bounds.tail - bounds.head) / (max - min), displacement = unit * (value - min), tail = bounds.tail, color = this.getForegroundColorByRange(value), requestID, startTime; if (nv && ov) { displacement = unit * nv - unit * ov; } function animate(timestamp) { timestamp = timestamp || new Date().getTime(); var runtime = timestamp - startTime; var progress = Math.min(runtime / duration, 1); // never exceed 100% var previousProgress = ov ? (ov * unit) : 0; var middle = start + previousProgress + displacement * progress; self.drawShell(start, middle, tail, color); if (runtime < duration) { requestID = window.requestAnimationFrame(function (timestamp) { animate(timestamp); }); } else { cancelAnimationFrame(requestID); } } requestAnimationFrame(function (timestamp) { startTime = timestamp || new Date().getTime(); animate(timestamp); }); }, getBounds: function (type) { var head, tail; if (type == 'semi') { head = Math.PI; tail = 2 * Math.PI; } else if (type == 'full') { head = 1.5 * Math.PI; tail = 3.5 * Math.PI; } else if (type === 'arch') { head = 0.8 * Math.PI; tail = 2.2 * Math.PI; } return { head: head, tail: tail }; }, drawShell: function (start, middle, tail, color) { var context = this.context, center = this.getCenter(), radius = this.getRadius(), foregroundColor = color, backgroundColor = this.getBackgroundColor(); this.clear(); middle = Math.max(middle, start); // never below 0% middle = Math.min(middle, tail); // never exceed 100% context.beginPath(); context.strokeStyle = backgroundColor; context.arc(center.x, center.y, radius, middle, tail, false); context.stroke(); context.beginPath(); context.strokeStyle = foregroundColor; context.arc(center.x, center.y, radius, start, middle, false); context.stroke(); }, clear: function () { this.context.clearRect(0, 0, this.getWidth(), this.getHeight()); }, update: function (nv, ov) { this.create(nv, ov); }, destroy: function () { this.clear(); }, getRadius: function () { var center = this.getCenter(); return center.x - this.getThickness(); }, getCenter: function () { var x = this.getWidth() / 2, y = this.getHeight() / 2; return { x: x, y: y }; }, getValue: function () { return this.options.value; }, getMin: function () { return this.options.min; }, getMax: function () { return this.options.max; }, getWidth: function () { return this.context.canvas.width; }, getHeight: function () { return this.context.canvas.height; }, getThickness: function () { return this.options.thick; }, getBackgroundColor: function () { return this.options.backgroundColor; }, getForegroundColor: function () { return this.options.foregroundColor; }, getForegroundColorByRange: function (value) { var isNumber = function (value) { return value != undefined && !isNaN(parseFloat(value)) && !isNaN(Number(value)); }; var match = Object.keys(this.options.thresholds) .filter(function (item) { return isNumber(item) && Number(item) <= value; }) .sort(function (a, b) { return Number(a) > Number(b); }).reverse()[0]; return match !== undefined ? this.options.thresholds[match].color || this.getForegroundColor() : this.getForegroundColor(); }, getLineCap: function () { return this.options.cap; }, getType: function () { return this.options.type; }, getDuration: function () { return this.options.duration; }, clamp: function (value, min, max) { return Math.max(min, Math.min(max, value)); } }; return { restrict: 'E', replace: true, template: tpl, scope: { append: '@?', backgroundColor: '@?', cap: '@?', foregroundColor: '@?', label: '@?', labelOnly: '@?', prepend: '@?', size: '@?', thick: '@?', type: '@?', duration: '@?', value: '=?', min: '=?', max: '=?', thresholds: '=?', fractionSize: '=?' }, link: function (scope, element) { var defaults = ngGauge.getOptions(); // fetching default settings from provider scope.min = angular.isDefined(scope.min) ? scope.min : defaults.min; scope.max = angular.isDefined(scope.max) ? scope.max : defaults.max; scope.value = angular.isDefined(scope.value) ? scope.value : defaults.value; scope.size = angular.isDefined(scope.size) ? scope.size : defaults.size; scope.cap = angular.isDefined(scope.cap) ? scope.cap : defaults.cap; scope.thick = angular.isDefined(scope.thick) ? scope.thick : defaults.thick; scope.type = angular.isDefined(scope.type) ? scope.type : defaults.type; scope.duration = angular.isDefined(scope.duration) ? scope.duration : defaults.duration; scope.labelOnly = angular.isDefined(scope.labelOnly) ? scope.labelOnly : defaults.labelOnly; scope.foregroundColor = angular.isDefined(scope.foregroundColor) ? scope.foregroundColor : defaults.foregroundColor; scope.backgroundColor = angular.isDefined(scope.backgroundColor) ? scope.backgroundColor : defaults.backgroundColor; scope.thresholds = angular.isDefined(scope.thresholds) ? scope.thresholds : {}; scope.fractionSize = angular.isDefined(scope.fractionSize) ? scope.fractionSize : defaults.fractionSize; var gauge = new Gauge(element, scope); scope.$watch('value', watchData, false); scope.$watch('min', watchData, false); scope.$watch('max', watchData, false); scope.$watch('cap', watchOther, false); scope.$watch('thick', watchOther, false); scope.$watch('type', watchOther, false); scope.$watch('size', watchOther, false); scope.$watch('duration', watchOther, false); scope.$watch('foregroundColor', watchOther, false); scope.$watch('backgroundColor', watchOther, false); scope.$watch('thresholds', watchOther, false); scope.$watch('fractionSize', watchData, false); scope.$on('$destroy', function () { }); scope.$on('$resize', function () { }); function watchData(nv, ov) { if (!gauge) return; if (!angular.isDefined(nv) || angular.equals(nv, ov)) return; gauge.update(nv, ov); } function watchOther(nv, ov) { if (!angular.isDefined(nv) || angular.equals(nv, ov)) return; gauge.destroy(); gauge.init(); } } }; } // oude /hub/v1/-adressen naar de pagina die ze vervangt function hubRedirect(p) { return {llmtraffic: '/ai-insights/verkeer', aioverview: '/ai-insights', keywordperformance: '/search-insights/gevolgd'}[p.view] || '/search-insights'; } app.config(['$routeProvider', '$locationProvider', 'USER', '$sceProvider', '$translateProvider', function AppConfig($routeProvider, $locationProvider, USER, $sceProvider, $translateProvider) { angular.lowercase = angular.$$lowercase; $translateProvider.useStaticFilesLoader({ prefix: '/translations/', suffix: '.json?r=' + Math.random() }); /* ZET */ $translateProvider.preferredLanguage(USER.site.locale.lang); $translateProvider.useSanitizeValueStrategy('sce'); $sceProvider.enabled(false); $routeProvider .when('/', { title: 'APP', templateUrl: viewfolder + '/home/index.html?r=' + r, nav: 'dashboard' }) /* help*/ .when('/help', { title: 'Help', templateUrl: viewfolder + '/help/index.html?r=' + r, nav: 'help' }) /* uptime-monitor */ .when('/uptime', { title: 'Uptime', templateUrl: viewfolder + '/uptime/index.html?r=' + r, nav: 'uptime' }) /* Posities in Google (SERP-tracker): overzicht, /serp/keywords en /serp/competitors; zichtbaar via user.site.show.serp */ .when('/serp', { title: 'Rank tracker', templateUrl: viewfolder + '/serp/index.html?r=' + r, nav: 'serp' }) /* Search Console Insights: overzicht, /onderwerpen(/:id), /gevolgd, /verkennen; zichtbaar via user.site.show.gscinsights (js/controller/gscinsights.js) */ .when('/search-insights', { title: 'Search Insights', templateUrl: viewfolder + '/gscinsights/index.html?r=' + r, nav: 'gscinsights' }) .when('/search-insights/:tab', { title: 'Search Insights', templateUrl: viewfolder + '/gscinsights/index.html?r=' + r, nav: 'gscinsights', reloadOnSearch: false }) .when('/search-insights/:tab/:id', { title: 'Search Insights', templateUrl: viewfolder + '/gscinsights/index.html?r=' + r, nav: 'gscinsights' }) .when('/ai-insights', { title: 'AI Insights', templateUrl: viewfolder + '/aiinsights/index.html?r=' + r, nav: 'aiinsights' }) .when('/ai-insights/:tab', { title: 'AI Insights', templateUrl: viewfolder + '/aiinsights/index.html?r=' + r, nav: 'aiinsights' }) .when('/ai-insights/:tab/:id', { title: 'AI Insights', templateUrl: viewfolder + '/aiinsights/index.html?r=' + r, nav: 'aiinsights' }) .when('/serp/:tab', { title: 'Rank tracker', templateUrl: viewfolder + '/serp/index.html?r=' + r, nav: 'serp' }) /* contentcheck (verstopt, alleen eigen admins: contentcheck::allowed) */ .when('/content-intelligence/check', { title: 'Contentcheck', templateUrl: viewfolder + '/content/check.html?r=' + r, nav: 'content' }) .when('/content-intelligence/check/:id', { title: 'Contentcheck', templateUrl: viewfolder + '/content/check.html?r=' + r, nav: 'content' }) /* mcp server */ .when('/mcp', { title: 'MCP Server', templateUrl: viewfolder + '/mcp/index.html?r=' + r, nav: 'mcp' }) /* onpage seo*/ .when('/onpage-seo', { title: 'ONPAGE', templateUrl: viewfolder + '/onpage/index.status.html?r=' + r, nav: 'onpage' }) .when('/onpage-seo/scan/:pageid', { title: 'ONPAGE SCAN', templateUrl: viewfolder + '/onpage/scan.html?r=' + r, nav: 'onpage', controller: function ($scope, $routeParams) { $scope.pageid = $routeParams.pageid; }, }) .when('/onpage-seo/:viewrulelabel', { title: 'ONPAGE', templateUrl: viewfolder + '/onpage/index.status.html?r=' + r, nav: 'onpage', controller: function ($scope, $routeParams) { $scope.viewrulelabel = $routeParams.viewrulelabel; }, }) .when('/onpage-seo/:viewrulelabel/:viewrule', { title: 'ONPAGE', templateUrl: viewfolder + '/onpage/index.status.html?r=' + r, nav: 'onpage', controller: function ($scope, $routeParams) { $scope.viewrulelabel = $routeParams.viewrulelabel; $scope.viewrule = $routeParams.viewrule; }, }) /* linkbuilding*/ .when('/content/', { title: 'Content Intelligence', templateUrl: viewfolder + '/content/index.html?r=' + r, nav: 'content' }) .when('/content/:modelid', { title: 'Content Intelligence', templateUrl: viewfolder + '/content/model.html?r=' + r, nav: 'content', controller: function ($scope, $routeParams) { $scope.modelid = $routeParams.modelid; }, }) /* linkbuilding*/ .when('/linkbuilding/assistant', { title: 'Linkbuilding', templateUrl: viewfolder + '/linkbuilding/assistant.html?r=' + r, nav: 'linkbuilding' }) .when('/linkbuilding/template', { title: 'Linkbuilding', templateUrl: viewfolder + '/linkbuilding/template.html?r=' + r, nav: 'linkbuilding' }) .when('/linkbuilding/wizzard', { title: 'Linkbuilding', templateUrl: viewfolder + '/linkbuilding/wizzard.html?r=' + r, nav: 'linkbuilding' }) .when('/linkbuilding/manager', { title: 'Linkbuilding', templateUrl: viewfolder + '/linkbuilding/manager.html?r=' + r, nav: 'linkbuilding' }) // LLM Visibility is opgegaan in AI Insights (2026-10-27) .when('/llm-visibility', { redirectTo: '/ai-insights/antwoorden' }) /* reports*/ .when('/analytics/:report/:view', { title: 'SITES', templateUrl: viewfolder + '/api/analytics.html?r=' + r, nav: 'analytics', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/analytics4/:report/:view', { title: 'SITES', templateUrl: viewfolder + '/api/analytics4.html?r=' + r, nav: 'analytics4', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/matomo/:report/:view', { title: 'SITES', templateUrl: viewfolder + '/api/matomo.html?r=' + r, nav: 'matomo', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) /* de oude Search Console-pagina is Search Insights geworden (2026-10-27) */ .when('/searchconsole/:report/:view', { redirectTo: '/search-insights' }) .when('/searchconsole/:report/:view/:urlfilter', { redirectTo: '/search-insights' }) .when('/youtube/:report/:view', { title: 'youtube', templateUrl: viewfolder + '/api/youtube.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) // de Performance HUB is opgegaan in Search Insights en AI Insights (2026-10-27); een functie, anders zet // Angular de routeparameters als ?report=...&view=... achter het nieuwe adres .when('/hub/:report/:view', { redirectTo: hubRedirect }) .when('/hub/:report/:view/:urlfilter', { redirectTo: hubRedirect }) .when('/mailchimp/:report/:view', { title: 'mailchimp', templateUrl: viewfolder + '/api/mailchimp.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/mailchimp/:report/:view/:filter', { title: 'mailchimp', templateUrl: viewfolder + '/api/mailchimp.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; $scope.urlfilter = $routeParams.filter; }, }) .when('/copernica/:report/:view', { title: 'copernica', templateUrl: viewfolder + '/api/copernica.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/copernica/:report/:view/:filter', { title: 'copernica', templateUrl: viewfolder + '/api/copernica.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; $scope.urlfilter = $routeParams.filter; }, }) .when('/facebookads/:report/:view', { title: 'facebookads', templateUrl: viewfolder + '/api/facebookxads.html?r=' + r, nav: 'ads', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/facebookinsights/:report/:view', { title: 'facebookinsights', templateUrl: viewfolder + '/api/facebookinsights.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/instagram/:report/:view', { title: 'instagram', templateUrl: viewfolder + '/api/instagram.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/linkedin/:report/:view', { title: 'linkedin', templateUrl: viewfolder + '/api/linkedin.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/linkedinxads/:report/:view', { title: 'linkedinxads', templateUrl: viewfolder + '/api/linkedinxads.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/googleads/:report/:view', { title: 'googleads', templateUrl: viewfolder + '/api/googlexads.html?r=' + r, nav: 'ads', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/mybusiness/:report/:view', { title: 'MYBUSINESS', templateUrl: viewfolder + '/api/mybusiness.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; $scope.urlview = $routeParams.view; }, }) .when('/planning', { title: 'planning', templateUrl: viewfolder + '/planning/index.html?r=' + r, nav: 'planning', }) .when('/site', { title: 'SITES', templateUrl: viewfolder + '/site/index.html?r=' + r, nav: 'sites', }) .when('/site/upgrade/:orderstate', { title: 'SITES', templateUrl: viewfolder + '/site/upgrade.html?r=' + r, nav: 'sites', controller: function ($scope, $routeParams) { $scope.orderstate = $routeParams.orderstate; }, allowguest: true }) .when('/site/upgrade', { title: 'SITES', templateUrl: viewfolder + '/site/upgrade.html?r=' + r, nav: 'sites', allowguest: true }) .when('/sites', { title: 'SITES', templateUrl: viewfolder + '/sites/index.html?r=' + r, nav: 'sites', }) .when('/sites/new', { title: 'Add a new site', templateUrl: viewfolder + '/sites/first.html?r=' + r, nav: 'sites', }) .when('/rank-tracker', { title: 'Rank Tracker', templateUrl: viewfolder + '/rank-tracker/index.html?r=' + r, nav: 'ranktracker', }) .when('/rank-tracker/competitors', { title: 'Rank Tracker - Competitors', templateUrl: viewfolder + '/rank-tracker/competitors.html?r=' + r, nav: 'ranktracker', }) .when('/rank-tracker/insights', { title: 'Rank Tracker - Insights', templateUrl: viewfolder + '/rank-tracker/insights.html?r=' + r, nav: 'ranktracker', }) .when('/keywordtool', { title: 'Rank Tracker', templateUrl: viewfolder + '/keywordtool/index.html?r=' + r, nav: 'keywordtool', }) .when('/insights', { title: 'Insights', templateUrl: viewfolder + '/insights/index.html?r=' + r, nav: 'insights', }) .when('/insights/:report', { title: 'Insights', templateUrl: viewfolder + '/insights/index.html?r=' + r, nav: 'tools', controller: function ($scope, $routeParams) { $scope.urlreport = $routeParams.report; }, }) /*report*/ .when('/report', { title: 'Report', templateUrl: viewfolder + '/report/index.html?r=' + r, nav: 'report', }) .when('/report/:report_id', { title: 'Report', templateUrl: viewfolder + '/report/report.html?r=' + r, nav: 'report', controller: function ($scope, $routeParams) { $scope.report_id = $routeParams.report_id; }, }) .when('/reportpage/:key', { title: 'Report', templateUrl: viewfolder + '/report/page.html?r=' + r, nav: 'report', controller: function ($scope, $routeParams) { $scope.key = $routeParams.key; }, }) /* guest*/ .when('/sites/first', { title: 'Add your first site', templateUrl: viewfolder + '/sites/first.html?r=' + r, nav: 'sites', allowguest: true, }) .when('/user/login', { title: 'LOGIN', allowguest: true, nav: 'user', class: 'fullscreen', templateUrl: viewfolder + '/user/login.html?r=' + r, }) .when('/user', { title: 'USER', nav: 'user', templateUrl: viewfolder + '/user/index.html?r=' + r, }) .when('/user/register', { title: 'LOGIN', nav: 'user', class: 'fullscreen', templateUrl: viewfolder + '/user/register.html?r=' + r, allowguest: true, }) .when('/user/recover', { title: 'RECOVER PASSWORD', templateUrl: viewfolder + '/user/recover.html?r=' + r, nav: 'user', class: 'fullscreen', allowguest: true, }) .when('/settings/reseller', { title: 'RESELLER', templateUrl: viewfolder + '/reseller/index.html?r=' + r, nav: 'user', allowguest: true, }) .when('/settings/reseller/:tab', { title: 'RESELLER', templateUrl: viewfolder + '/reseller/index.html?r=' + r, nav: 'user', allowguest: true, }) .when('/settings/site', { title: 'RESELLER', templateUrl: viewfolder + '/site/index.html?r=' + r, nav: 'user', allowguest: false, }) .otherwise({ redirectTo: '/' }); // enable html5Mode for pushstate ('#'-less URLs) $locationProvider.html5Mode(true); }]); app.run(['$route', '$rootScope', '$location', '$window', 'USER', function ($route, $rootScope, $location, $window, USER) { $rootScope.r = r; $rootScope.user = USER; //set menu state $rootScope.app = { body: { class: ['md-open', 'closed'] } }; $rootScope.$on('$routeChangeSuccess', function (event, current, prev) { $rootScope.app = { body: { class: ['md-open', 'closed'] } }; $rootScope.modal = {}; if (!$rootScope.user._id && !$route.current.allowguest) { $location.path('/user/login'); } else if (!$rootScope.user.site._id && !$route.current.allowguest) { $location.path('/sites/first'); } else if (!$route.current.allowguest && $rootScope.user.site._id && current.originalPath != '/site/upgrade' && $rootScope.user.site.status.expire - (Date.now() / 1000) < 0) { //if ($rootScope.user.reseller.config.own) { $location.path('/site/upgrade'); //} } $rootScope.app.meta = { nav: $route.current.nav, class: $route.current.class, title: USER.reseller.config.name + ' - ' + $route.current.title, description: $route.current.description }; $rootScope.app.sitesdropdown = false; }); }]) app.controller('core', ['$scope', '$rootScope', '$location', 'h', '$templateRequest', '$window', '$http', '$route', '$translate', '$http', function($scope, $rootScope, $location, h, $templateRequest, $window, $http, $route, $translate, $http) { $scope.fnToggleMenu = function() { $rootScope.app.body.class = ($rootScope.app.body.class[0] == 'md-open') ? ['md-closed', 'open'] : ['md-open', 'closed']; } $scope.fnLogout = function() { $http.post('/rest/user/logout').then(function successCallback(res) { h.login(function() { $location.path('/user/login'); }); }); } $scope.fnModal = function(url) { $rootScope.modal = {}; $rootScope.modal.show = true; $rootScope.modal.url = viewfolder+'/' + url + '.html'; } $scope.fnCloseModal = function() { $rootScope.modal = {}; } $scope.fnClick = function(path) { $location.path(path); } $scope.fnGoto = function(path) { //$location.path(path); $window.location.href = path; } $scope.fnSetLang = function(lang) { $rootScope.user.site.locale.lang = lang; $translate.use(lang); $http.get('/rest/global/lang.set', { params: { 'lang': lang, } }); } $scope.fnChangeSite = function(site) { $http.post('/rest/sites/change', { websiteid: site._id.$oid }).then(function successCallback(res) { h.login(function() { $rootScope._pages = false; $rootScope._history = false; if ($location.path() == '/site/upgrade') { $scope.fnClick('/'); } $route.reload(); }); }); } // de lijst pas opbouwen bij de eerste klik; openen en sluiten doet Bootstrap zelf // (omdraaien liep uit de pas met Bootstrap: na sluiten zonder keuze bleef de lijst leeg) $scope.fnSitesDropdown = function(site) { $rootScope.app.sitesdropdown = true; } $scope.fnDaysLeft = function() { return Math.ceil(($rootScope.user.site.status.expire - (Date.now() / 1000)) / (24 * 60 * 60)); } $scope.isValidSite = function() { return ($rootScope.user.site.status.expire > (Date.now() / 1000)) ? true : false; } $scope.colors = ['#E91E63', '#3F51B5', '#00BCD4', '#8BC34A', '#FF9800', '#795548', '#607D8B', '#00CC00', '#0000CC', '#DDDDDD', '#999999', '#333333', '#990000'] $scope.fnPdf = function() { console.log('asdasdasd'); var imgs = document.getElementsByTagName('canvas'); for (var i in imgs) { if (typeof(imgs[i]) == 'object') { var img = imgs[i].toDataURL("image/png"); var replace = document.createElement("img"); replace.src = img; imgs[i].parentNode.replaceChild(replace, imgs[i]); } } $http.post("/rest/report/page.download", { html: angular.element('.reportcontainer').html() }).then(function successCallback(res) { $scope.fnGoto('https://robinssl.seoserver.nl/engels/page.php?website_id=' + h.siteId() + '&reseller_id=' + $scope.user.reseller._id.$oid + '&key=' + res.data.k) /*$http.get('https://robinssl.seoserver.nl/engels/page.php', { 'params': { 'website_id': h.siteId(), 'reseller_id': $scope.user.reseller._id.$oid, 'key': res.data.k } }).then(function successCallback(pdf) { var newBlob = new Blob([pdf.data], { type: "application/pdf" }) console.log(newBlob); });*/ }); } } ]); // Dashboard (/): widgets per site in de stijl van de nieuwe pagina's (docs/ui.md). Iedereen deelt per site dezelfde // indeling; rol 4 of lager mag hem aanpassen: slepen, breedte trekken aan de rechterrand, instellingen, weghalen, en // widgets of een hele indeling kiezen in de widgetkiezer. Opslag en regels: rest/include/home.php en rest/data/home/. // Arjen (2026-10-28): wie al een dashboard had, ziet dezelfde widgets op dezelfde plek; alleen nieuwe sites krijgen de // nieuwe standaardindeling. Search Insights en AI Insights tekenen hun tegels en grafieken met dezelfde code als hun // eigen pagina (gsciView, aiView). app.controller('home', ['$scope', '$rootScope', '$http', '$translate', '$timeout', '$q', 'h', 'mtFmt', 'gsciView', 'aiView', function($scope, $rootScope, $http, $translate, $timeout, $q, h, mtFmt, gsciView, aiView) { if ($rootScope.user.firstrun) { $scope.fnModal('home/modal.firstrun'); } $scope.fnCloseFirstRun = function() { $http.get('/rest/user/firstrun.set'); $rootScope.user.firstrun = false; $scope.fnCloseModal(); } // favoriet: de huidige site in user.sites (favorieten staan bovenaan in de siteswitcher) var fnCurrentSite = function() { return ($rootScope.user.sites || []).find(function(site) { return site._id && site._id.$oid == h.siteId(); }); } $scope.isFavorite = function() { var site = fnCurrentSite(); return !!(site && site.favorite); } $scope.fnFavorite = function() { var site = fnCurrentSite(); if (!site) { return; } site.favorite = !site.favorite; $http.post('/rest/sites/favorite', { websiteid: h.siteId(), favorite: site.favorite }).then(function successCallback(res) { if (res.data.cb != 'ok') { site.favorite = !site.favorite; } }, function errorCallback() { site.favorite = !site.favorite; }); } var t = function (k, v) { return $translate.instant(k, v); }; var f = mtFmt; var show = ($rootScope.user.site && $rootScope.user.site.show) || {}; var WIDTHS = [3, 4, 6, 8, 9, 12]; var MAX = 24; var GA4_METRICS = ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser']; var ADS_METRICS = ['Clicks', 'Impressions', 'Cost', 'AverageCpc', 'Ctr', 'Conversions', 'CostPerConversion', 'ConversionRate', 'ConversionValue', 'SearchImpressionShare', 'SearchAbsoluteTopImpressionShare', 'roas']; var GSC_METRICS = ['clicks', 'impressions', 'ctr', 'position']; // Analytics 4 per kanaal (sessionDefaultChannelGroup; analytics4/report?channel=) var GA4_CHANNELS = { organic: 'Organic Search', paid: 'Paid Search', direct: 'Direct', referral: 'Referral', social: 'Organic Social', email: 'Email' }; // gemiddelden tel je niet op: in de kop het gemiddelde in plaats van het totaal var AVERAGES = ['screenPageViewsPerSession', 'averageSessionDuration', 'bounceRate', 'eventCountPerUser', 'AverageCpc', 'Ctr', 'CostPerConversion', 'ConversionRate', 'SearchImpressionShare', 'SearchAbsoluteTopImpressionShare', 'roas', 'ctr', 'position']; var ICONS = { all: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z', gsc: 'M21 21l-4.3-4.3M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14z', ai: 'M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z', seo: 'M9 11l3 3 8-8M20 12v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11', rank: 'M4 20V10M10 20V4M16 20v-6M22 20H2', up: 'M3 12h4l3-8 4 16 3-8h4', ga: 'M4 20v-6M10 20V8M16 20v-9M22 20V4', ads: 'M3 11l18-8-8 18-2-8z', star: 'M12 3l2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.3 6.4 20.2l1.1-6.2L3 9.6l6.2-.9z', layout: 'M3 4h18v6H3zM3 14h8v6H3zM15 14h6v6h-6z' }; // de groepen in de widgetkiezer, met de tool die in het menu moet staan var CATS = [ { key: 'all_tools', icon: ICONS.all, tool: null }, { key: 'gscinsights', icon: ICONS.gsc, tool: 'gscinsights', href: '/search-insights' }, { key: 'aiinsights', icon: ICONS.ai, tool: 'aiinsights', href: '/ai-insights' }, { key: 'onpage', icon: ICONS.seo, tool: 'onpageseo', href: '/onpage-seo' }, { key: 'serp', icon: ICONS.rank, tool: 'serp', href: '/serp' }, { key: 'uptime', icon: ICONS.up, tool: 'uptime', href: '/uptime' }, { key: 'analytics', icon: ICONS.ga, tool: 'analytics', href: '/analytics4/channels/all' }, { key: 'googleads', icon: ICONS.ads, tool: 'googleads', href: '/googleads/campaigns/all' } ]; var CAT = {}; CATS.forEach(function (c) { CAT[c.key] = c; }); // alle widgets (zelfde lijst als homeWidgets::TYPES): groep, voorbeeldplaatje, standaardbreedte, instellingen var WIDGETS = { // De stand in één zin rekent met Search Insights: zonder die tool niet te kiezen summary: { cat: 'all_tools', kind: 'sentence', span: 12, tip: true, needs: 'gscinsights' }, todo: { cat: 'all_tools', kind: 'list', span: 8, tip: true }, si_kpis: { cat: 'gscinsights', kind: 'kpis', span: 12, tip: true }, si_chart: { cat: 'gscinsights', kind: 'bars', span: 8, metrics: GSC_METRICS, metric: 'clicks', tip: true }, si_changes: { cat: 'gscinsights', kind: 'list', span: 4 }, si_kansen: { cat: 'gscinsights', kind: 'list', span: 6 }, searchinsights_tracked: { cat: 'gscinsights', kind: 'table', span: 6, more: '/search-insights/gevolgd' }, searchconsole_timeline: { cat: 'gscinsights', kind: 'line', span: 6, metrics: GSC_METRICS, metric: 'clicks' }, searchconsole_chart: { cat: 'gscinsights', kind: 'top', span: 6, metrics: GSC_METRICS, metric: 'clicks', dimensions: ['query', 'page', 'country', 'device'], dimension: 'query' }, searchconsole_keywords: { cat: 'gscinsights', kind: 'table', span: 6, more: '/search-insights/verkennen' }, ai_kpis: { cat: 'aiinsights', kind: 'kpis', span: 12 }, ai_chart: { cat: 'aiinsights', kind: 'bars', span: 6, more: '/ai-insights/verkeer' }, onpage_seo: { cat: 'onpage', kind: 'kpis', span: 12 }, onpage_changes: { cat: 'onpage', kind: 'list', span: 6, more: '/onpage-seo/changes' }, onpage_changes_donut: { cat: 'onpage', kind: 'counts', span: 4, more: '/onpage-seo/changes' }, ranktracer: { cat: 'serp', kind: 'dist', span: 6 }, uptime: { cat: 'uptime', kind: 'heat', span: 4 }, analytics4_timeline: { cat: 'analytics', kind: 'line', span: 4, metrics: GA4_METRICS, metric: 'sessions', filters: Object.keys(GA4_CHANNELS) }, googleads_timeline: { cat: 'googleads', kind: 'line', span: 6, metrics: ADS_METRICS, metric: 'Clicks' }, googleads_performance_chart: { cat: 'googleads', kind: 'top', span: 6, metrics: ADS_METRICS, metric: 'Clicks', dimensions: ['CampaignName', 'AdGroupName', 'Query'], dimension: 'CampaignName' }, googleads_conversion_chart: { cat: 'googleads', kind: 'top', span: 6, metrics: ['Conversions', 'CostPerConversion', 'ConversionRate', 'ConversionValue'], metric: 'Conversions', dimensions: ['ConversionTypeName'], dimension: 'ConversionTypeName' } }; var offered = function (type) { var w = WIDGETS[type], tool = w && (w.needs || CAT[w.cat].tool); return !!w && (!tool || !!show[tool]); }; var d = $scope.d = { f: f, icons: ICONS, widgets: [], loaded: false, edit: false, compare: 'prev', hasCompare: false, canEdit: ($rootScope.user.site && $rootScope.user.site.role <= 4), max: MAX, picker: null, templates: [], canTemplates: false, confirm: null, form: null, drag: null, over: null, settings: null }; // ------------------------------------------------------------------ gegevens // elk adres één keer per bezoek: widgets die hetzelfde nodig hebben (De stand, Doe dit eerst, de tegels) delen het var cache = {}; var get = function (url, params) { var p = angular.extend({ websiteid: h.siteId() }, params || {}); var key = url + '|' + JSON.stringify(p); if (!cache[key]) { cache[key] = $http.get(url, { params: p }).then(function (res) { return res.data; }, function (res) { return { error: res.status === 403 ? 'no access' : 'failed' }; }); } return cache[key]; }; var state = function (kind, extra) { return angular.extend({ state: kind }, extra || {}); }; var connect = function (cat) { return state('connect', { href: CAT[cat].href }); }; // Search Console: niet gekoppeld of geen property gekozen = koppelen; anders een fout. // gscinsights/report zonder koppeling: action (getcustomer, of connect met voorbeelddata die we niet tonen) var gscState = function (x) { if (x && x.action) { return connect('gscinsights'); } if (!x || !x.error) { return null; } return x.error === 'not_connected' || x.error === 'no_property' ? connect('gscinsights') : state('error'); }; // pad zonder domein; de scan bewaart adressen met en zonder https:// var path = function (u) { return String(u || '').replace(/^(https?:\/\/)?[^\/]+/, '') || '/'; }; var countryName = function (cc) { try { return cc ? new Intl.DisplayNames([$translate.use() || 'nl'], { type: 'region' }).of(cc.toUpperCase()) : ''; } catch (e) { return cc; } }; var metricFmt = function (m) { if (m === 'ctr' || m === 'bounceRate' || m === 'Ctr' || m === 'ConversionRate' || m === 'SearchImpressionShare' || m === 'SearchAbsoluteTopImpressionShare') { return function (v) { return f.pct(v); }; } if (m === 'position') { return f.pos; } if (m === 'Cost' || m === 'AverageCpc' || m === 'CostPerConversion' || m === 'ConversionValue') { return f.euro; } if (m === 'screenPageViewsPerSession' || m === 'eventCountPerUser' || m === 'roas') { return function (v) { return f.num(v, 2); }; } return function (v) { return f.num(v); }; }; // een reeks per dag (GA4, Google Ads, Search Console) als lijn, met het totaal of gemiddelde in de kop var dayLine = function (rows, dateKey, metric, label) { rows = (rows || []).slice().sort(function (a, b) { return String(a[dateKey]) < String(b[dateKey]) ? -1 : 1; }); var dates = rows.map(function (r) { var s = String(r[dateKey]); return s.length === 8 ? s.substr(0, 4) + '-' + s.substr(4, 2) + '-' + s.substr(6, 2) : s.substr(0, 10); }); var values = rows.map(function (r) { var v = parseFloat(r[metric]); return isNaN(v) ? null : v; }); var sum = values.reduce(function (a, v) { return a + (v || 0); }, 0); var fmt = metricFmt(metric); return { kind: 'line', fmt: fmt, invert: metric === 'position', big: values.length ? fmt(AVERAGES.indexOf(metric) >= 0 ? sum / values.length : sum) : '–', bigLabel: AVERAGES.indexOf(metric) >= 0 ? t('home.w.average') : t('home.w.total'), chart: { labels: dates.map(function (x, i) { return i % 7 === 0 ? f.day(x) : ''; }), titles: dates.map(function (x) { return f.day(x); }), series: [{ label: label, color: '#2563eb', values: values, width: 2.5, strong: true }] } }; }; // de grootste 8 per dimensie, met een balk naar verhouding var topRows = function (rows, dim, metric, label) { var fmt = metricFmt(metric); rows = (rows || []).slice().sort(function (a, b) { return (parseFloat(b[metric]) || 0) - (parseFloat(a[metric]) || 0); }).slice(0, 8); var max = rows.length ? Math.max.apply(null, rows.map(function (r) { return parseFloat(r[metric]) || 0; })) : 0; return { kind: 'top', rows: rows.map(function (r) { var v = parseFloat(r[metric]) || 0; return { label: label(r), value: fmt(v), share: max ? 100 * v / max : 0 }; }) }; }; var LOAD = { si_kpis: function (w) { return get('/rest/gscinsights/overview', { part: 'kpis', compare: d.compare }).then(function (x) { return gscState(x) || { kind: 'kpis', kpis: gsciView.kpis(x.totals.now, x.totals.before, x.ga4) }; }); }, si_chart: function (w) { return get('/rest/gscinsights/overview', { part: 'chart', compare: 'prev' }).then(function (x) { if (gscState(x)) { return gscState(x); } var c = gsciView.overviewChart(x, w.metric || 'clicks'); return { kind: c.chartKind === 'line' ? 'line' : 'bars', chart: c.chart, fmt: c.chartFmt, invert: c.chartInvert, title: c.chartTitle, sub: c.brandNote }; }); }, si_changes: function (w) { return get('/rest/gscinsights/overview', { part: 'why', compare: d.compare }).then(function (x) { if (gscState(x)) { return gscState(x); } var why = gsciView.why(x.why); if (!why.rows.length) { return state('empty', { text: t('home.w.no_changes') }); } return { kind: 'list', sub: why.text, rows: why.rows.slice(0, 6).map(function (r) { return { lead: (r.up ? '+' : '') + r.diff, leadCls: r.up ? 'text-green-600' : 'text-red-600', text: r.q, sub: r.why, href: r.href }; }) }; }); }, si_kansen: function (w) { return get('/rest/gscinsights/overview', { part: 'actions', compare: 'prev' }).then(function (x) { if (gscState(x)) { return gscState(x); } if (!x.actions.length) { return state('empty', { text: t('home.w.no_chances') }); } return { kind: 'list', rows: x.actions.slice(0, 6).map(function (a) { return { badge: a.kind, text: gsciView.actionText(a), end: a.gain ? '+' + f.num(a.gain) + ' ' + t('home.w.clicks') : '', href: gsciView.exploreHref(a.query) }; }) }; }); }, searchinsights_tracked: function (w) { return get('/rest/gscinsights/tracked').then(function (x) { if (gscState(x)) { return gscState(x); } var months = (x.months || []).map(function (m) { return f.monthLong(m); }); var rows = []; (x.groups || []).forEach(function (g) { (g.keywords || []).forEach(function (k) { rows.push(k); }); }); if (!rows.length) { return state('empty', { text: t('gscinsights.intro.tracked'), href: '/search-insights/gevolgd', action: t('gscinsights.tracked.add') }); } rows.sort(function (a, b) { return b.now.clicks - a.now.clicks; }); return { kind: 'tracked', months: months, rows: rows.slice(0, 8).map(function (k) { return { q: k.query, pos: f.pos(k.now.position), posDelta: k.year && k.year.position && k.now.position ? k.now.position - k.year.position : null, clicks: f.num(k.now.clicks), change: k.change, spark: k.monthly_pos }; }) }; }); }, searchconsole_timeline: function (w) { var m = w.metric || 'clicks'; return get('/rest/gscinsights/report', { dimension: 'date', end: 'TODAY', metrics: m, start: '30DAYSAGO', report: 'searchAnalytics' }).then(function (x) { return gscState(x) || dayLine(x.data, 'date', m, t('gscinsights.kpi.' + m)); }); }, searchconsole_chart: function (w) { var m = w.metric || 'clicks', dim = w.dimension || 'query'; return get('/rest/gscinsights/report', { dimension: dim, end: 'TODAY', metrics: m, start: '30DAYSAGO' }).then(function (x) { return gscState(x) || topRows(x.data, dim, m, function (r) { if (dim === 'page') { return path(r.page); } if (dim === 'country') { return countryName(r.cc) || String(r.country || '').toUpperCase(); } if (dim === 'device') { return t('gscinsights.explore.device_name.' + r.device); } return r[dim]; }); }); }, searchconsole_keywords: function (w) { return get('/rest/gscinsights/report', { dimension: 'query', end: 'TODAY', metrics: 'clicks,ctr,position,impressions', start: '30DAYSAGO', report: 'searchAnalytics' }).then(function (x) { if (gscState(x)) { return gscState(x); } return { kind: 'keywords', rows: (x.data || []).slice(0, 10).map(function (r) { return { q: r.query, pos: f.pos(r.position), clicks: f.num(r.clicks), impressions: f.num(r.impressions), ctr: f.pct(r.ctr) }; }) }; }); }, ai_kpis: function (w) { return get('/rest/aiinsights/overview', { part: 'kpis' }).then(function (x) { return x.error ? state('error') : { kind: 'kpis', kpis: aiView.kpis(x) }; }); }, ai_chart: function (w) { return get('/rest/aiinsights/overview', { part: 'chart' }).then(function (x) { if (x.error) { return state('error'); } var c = aiView.chart(x); return c ? { kind: 'bars', chart: c } : connect('analytics'); }); }, onpage_seo: function (w) { return get('/rest/onpage/list').then(function (x) { var hist = (x && x.history) || []; if (!hist.length) { return state('empty', { text: t('home.w.no_scan') }); } var last = hist[hist.length - 1], prev = hist.length > 1 ? hist[hist.length - 2] : null; var tile = function (key, label) { return { label: label, value: f.pct(last[key] || 0, 0), delta: prev ? (last[key] || 0) - (prev[key] || 0) : null, unit: 'pt' }; }; var kpis = [tile('all', t('home.onpage.overallseo')), tile('content', t('home.onpage.contentseo')), tile('technical', t('home.onpage.technicalseo')), tile('mobile', t('home.onpage.mobileseo')), tile('indexability', t('menu.onpage.indexability'))]; kpis[0].note = t('home.w.vs_prev_scan'); return { kind: 'kpis', kpis: kpis }; }); }, onpage_changes: function (w) { return $q.all([get('/rest/onpage/rules', { lang: $rootScope.user.site.locale.lang }), get('/rest/onpage/changes')]).then(function (r) { var rules = (r[0] && r[0].rules) || {}, list = ((r[1] && r[1].data) || []).filter(function (c) { return c.url; }); if (!list.length) { return state('empty', { text: t('home.changes.text_paragraph_no_changes') }); } return { kind: 'list', rows: list.slice(0, 6).map(function (c) { var rule = rules[c.rule] ? rules[c.rule].title : c.rule; return { mark: c.r === true ? 'ok' : (c.r === false ? 'bad' : 'info'), text: path(c.url), sub: rule + (c.to ? ': ' + c.to : ''), href: c.page_id && c.page_id.$oid ? '/onpage-seo/scan/' + c.page_id.$oid : null }; }) }; }); }, onpage_changes_donut: function (w) { return get('/rest/onpage/changes').then(function (x) { var n = { done: 0, issue: 0, change: 0 }; ((x && x.data) || []).forEach(function (c) { n[c.r === true ? 'done' : (c.r === false ? 'issue' : 'change')]++; }); var total = n.done + n.issue + n.change; if (!total) { return state('empty', { text: t('home.changes.text_paragraph_no_changes') }); } return { kind: 'counts', total: total, items: [ { label: t('home.w.count_issue'), n: n.issue, color: '#f59e0b', share: 100 * n.issue / total }, { label: t('home.w.count_done'), n: n.done, color: '#16a34a', share: 100 * n.done / total }, { label: t('home.w.count_change'), n: n.change, color: '#9ca3af', share: 100 * n.change / total } ] }; }); }, // Rank tracker: de posities uit de laatste meting, met het verschil met de meting daarvoor (zoals op /serp) ranktracer: function (w) { return get('/rest/serp/get').then(function (x) { if (x.error) { return state('error'); } if (x.trial) { return state('empty', { text: t('serp.trial_text') }); } var last = function (r, before) { for (var i = (before === undefined ? r.measured.length : before) - 1; i >= 0; i--) { if (r.measured[i]) { return i; } } return -1; }; var depth = x.depth || 20; var rows = (x.keywords || []).map(function (r) { var i = last(r), j = i < 0 ? -1 : last(r, i); var pos = i < 0 ? undefined : r.series[i], prev = j < 0 ? undefined : r.series[j]; var rank = function (p) { return p === null ? depth + 1 : p; }; return { q: r.q, cc: r.cc, device: r.device, star: r.star, waiting: i < 0, pos: pos, label: pos === undefined ? '' : (pos === null ? '> ' + depth : pos), diff: pos === undefined || prev === undefined ? 0 : rank(prev) - rank(pos) }; }); if (!rows.length) { return state('empty', { text: t('serp.empty_text'), href: '/serp/keywords', action: t('serp.add') }); } var measured = rows.filter(function (r) { return !r.waiting; }); rows.sort(function (a, b) { var pa = a.waiting ? 1000 : (a.pos === null ? 999 : a.pos), pb = b.waiting ? 1000 : (b.pos === null ? 999 : b.pos); return (b.star - a.star) || (pa - pb); }); var count = function (lo, hi) { return measured.filter(function (r) { return r.pos && r.pos >= lo && r.pos <= hi; }).length; }; var dist = [ { label: t('home.w.top3'), n: count(1, 3) }, { label: '4–10', n: count(4, 10) }, { label: '11–' + depth, n: count(11, depth) }, { label: t('home.w.not_found'), n: measured.filter(function (r) { return r.pos === null; }).length, grey: true } ]; dist.forEach(function (x2) { x2.share = measured.length ? 100 * x2.n / measured.length : 0; }); return { kind: 'rank', total: rows.length, top10: count(1, 10), dist: dist, rows: rows.slice(0, 6) }; }); }, uptime: function (w) { return get('/rest/uptime/get').then(function (x) { if (x.error) { return state('error'); } if (!x.enabled && !x.last_check_at) { return state('empty', { text: t('uptime.intro_text'), href: '/uptime', action: t('uptime.enable') }); } var cls = { up: 'bg-green-500', warn: 'bg-amber-400', blocked: 'bg-amber-400', failing: 'bg-amber-400', down: 'bg-red-500' }; var pill = { up: 'bg-green-100 text-green-800', down: 'bg-red-100 text-red-800', pending: 'bg-gray-100 text-gray-700' }; return { kind: 'uptime', stateText: t('uptime.state.' + x.state), stateCls: pill[x.state] || 'bg-amber-100 text-amber-800', since: x.state === 'up' && x.up_since ? t('home.w.since', { date: f.day(x.up_since) }) : (x.state === 'down' && x.down_since ? t('home.w.since', { date: f.day(x.down_since) }) : ''), bars: (x.bars || []).map(function (b) { return { cls: cls[b.outcome] || 'bg-gray-200', title: f.day(b.at) + ' ' + new Date(b.at).getHours() + ':00' + (b.outcome ? ' · ' + t('uptime.state.' + b.outcome) : '') }; }), first: x.bars && x.bars.length ? f.day(x.bars[0].at) : '', up7: x.uptime_7d === null ? '–' : f.pct(x.uptime_7d, x.uptime_7d === 100 ? 0 : 2), ttfb: x.ttfb_median_7d === null ? '–' : f.num(x.ttfb_median_7d) + ' ms', cert: x.cert_expires ? f.day(x.cert_expires) : '–' }; }); }, analytics4_timeline: function (w) { var m = w.metric || 'sessions'; var p = { dimension: 'date', end: 'TODAY', metrics: m, start: '30DAYSAGO' }; if (GA4_CHANNELS[w.filter]) { p.channel = GA4_CHANNELS[w.filter]; } return get('/rest/analytics4/report', p).then(function (x) { if (x.action) { return connect('analytics'); } return x.error ? state('error') : dayLine(x.data, 'date', m, t('metrics.' + m)); }); }, googleads_timeline: function (w) { var m = w.metric || 'Clicks'; return get('/rest/googlexads/report', { dimension: 'Date', end: 'TODAY', metrics: m, start: '30DAYSAGO', report: 'ACCOUNT_PERFORMANCE_REPORT' }).then(function (x) { if (x.action) { return connect('googleads'); } return x.error ? state('error') : dayLine(x.data, 'Date', m, t('metrics.' + m)); }); }, googleads_performance_chart: function (w) { var REPORT = { CampaignName: 'CAMPAIGN_PERFORMANCE_REPORT', AdGroupName: 'ADGROUP_PERFORMANCE_REPORT', Query: 'SEARCH_QUERY_PERFORMANCE_REPORT' }; var m = w.metric || 'Clicks', dim = w.dimension || 'CampaignName'; return get('/rest/googlexads/report', { dimension: dim, end: 'TODAY', metrics: m, start: '30DAYSAGO', report: REPORT[dim] || REPORT.CampaignName }).then(function (x) { if (x.action) { return connect('googleads'); } return x.error ? state('error') : topRows(x.data, dim, m, function (r) { return r[dim]; }); }); }, googleads_conversion_chart: function (w) { var m = w.metric || 'Conversions'; return get('/rest/googlexads/report', { dimension: 'ConversionTypeName', end: 'TODAY', metrics: m, start: '30DAYSAGO', report: 'ACCOUNT_PERFORMANCE_REPORT' }).then(function (x) { if (x.action) { return connect('googleads'); } return x.error ? state('error') : topRows(x.data, 'ConversionTypeName', m, function (r) { return r.ConversionTypeName; }); }); }, // De stand in één zin: bezoek uit Google (en AI, als dat er is) tegenover de vorige periode, plus de grootste kans. // AI kent alleen de vorige 28 dagen: tegenover vorig jaar telt alleen Google. summary: function (w) { var withAi = show.aiinsights && d.compare === 'prev'; return $q.all([ get('/rest/gscinsights/overview', { part: 'kpis', compare: d.compare }), withAi ? get('/rest/aiinsights/overview', { part: 'kpis' }) : $q.when(null), get('/rest/gscinsights/overview', { part: 'actions', compare: 'prev' }) ]).then(function (r) { if (gscState(r[0])) { return gscState(r[0]); } var g = r[0].totals, ai = r[1] && !r[1].error && r[1].traffic ? r[1].traffic : null; var now = g.now.clicks + (ai ? ai.now.sessions : 0); var before = g.before ? g.before.clicks + (ai ? ai.before.sessions : 0) : null; var ch = before ? 100 * (now - before) / before : null; var chance = ((r[2] && r[2].actions) || []).filter(function (a) { return a.kind === 'chance'; })[0]; return { kind: 'summary', lead: t(ai ? 'home.w.summary_both' : 'home.w.summary_google', { n: f.num(now) }), change: ch === null ? '' : t(ch >= 0 ? 'home.w.summary_more' : 'home.w.summary_less', { pct: f.num(Math.abs(ch)) + '%' }), up: ch !== null && ch >= 0, vs: ch === null ? '' : t(d.compare === 'year' ? 'home.w.vs_year' : 'home.w.vs_prev'), chance: chance ? t('home.w.summary_chance', { q: chance.query, gain: f.num(chance.gain) }) : '', chanceHref: chance ? gsciView.exploreHref(chance.query) : null }; }); }, // Doe dit eerst: storingen en nieuwe scanfouten bovenaan, dan de kansen uit Google op opbrengst, dan AI todo: function (w) { var none = $q.when(null); return $q.all([ show.uptime ? get('/rest/uptime/get') : none, show.onpageseo ? get('/rest/onpage/changes') : none, show.gscinsights ? get('/rest/gscinsights/overview', { part: 'actions', compare: 'prev' }) : none, show.aiinsights ? get('/rest/aiinsights/overview', { part: 'actions' }) : none ]).then(function (r) { var items = [], weekAgo = Date.now() - 7 * 86400000; if (r[0] && !r[0].error) { (r[0].incidents || []).filter(function (i) { return i.outcome === 'down' && i.start >= weekAgo; }).slice(0, 1).forEach(function (i) { var mins = Math.max(1, Math.round(((i.end || Date.now()) - i.start) / 60000)); items.push({ rank: 0, pill: t('home.w.pill_down'), pillCls: 'bg-red-100 text-red-800', tool: t('home.w.cat.uptime'), href: '/uptime', text: t(i.end ? 'home.w.todo_down' : 'home.w.todo_down_now', { date: f.day(i.start), time: new Date(i.start).getHours() + ':' + ('0' + new Date(i.start).getMinutes()).slice(-2), dur: mins >= 60 ? f.num(mins / 60, 1) + ' ' + t('uptime.unit_hour') : mins + ' min' }) }); }); } if (r[1] && r[1].data) { var bad = r[1].data.filter(function (c) { return c.r === false; }).length; if (bad) { items.push({ rank: 1, pill: t('home.w.pill_issues', { n: bad }), pillCls: 'bg-red-100 text-red-800', tool: t('home.w.cat.onpage'), href: '/onpage-seo/changes', text: t('home.w.todo_issues', { n: bad }) }); } } if (r[2] && !r[2].error && r[2].actions) { r[2].actions.slice(0, 4).forEach(function (a) { items.push({ rank: 2, gain: a.gain || 0, badge: a.kind, text: gsciView.actionText(a), end: a.gain ? '+' + f.num(a.gain) + ' ' + t('home.w.clicks') : '', tool: 'Search Insights', href: gsciView.exploreHref(a.query) }); }); } if (r[3] && !r[3].error && r[3].actions) { aiView.actions(r[3].actions).slice(0, 2).forEach(function (a) { items.push({ rank: 3, badge: a.kind, text: a.text, tool: 'AI Insights', href: a.href }); }); } if (!items.length) { return state('empty', { text: t('home.w.todo_nothing') }); } items.sort(function (a, b) { return (a.rank - b.rank) || ((b.gain || 0) - (a.gain || 0)); }); return { kind: 'list', rows: items.slice(0, 6) }; }); } }; // titel, groep en link van een widget op het dashboard; v = wat er getekend wordt var decorate = function (w) { var def = WIDGETS[w.type], cat = CAT[def.cat]; w.cat = def.cat; w.icon = cat.icon; w.tool = t('home.w.cat.' + def.cat); var mkey = w.type.indexOf('searchconsole') === 0 || w.type === 'si_chart' ? 'gscinsights.kpi.' : 'metrics.'; // de titel uit de widget zelf, met het kanaal erbij; een eigen titel (instellingen) gaat voor w.autoTitle = t('home.w.title.' + w.type, { metric: w.metric ? t(mkey + w.metric) : '', dimension: w.dimension ? t('dimensions.' + w.dimension) : '' }) + (w.filter && def.filters ? ' · ' + t('home.w.channel.' + w.filter) : ''); w.title = w.ownTitle || w.autoTitle; w.more = def.more || cat.href || null; w.hasSettings = true; w.filterOpts = def.filters ? [{ key: null, label: t('home.w.channel.all') }].concat(def.filters.map(function (x) { return { key: x, label: t('home.w.channel.' + x) }; })) : null; w.metricOpts = def.metrics ? def.metrics.map(function (m) { return { key: m, label: t(mkey + m) }; }) : null; w.dimensionOpts = def.dimensions && def.dimensions.length > 1 ? def.dimensions.map(function (x) { return { key: x, label: t('dimensions.' + x) }; }) : null; w.width = Math.round(100 * w.colspan / 12) + '%'; // vaste hoogte (zoals het oude dashboard), zodat widgets naast elkaar even hoog zijn zonder lege vlakken; // een zin of een rij tegels houdt zijn eigen hoogte w.fixed = def.kind !== 'sentence' && def.kind !== 'kpis'; return w; }; var load = function (w) { var mine = w.v = { state: 'loading' }; LOAD[w.type](w).then(function (v) { if (w.v === mine) { w.v = v; if (v.title && !w.ownTitle) { w.title = v.title; } } }, function () { if (w.v === mine) { w.v = state('error'); } }); }; var setWidgets = function (list) { d.widgets = (list || []).filter(function (x) { return offered(x.type); }).map(function (x) { return decorate({ type: x.type, metric: x.metric || WIDGETS[x.type].metric || null, dimension: x.dimension || WIDGETS[x.type].dimension || null, chart: x.chart || null, colspan: x.colspan || 6, filter: x.filter || null, ownTitle: x.title || '' }); }); d.hasCompare = d.widgets.some(function (w) { return w.type === 'si_kpis' || w.type === 'summary' || w.type === 'si_changes'; }); d.widgets.forEach(load); }; $http.get('/rest/home/widgets.list', { params: { websiteid: h.siteId() } }).then(function (res) { // niets te tonen van een eigen indeling (alleen tools die deze rol niet ziet): de standaard, zoals vroeger var mine = (res.data || []).filter(function (x) { return offered(x.type); }); return mine.length || !(res.data || []).length ? res : $http.get('/rest/home/widgets.list', { params: { websiteid: h.siteId(), which: 'fallback' } }); }).then(function (res) { $translate.onReady().then(function () { setWidgets(res.data); d.loaded = true; }); }); d.setCompare = function (c) { if (d.compare === c) { return; } d.compare = c; d.widgets.forEach(function (w) { if (w.type === 'si_kpis' || w.type === 'summary' || w.type === 'si_changes') { load(w); } }); }; // ------------------------------------------------------------------ bewaren var saveTimer = null; var save = function () { if (!d.canEdit) { return; } $timeout.cancel(saveTimer); saveTimer = $timeout(function () { $http.post('/rest/home/widgets.save', { websiteid: h.siteId(), widgets: cleanList() }); }, 400); }; var cleanList = function () { return d.widgets.map(function (w) { return { type: w.type, metric: w.metric, dimension: w.dimension, chart: w.chart, colspan: w.colspan, filter: w.filter, title: w.ownTitle }; }); }; // ------------------------------------------------------------------ bewerken d.toggleEdit = function () { d.edit = !d.edit; d.settings = null; d.confirm = null; }; d.remove = function (w) { d.widgets.splice(d.widgets.indexOf(w), 1); d.settings = null; save(); refreshPicker(); }; d.move = function (w, step) { var i = d.widgets.indexOf(w), j = i + step; if (j < 0 || j >= d.widgets.length) { return; } d.widgets.splice(i, 1); d.widgets.splice(j, 0, w); save(); }; // slepen: de gesleepte widget komt vóór of na de widget waar je hem loslaat, naar de helft waar de muis is // (links/rechts, bij een brede widget boven/onder); op een lege plek in het raster komt hij achteraan d.dragStart = function (w) { d.drag = w; d.settings = null; }; d.dragOver = function (w, after) { d.over = w; d.after = after; }; d.dropOn = function (w) { var from = d.drag, after = d.after; d.drag = d.over = null; if (!from || from === w) { return; } d.widgets.splice(d.widgets.indexOf(from), 1); d.widgets.splice(w ? d.widgets.indexOf(w) + (after ? 1 : 0) : d.widgets.length, 0, from); save(); }; d.dragEnd = function () { d.drag = d.over = null; }; // breedte: de dichtstbijzijnde van 25, 33, 50, 67, 75 en 100% d.setWidth = function (w, cols) { var best = WIDTHS[0]; WIDTHS.forEach(function (x) { if (Math.abs(x - cols) < Math.abs(best - cols)) { best = x; } }); if (w.colspan !== best) { w.colspan = best; w.width = Math.round(100 * best / 12) + '%'; return true; } return false; }; d.widthDone = function () { save(); }; d.openSettings = function (w) { d.settings = d.settings === w ? null : w; }; d.changed = function (w) { decorate(w); load(w); save(); }; // eigen titel: alleen de kop verandert, de gegevens niet d.retitle = function (w) { w.ownTitle = (w.ownTitle || '').slice(0, 60); w.title = w.ownTitle.trim() || w.autoTitle; save(); }; // ------------------------------------------------------------------ widgetkiezer var refreshPicker = function () { var p = d.picker; if (!p) { return; } var q = (p.q || '').toLowerCase().trim(); var on = {}; d.widgets.forEach(function (w) { on[w.type] = true; }); var types = Object.keys(WIDGETS).filter(offered); p.cats = [{ key: 'tip', icon: ICONS.star }, { key: 'all', icon: ICONS.all }].concat(CATS.filter(function (c) { return !c.tool || show[c.tool]; })) .concat([{ key: 'layouts', icon: ICONS.layout }]).map(function (c) { var n = c.key === 'layouts' ? p.layouts.length : types.filter(function (x) { return c.key === 'all' || (c.key === 'tip' ? WIDGETS[x].tip : WIDGETS[x].cat === c.key); }).length; return { key: c.key, icon: c.icon, label: t('home.w.cat.' + c.key), n: n }; }); p.showLayouts = p.cat === 'layouts' && !q; p.full = d.widgets.length >= MAX; p.cards = types.filter(function (x) { var w = WIDGETS[x]; if (q) { return (t('home.w.name.' + x) + ' ' + t('home.w.desc.' + x) + ' ' + t('home.w.cat.' + w.cat)).toLowerCase().indexOf(q) >= 0; } return p.cat === 'all' || (p.cat === 'tip' ? w.tip : w.cat === p.cat); }).map(function (x) { var w = WIDGETS[x]; return { type: x, kind: w.kind, name: t('home.w.name.' + x), desc: t('home.w.desc.' + x), tool: t('home.w.cat.' + w.cat), icon: CAT[w.cat].icon, size: Math.round(100 * w.span / 12) + '%', on: !!on[x] }; }); p.hint = q ? t('home.w.found', { n: p.cards.length, q: p.q }) : (p.showLayouts ? t('home.w.layouts_hint') : (p.cat === 'tip' ? t('home.w.tip_hint') : t('home.w.cards_hint'))); }; // een indeling als schets: een blokje per widget, zo breed als de widget var sketch = function (list) { return list.filter(function (x) { return offered(x.type); }).map(function (x) { return x.colspan || 6; }); }; var loadLayouts = function () { $q.all([ $http.get('/rest/home/widgets.list', { params: { websiteid: h.siteId(), which: 'default' } }), $http.get('/rest/home/template.list', { params: { websiteid: h.siteId() } }) ]).then(function (r) { var tl = r[1].data || {}; d.canTemplates = !!tl.can_edit; d.picker.layouts = [{ id: null, name: t('home.w.layout_default'), description: t('home.w.layout_default_desc'), widgets: r[0].data, blocks: sketch(r[0].data) }] .concat((tl.templates || []).map(function (x) { return angular.extend(x, { own: true, blocks: sketch(x.widgets) }); })); refreshPicker(); }); }; d.openPicker = function (cat) { d.picker = { cat: cat || 'tip', q: '', layouts: [], cards: [], cats: [] }; d.confirm = null; d.form = null; refreshPicker(); loadLayouts(); }; d.closePicker = function () { d.picker = null; d.confirm = null; d.form = null; }; d.pickCat = function (key) { d.picker.cat = key; d.picker.q = ''; d.confirm = null; refreshPicker(); }; d.search = function () { refreshPicker(); }; d.add = function (card) { if (card.on || d.widgets.length >= MAX) { return; } var def = WIDGETS[card.type]; var w = decorate({ type: card.type, metric: def.metric || null, dimension: def.dimension || null, chart: null, colspan: def.span }); d.widgets.push(w); d.hasCompare = d.hasCompare || w.type === 'si_kpis' || w.type === 'summary' || w.type === 'si_changes'; load(w); save(); d.closePicker(); d.edit = true; }; // een indeling vervangt de huidige: eerst bevestigen (in de kaart, geen venster van de browser) d.useLayout = function (l) { if (d.confirm !== l) { d.confirm = l; return; } setWidgets(l.widgets); save(); d.closePicker(); }; // indelingen van de white label: de huidige indeling bewaren, bijwerken of weghalen (rol 1-3 bij de reseller) d.newTemplate = function () { d.form = { name: '', description: '' }; }; d.saveTemplate = function (l) { var tpl = l ? { id: l.id, name: l.name, description: l.description, widgets: cleanList() } : { name: d.form.name, description: d.form.description, widgets: cleanList() }; $http.post('/rest/home/template.save', { websiteid: h.siteId(), template: tpl }).then(function () { d.form = null; loadLayouts(); }); }; d.deleteTemplate = function (l) { if (d.confirm !== 'del' + l.id) { d.confirm = 'del' + l.id; return; } $http.post('/rest/home/template.delete', { websiteid: h.siteId(), id: l.id }).then(function () { d.confirm = null; loadLayouts(); }); }; } ]); // Dashboard: een widget verslepen (HTML5 slepen; op een telefoon de pijltjes in de werkbalk). home-drag="w" app.directive('homeDrag', function () { return { link: function (scope, el, attrs) { var d = scope.d, node = el[0]; scope.$watch('d.edit', function (on) { node.setAttribute('draggable', on ? 'true' : 'false'); }); el.on('dragstart', function (e) { if (!d.edit) { return; } e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', 'widget'); scope.$apply(function () { d.dragStart(scope.$eval(attrs.homeDrag)); }); }); el.on('dragover', function (e) { if (!d.drag) { return; } e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'move'; var w = scope.$eval(attrs.homeDrag), r = node.getBoundingClientRect(); var after = w.colspan >= 9 ? e.clientY > r.top + r.height / 2 : e.clientX > r.left + r.width / 2; if (d.over !== w || d.after !== after) { scope.$apply(function () { d.dragOver(w, after); }); } }); el.on('drop', function (e) { e.preventDefault(); e.stopPropagation(); scope.$apply(function () { d.dropOn(scope.$eval(attrs.homeDrag)); }); }); el.on('dragend', function () { scope.$apply(function () { d.dragEnd(); }); }); } }; }); // Dashboard: loslaten op een lege plek in het raster zet de widget achteraan. home-drop-end app.directive('homeDropEnd', function () { return { link: function (scope, el) { var d = scope.d; el.on('dragover', function (e) { if (!d.drag) { return; } e.preventDefault(); if (d.over) { scope.$apply(function () { d.over = null; }); } }); el.on('drop', function (e) { if (!d.drag) { return; } e.preventDefault(); scope.$apply(function () { d.dropOn(null); }); }); } }; }); // Dashboard: breedte kiezen door aan de rechterrand te trekken; klikt vast op 25/33/50/67/75/100%. home-resize="w" app.directive('homeResize', function () { return { link: function (scope, el, attrs) { var start = null; el.on('pointerdown', function (e) { var grid = el[0].closest('[data-home-grid]'), item = el[0].closest('[draggable]'); if (!grid) { return; } e.preventDefault(); e.stopPropagation(); el[0].setPointerCapture(e.pointerId); if (item) { item.setAttribute('draggable', 'false'); } var w = scope.$eval(attrs.homeResize); start = { x: e.clientX, cols: w.colspan, col: (grid.clientWidth + 24) / 12, item: item, w: w }; }); el.on('pointermove', function (e) { if (!start) { return; } var cols = start.cols + (e.clientX - start.x) / start.col; if (scope.d.setWidth(start.w, cols)) { scope.$apply(); } }); var end = function () { if (!start) { return; } if (start.item) { start.item.setAttribute('draggable', 'true'); } start = null; scope.$apply(function () { scope.d.widthDone(); }); }; el.on('pointerup', end); el.on('pointercancel', end); } }; }); app.controller('help', ['$scope', '$timeout', '$http', '$rootScope', 'h', function($scope, $timeout, $http, $rootScope, h) { $http.get('https://www.marketingtracer.com/help/listjson', { params: { lang: $rootScope.user.site.locale.lang, } }).then(function successCallback(res) { $scope.topics = res.data; }); $scope.getText = function(_id, sub_id, subtopic) { $http.get('https://www.marketingtracer.com/help/subtopicjson', { params: { lang: $rootScope.user.site.locale.lang, _id: _id, sub_id: sub_id, } }).then(function successCallback(res) { subtopic.html = res.data; }); } var users = { userList: function() { $http.get('/rest/user/reseller.site.list', { params: { websiteid: h.siteId(), } }).then(function successCallback(res) { $scope.users = res.data; }); }, matchUser: function(UserId) { for (var i in $scope.users) { if ($scope.users[i]._id.$oid == UserId) { return $scope.users[i].name; } } return ''; } } } ]); app.controller("ranktracker", [ "$scope", "$timeout", "$http", "$rootScope", "$filter", "h", function ($scope, $timeout, $http, $rootScope, $filter, h) { $scope.rankings = []; $scope.page = { bFilterStar: false, label: "", lgchart: "", labels: {}, filter: {}, sortType: "q", collapse: {}, chartlg: false, locale: $rootScope.user.site.locale, fnChartLg: function (c) { this.chartlg = this.chartlg == c ? false : c; }, sortReverse: false, fnSort: function (s) { if ($scope.page.sortType == s) { $scope.page.sortReverse = !$scope.page.sortReverse; } else { $scope.page.sortReverse = false; } $scope.page.sortType = s; }, provider: "google", }; $scope.fnCustomDate = { init: function () { var d = new Date(); d.setMonth(d.getMonth() - 1); $scope.customdate = { start: d, end: new Date(), show: false }; }, set: function () { $scope.customdate.show = false; $rootScope.reportdates.start = Math.round($scope.customdate.start.getTime() / 1000); $rootScope.reportdates.end = Math.round($scope.customdate.end.getTime() / 1000); $rootScope.reportdates.description = "Custom"; $scope.getKeywords(); }, }; $scope.dateoptions = [ { start: false, end: false, description: "All times" }, { start: "TODAY", end: "TODAY", description: "Today" }, { start: "YESTYERDAY", end: "YESTYERDAY", description: "Yesterday" }, { start: "7DAYSAGO", end: "TODAY", description: "Past 7 days" }, { start: "14DAYSAGO", end: "TODAY", description: "Past 14 days" }, { start: "30DAYSAGO", end: "TODAY", description: "Past 30 days" }, { start: "1YEARAGO", end: "TODAY", description: "Past year" }, { start: "THISMONTH", end: "TODAY", description: "This month" }, { start: "LASTMONTH", end: "TODAY", description: "Previous month" }, ]; if (!$rootScope.reportdates) { $rootScope.reportdates = { start: false, end: false, description: "All time" }; } $scope.fnDateSelection = function (reportdates) { $rootScope.reportdates.start = reportdates.start; $rootScope.reportdates.end = reportdates.end; $rootScope.reportdates.description = reportdates.description; $scope.getKeywords(); $scope.getVisibility(); }; $scope.do = function (obj, method, param) { if (obj == "fnSingle") { fnSingle[method](param); } else { fnMultiple[method](param); } }; var fnSingle = { getParams: function (ranking) { return { websiteid: h.siteId(), ids: [ranking._id.$oid], }; }, pre_options: function (ranking) { ranking.pre_options = !ranking.pre_options; if (ranking.pre_options) { $http .get("/rest/rank-tracker/history", { params: { websiteid: h.siteId(), ranking_id: ranking._id.$oid, provider: $scope.page.provider, start: $rootScope.reportdates.start, end: $rootScope.reportdates.end, }, }) .then(function successCallback(res) { var data = res.data; }); } }, options: function (ranking) { $http .post( "/rest/rank-tracker/edit", angular.extend( { "summary.google.start": ranking.summary.google.start, "summary.googlemobile.start": ranking.summary.googlemobile.start, "meta.label": ranking.meta.label, url: ranking.url, fields: ["summary.google.start", "summary.googlemobile.start", "meta.label", "url"], }, fnSingle.getParams(ranking) ) ) .then(function successCallback(res) { $scope.getKeywords(); }); }, sanitize: function (ranking) { $http.post("/rest/rank-tracker/sanitize", angular.extend({ "q": ranking.q }, fnSingle.getParams(ranking))).then(function successCallback(res) { $scope.getKeywords(); }); }, delete: function (ranking) { $http.post("/rest/rank-tracker/delete", fnSingle.getParams(ranking)).then(function successCallback(res) { $scope.getKeywords(); }); }, star: function (ranking) { $http.post("/rest/rank-tracker/edit", angular.extend({ "meta.star": !ranking.meta.star, fields: ["meta.star"] }, fnSingle.getParams(ranking))).then(function successCallback(res) { $scope.getKeywords(); }); }, }; var fnMultiple = { getParams: function (ranking) { var ids = []; for (var i in $scope.rankings) { if ($scope.rankings[i].checked) { ids.push($scope.rankings[i]._id.$oid); } } return { websiteid: h.siteId(), ids: ids, }; }, multioptions: function () { $scope.page.multioptions = false; for (var i in $scope.rankings) { if ($scope.rankings[i].checked) { $scope.page.multioptions = true; break; } } }, check: function (ranking) { ranking.checked = !ranking.checked; fnMultiple.multioptions(); }, checkall: function (label) { $scope.page.labels[label] = !$scope.page.labels[label]; for (var i in $scope.rankings) { if ($scope.rankings[i].meta.label == label) { $scope.rankings[i].checked = $scope.page.labels[label]; } } fnMultiple.multioptions(); }, options: function (ranking) { $http .post( "/rest/rank-tracker/edit", angular.extend( { "meta.label": $scope.page.label, fields: ["meta.label"], }, fnMultiple.getParams(ranking) ) ) .then(function successCallback(res) { $scope.getKeywords(); }); }, delete: function (ranking) { $http.post("/rest/rank-tracker/delete", fnMultiple.getParams(ranking)).then(function successCallback(res) { $scope.getKeywords(); }); }, resetvolume: function (ranking) { $http.post("/rest/rank-tracker/resetvolume", fnMultiple.getParams(ranking)).then(function successCallback(res) { $scope.getKeywords(); }); }, export: function (ranking) { $http.post("/rest/rank-tracker/export", fnMultiple.getParams(ranking)).then(function successCallback(res) { var element = document.createElement("a"); element.setAttribute("href", "data:text/html;," + encodeURIComponent(res.data.csv)); element.setAttribute("download", "export.csv"); element.style.display = "none"; document.body.appendChild(element); element.click(); document.body.removeChild(element); }); }, exportxls: function (ranking) { $http.post("/rest/rank-tracker/exportxls", fnMultiple.getParams(ranking)).then(function successCallback(res) { var element = document.createElement("a"); element.setAttribute("href", "data:text/html;," + encodeURIComponent(res.data.csv)); element.setAttribute("download", "export.xls"); element.style.display = "none"; document.body.appendChild(element); element.click(); document.body.removeChild(element); }); }, }; // ranking grafiek'heschiedenis $scope.fnRankingHistory = function (ranking, provider) { if (ranking.active && $scope.page.provider == provider) { ranking.active = false; return; } $scope.page.provider = provider; for (var x in $scope.rankings) { $scope.rankings[x].active = false; } ranking.active = true; $http .get("/rest/rank-tracker/history", { params: { websiteid: h.siteId(), ranking_id: ranking._id.$oid, provider: $scope.page.provider, start: $rootScope.reportdates.start, end: $rootScope.reportdates.end, }, }) .then(function successCallback(res) { var data = res.data; var graphs = []; var _d = {}; var d = []; let urlkeyexists = {}; for (var i in data) { if (data[i]?.data?.length > 0 || typeof data[i]?.data === "object") { if (data[i]?.data?.length === 0) { // stupid array identifies as object! } else { console.log(data[i]?.data); console.log(data[i]?.data?.length); console.log(typeof data[i]?.data); for (var y in data[i].data) { _d[data[i].data[y].date] = _d[data[i].data[y].date] || { date: data[i].data[y].date }; _d[data[i].data[y].date][i] = data[i].data[y].pos; } graphs.push({ balloonText: data[i].url + "
position: [[value]]", title: data[i].url, bullet: "round", bulletBorderAlpha: 1, bulletColor: "#FFFFFF", useLineColorForBulletBorder: true, fillAlphas: 0, lineThickness: 2, lineAlpha: 1, bulletSize: 7, valueField: i, connect: false, }); } } } for (var i in _d) { d.push(_d[i]); } ranking.chart = { data: d, type: "serial", theme: "light", marginLeft: 0, marginRight: 0, marginTop: 10, categoryField: "date", rotate: false, pathToImages: "vendor/amchart/images/", dataDateFormat: "YYYYMMDD", legend: { position: "bottom", enabled: true, useGraphSettings: true, }, categoryAxis: { gridPosition: "start", parseDates: true, labelRotation: 45, }, valueAxes: [ { reversed: true, position: "left", axisAlpha: 0, labelsEnabled: true, minimum: 1, }, ], graphs: graphs, }; }); }; // haal alle sleutelwoorden op $scope.getKeywords = function (a) { // all active plans $scope.urls = {}; var startDate = $scope.custominit ? $scope.custominit.start : $rootScope.reportdates.start; $scope.page.bFilterStar = $scope.custominit ? $scope.custominit.bFilterStar : $scope.page.bFilterStar; if ($scope.page.bFilterStar == true) { $scope.page.filter.meta = { star: true }; } $http .get("/rest/rank-tracker/list", { params: { websiteid: h.siteId(), start: startDate, end: $rootScope.reportdates.end, }, }) .then(function successCallback(res) { $scope.page.rankinglength = res.data.length; if ($scope.page.rankinglength === 0) { //$scope.fnModal("rank-tracker/modal.addkeyword"); } $scope.change = { nverlies: 0, nwinst: 0, nsame: 0, verlies: 0, winst: 0, totaal: 0, n: 0 }; $scope.pie = { top3: 0, top10: 0, top100: 0, overig: 0 }; $scope._rankings = angular.copy(res.data); //$scope.rankings = res.data; $scope.sortranking(); }); }; $scope.switchstar = function () { if ($scope.page.filter.meta) { delete $scope.page.filter.meta; } else { $scope.page.filter.meta = { star: true }; } $scope.sortranking(); }; $scope.sortranking = function () { $scope.rankings = $filter("filter")($scope._rankings, $scope.page.filter); if ($scope.rankings.length > 0) { for (var i in $scope.rankings) { if ($scope.rankings[i].active) { var active = i; } } } var sorted = {}; // some stats var totalenhancements = {}; for (var i in $scope.rankings) { var ranking = $scope.rankings[i]; ranking.meta.label = ranking.meta.label || "--"; if (ranking?.url && ranking.url.indexOf("http") != 0) { ranking.url = "https://" + ranking.url; } var tmpg = ranking?.summary?.google?.pos || 100; var tmpgm = ranking?.summary?.googlemobile?.pos || 100; ranking.summary = ranking?.summary || { google: { pos: tmpg }, googlemobile: { pos: tmpgm } }; //ranking.summary.google = ranking?.summary?.google || {pos:100} //ranking.summary.googlemobile.pos = ranking.summary.googlemobile.pos || 100 if (active && i == active) { $scope.fnRankingHistory(ranking, $scope.page.provider); } // kan weg if (ranking.summary.google.pos < 4) { ++$scope.pie.top3; } else if (ranking.summary.google.pos < 11) { ++$scope.pie.top10; } else if (ranking.summary.google.pos < 100) { ++$scope.pie.top100; } else { ++$scope.pie.overig; } //health ranking.health = 100 - ((ranking.summary.google.worst - ranking.summary.google.pos + 0.5) / (ranking.summary.google.worst - ranking.summary.google.best)) * 100; if (ranking.health > 100) { ranking.health = 100; } sorted[ranking.meta.label] = sorted[ranking.meta.label] || []; sorted[ranking.meta.label].push(ranking); //change if (ranking.summary.google.start > ranking.summary.google.pos) { $scope.change.nwinst += 1; $scope.change.winst += ranking.summary.google.start - ranking.summary.google.pos; } else if (ranking.summary.google.start < ranking.summary.google.pos) { $scope.change.nverlies += 1; $scope.change.verlies += ranking.summary.google.pos - ranking.summary.google.start; } else { $scope.change.nsame += 1; } // enhancements var enhancements = {}; if (typeof ranking.summary.google.enhancements == "object" && ranking.summary.google.enhancements != null) { if (typeof ranking.summary.google.enhancements.other == "object") { ranking.summary.google.enhancements.other = ranking.summary.google.enhancements.other || {}; ranking.summary.google.enhancements.other = Object.values(ranking.summary.google.enhancements.other); } if (typeof ranking.summary.google.enhancements.my == "object") { ranking.summary.google.enhancements.my = ranking.summary.google.enhancements.my || {}; ranking.summary.google.enhancements.my = Object.values(ranking.summary.google.enhancements.my); } ranking.summary.google.enhancements.other = ranking.summary.google.enhancements.other || []; ranking.summary.google.enhancements.my = ranking.summary.google.enhancements.my || []; ranking.summary.google.enhancements.other.forEach(function (v, k) { totalenhancements[v] = totalenhancements[v] || { v: v, my: 0, other: 0 }; totalenhancements[v].other += 1; enhancements[v] = enhancements[v] || { v: v, my: 0, other: 0 }; enhancements[v].other += 1; }); ranking.summary.google.enhancements.my.forEach(function (v, k) { totalenhancements[v] = totalenhancements[v] || { v: v, my: 0, other: 0 }; totalenhancements[v].my += 1; enhancements[v] = enhancements[v] || { v: v, my: 0, other: 0 }; enhancements[v].my += 1; }); } ranking.enhancements = Object.values(enhancements).sort(function (a, b) { return a.v < b.v ? -1 : 1; }); $scope.totalenhancements = Object.values(totalenhancements).sort(function (a, b) { return a.v < b.v ? -1 : 1; }); $scope.change.n += 1; $scope.change.totaal += ranking.summary.google.start - ranking.summary.google.pos; } let sortedranking = []; Object.keys(sorted).forEach(function (k) { let _sorted = sorted[k]; sortedranking.push({ label: k, rankings: _sorted }); }); $scope.sortedranking = sortedranking; }; $scope.charts = {}; $scope.changes = {}; var chart = { small: { type: "serial", categoryField: "date", dataDateFormat: "YYYYMMDD", autoMargins: false, categoryAxis: { parseDates: true, gridAlpha: 0, axisAlpha: 0 }, valueAxes: [{ gridAlpha: 0, axisAlpha: 0, color: "#FFFFFF", stackType: "regular" }], }, large: { type: "serial", categoryField: "date", dataDateFormat: "YYYYMMDD", autoMargins: true, categoryAxis: { gridPosition: "start", parseDates: true, gridThickness: 0 }, valueAxes: [{ position: "left", axisAlpha: 0, labelsEnabled: true, dashLength: 4, color: "#dedede", stackType: "regular" }], }, visibility: function (data, type) { $scope.charts.visibility = angular.extend({}, chart[type], { data: data, graphs: [ { balloonText: "Visibility: [[value]]%", fillAlphas: 0, lineAlpha: 1, bulletAlpha: 0.7, valueField: "visibility", lineColor: h.colors()[1], }, ], }); $scope.charts.visibilitylg = angular.extend({}, chart["large"], { data: data, graphs: [ { balloonText: "Visibility: [[value]]%", lineAlpha: 1, valueField: "visibility", lineColor: h.colors()[1], bullet: "round", bulletBorderAlpha: 1, bulletColor: "#FFFFFF", useLineColorForBulletBorder: true, fillAlphas: 0, lineThickness: 2, lineAlpha: 1, bulletSize: 7, }, ], }); $scope.changes.visibility = { cur: data[data.length - 1].visibility, start: data[0].visibility, dif: data[data.length - 1].visibility - data[0].visibility }; }, avgpos: function (data, type) { var _data = data.reduce(function (acc, cur) { if (cur.avg) { acc.push(cur); } return acc; }, []); if (_data.length == 1) { _data.unshift({ date: "20191031", avg: 0 }); } $scope.charts.avgpos = angular.extend({}, chart[type], { data: _data, graphs: [ { balloonText: "Avg position: [[value]]", lineAlpha: 1, valueField: "avg", lineColor: h.colors()[3], }, ], valueAxes: [{ gridAlpha: 0, axisAlpha: 0, color: "#FFFFFF", stackType: "regular", reversed: true }], }); $scope.charts.avgposlg = angular.extend({}, chart["large"], { data: _data, graphs: [ { balloonText: "Avg position: [[value]]", valueField: "avg", lineColor: h.colors()[3], lineAlpha: 1, bullet: "round", bulletBorderAlpha: 1, bulletColor: "#FFFFFF", useLineColorForBulletBorder: true, fillAlphas: 0, lineThickness: 2, lineAlpha: 1, bulletSize: 7, }, ], valueAxes: [{ gridAlpha: 0, axisAlpha: 0, color: "#FFFFFF", stackType: "regular", reversed: true }], }); $scope.changes.avgpos = { cur: _data[_data.length - 1].avg, start: _data[0].avg, dif: (_data[_data.length - 1].avg - _data[0].avg) * -1 }; }, enhancement: function (data, type) { var _data = data.reduce(function (acc, cur) { if (cur.my_enh || cur.other_enh) { acc.push(cur); } return acc; }, []); if (_data.length == 1) { _data.unshift({ date: "20191031", my_enh: 0, other_enh: 0 }); } $scope.charts.enhancement = angular.extend({}, chart[type], { data: _data, graphs: [ { balloonText: "My enhancements: [[value]]", fillAlphas: 0.6, lineAlpha: 1, valueField: "my_enh", lineColor: h.colors()[4], }, { balloonText: "Total enhancements: [[value]]", fillAlphas: 0.6, lineAlpha: 1, valueField: "other_enh", lineColor: "#DDDDDD", }, ], }); $scope.charts.enhancementlg = angular.extend({}, chart["large"], { data: _data, graphs: [ { balloonText: "My enhancements: [[value]]", bullet: "round", lineAlpha: 1, bullet: "round", bulletBorderAlpha: 1, bulletColor: "#FFFFFF", useLineColorForBulletBorder: true, fillAlphas: 0.6, lineThickness: 2, lineAlpha: 1, bulletSize: 7, valueField: "my_enh", lineColor: h.colors()[4], }, { balloonText: "Total enhancements: [[value]]", bullet: "round", lineAlpha: 1, bullet: "round", bulletBorderAlpha: 1, bulletColor: "#FFFFFF", useLineColorForBulletBorder: true, fillAlphas: 0.6, lineThickness: 2, lineAlpha: 1, bulletSize: 7, valueField: "other_enh", lineColor: "#DDDDDD", }, ], }); $scope.changes.enhancement = { cur: _data[_data.length - 1]?.my_enh, start: _data[0]?.my_enh, dif: _data[_data.length - 1]?.my_enh - _data[0]?.my_enh }; }, top: function (data, type) { var _data = data.reduce(function (acc, cur) { if (cur.top3 || cur.top10 || cur.top50 || cur.top100 || cur.norank) { acc.push(cur); } return acc; }, []); $scope.charts.top = angular.extend({}, chart[type], { data: _data, graphs: [ { balloonText: "Not ranked: [[value]]", fillAlphas: 0.6, lineAlpha: 0.6, valueField: "norank", lineColor: "#DDDDDD" }, { balloonText: "Top100: [[value]]", fillAlphas: 0.25, lineAlpha: 0.25, valueField: "top100", lineColor: h.colors()[2] }, { balloonText: "Top50: [[value]]", fillAlphas: 0.45, lineAlpha: 0.45, valueField: "top50", lineColor: h.colors()[2] }, { balloonText: "Top10: [[value]]", fillAlphas: 0.65, lineAlpha: 0.65, valueField: "top10", lineColor: h.colors()[2] }, { balloonText: "Top3: [[value]]", fillAlphas: 0.85, lineAlpha: 0.85, valueField: "top3", lineColor: h.colors()[2] }, { balloonText: "Top1: [[value]]", fillAlphas: 1, lineAlpha: 1, valueField: "top1", lineColor: h.colors()[2] }, ], }); $scope.changes.top = { cur: _data[_data.length - 1], start: _data[0], dif: { top1: _data[_data.length - 1].top1 - _data[0].top1 || 0, top3: _data[_data.length - 1].top3 - _data[0].top3 || 0, top10: _data[_data.length - 1].top10 - _data[0].top10 || 0, top50: _data[_data.length - 1].top50 - _data[0].top50 || 0, top100: _data[_data.length - 1].top100 - _data[0].top100 || 0, norank: _data[_data.length - 1].norank - _data[0].norank || 0, }, }; }, }; // haal alle sleutelwoorden op $scope.getVisibility = function (a) { $http .get("/rest/rank-tracker/visibility", { params: { websiteid: h.siteId(), start: $rootScope.reportdates.start, end: $rootScope.reportdates.end, }, }) .then(function successCallback(res) { chart.avgpos(res.data, "small"); chart.visibility(res.data, "small"); chart.enhancement(res.data, "small"); chart.top(res.data, "small"); }); }; $scope.fnAddKeywords = function () { $http .post("/rest/rank-tracker/add", { keywords: $scope.page.addkeywords, locale: $scope.page.locale, websiteid: h.siteId(), }) .then(function successCallback(res) { $scope.getKeywords(); $scope.fnCloseModal(); $scope.page.addkeywords = ""; }); }; $scope.getKeywords(); $scope.getVisibility(); $http.get("/rest/locale/language.list").then(function successCallback(res) { $scope.languages = res.data; }); $http.get("/rest/locale/country.list").then(function successCallback(res) { $scope.countries = res.data; }); $scope.fnSearchCity = function (q) { $http.get("/rest/locale/city.list", { params: { q: q } }).then(function successCallback(res) { $scope.cities = res.data; }); }; $scope.setCity = function (city) { $scope.page.locale.city = city.city; $scope.page.locale.uule = city.uule; $scope.cities = []; }; }, ]); app.controller('ranktrackerinsights', ['$scope', '$timeout', '$http', '$rootScope', '$filter', 'h', function($scope, $timeout, $http, $rootScope, $filter, h) { $scope.rankings = []; if($scope.custominit){ $scope.page = {'timeframe': '31d'}; $scope.mds = [ { 'title': '31-day mean - position improvement', 'timeframe':'31d', 'start': 'r31d', 'end': 'r62d', 'dif': 's31dc', 'vol': 's31dvol', 'order': '-median.s31dc', 'lengthindex': '31dl', 'comp': 'gt' }, { 'title': '31-day mean - position loss', 'timeframe':'31d', 'start': 'r31d', 'end': 'r62d', 'dif': 's31dc', 'vol': 's31dvol', 'order': 'median.s31dc', 'lengthindex': '31dw', 'comp': 'lt' }, ]; } else { $scope.page = {'timeframe': '3d'}; $scope.mds = [ { 'title': '3-day mean - position improvement', 'timeframe':'3d', 'start': 'r3d', 'end': 'r6d', 'dif': 's3dc', 'vol': 's3dvol', 'order': '-median.s3dc', 'lengthindex': '3dw', 'comp': 'gt' }, { 'title': '3-day mean - traffic improvement', 'timeframe':'3d', 'start': 'r3d', 'end': 'r6d', 'dif': 's3dc', 'vol': 's3dvol', 'order': '-median.s3dvol', 'lengthindex': '3dw', 'comp': 'gt' }, { 'title': '3-day mean - position loss', 'timeframe':'3d', 'start': 'r3d', 'end': 'r6d', 'dif': 's3dc', 'vol': 's3dvol', 'order': 'median.s3dc', 'lengthindex': '3dl', 'comp': 'lt' }, { 'title': '3-day mean - traffic loss', 'timeframe':'3d', 'start': 'r3d', 'end': 'r6d', 'dif': 's3dc', 'vol': 's3dvol', 'order': 'median.s3dvol', 'lengthindex': '3dl', 'comp': 'lt' }, { 'title': '7-day mean - position improvement', 'timeframe':'7d', 'start': 'r7d', 'end': 'r14d', 'dif': 's7dc', 'vol': 's7dvol', 'order': '-median.s7dc', 'lengthindex': '7dw', 'comp': 'gt' }, { 'title': '7-day mean - traffic improvement', 'timeframe':'7d', 'start': 'r7d', 'end': 'r14d', 'dif': 's7dc', 'vol': 's7dvol', 'order': '-median.s7dvol', 'lengthindex': '7dw', 'comp': 'gt' }, { 'title': '7-day mean - position loss', 'timeframe':'7d', 'start': 'r7d', 'end': 'r14d', 'dif': 's7dc', 'vol': 's7dvol', 'order': 'median.s7dc', 'lengthindex': '7dl', 'comp': 'lt' }, { 'title': '7-day mean - traffic loss', 'timeframe':'7d', 'start': 'r7d', 'end': 'r14d', 'dif': 's7dc', 'vol': 's7dvol', 'order': 'median.s7dvol', 'lengthindex': '7dl', 'comp': 'lt' }, { 'title': '31-day mean - position improvement', 'timeframe':'31d', 'start': 'r31d', 'end': 'r62d', 'dif': 's31dc', 'vol': 's31dvol', 'order': '-median.s31dc', 'lengthindex': '31dl', 'comp': 'gt' }, { 'title': '31-day mean - traffic improvement', 'timeframe':'31d', 'start': 'r31d', 'end': 'r62d', 'dif': 's31dc', 'vol': 's31dvol', 'order': '-median.s31dvol', 'lengthindex': '31dl', 'comp': 'gt' }, { 'title': '31-day mean - position loss', 'timeframe':'31d', 'start': 'r31d', 'end': 'r62d', 'dif': 's31dc', 'vol': 's31dvol', 'order': 'median.s31dc', 'lengthindex': '31dw', 'comp': 'lt' }, { 'title': '31-day mean - traffic loss', 'timeframe':'31d', 'start': 'r31d', 'end': 'r62d', 'dif': 's31dc', 'vol': 's31dvol', 'order': 'median.s31dvol', 'lengthindex': '31dw', 'comp': 'lt' }, ]; } $scope.exists = function(prop, comp) { return function(item) { return item.median.hasOwnProperty(prop) && (comp == 'lt') ? item.median[prop] < 0 : item.median[prop] > 0 } } var competitors = { listinsights: function() { $http.get("/rest/rank-tracker/median", { params: { websiteid: h.siteId(), } }).then(function successCallback(res) { $scope.insights = res.data; $scope.lengths = $scope.insights.reduce(function(acc, cur) { if (cur.median.s3dc > 0) { acc['3dw'] += 1; } else if (cur.median.s3dc < 0) { acc['3dl'] += 1; } if (cur.median.s7dc > 0) { acc['7dw'] += 1; } else if (cur.median.s7dc < 0) { acc['7dl'] += 1; } if (cur.median.s7dc > 0) { acc['31dw'] += 1; } else if (cur.median.s31dc < 0) { acc['31dl'] += 1; } return acc; }, { '3dl': 0, '3dw': 0, '7dl': 0, '7dw': 0, '31dl': 0, '31dw': 0 }) }); }, } competitors.listinsights(); // ranking grafiek'heschiedenis $scope.fnRankingHistory = function(ranking, provider) { if (ranking.active && $scope.page.provider == provider) { ranking.active = false; return; } $scope.page.provider = provider; for (var x in $scope.rankings) { $scope.rankings[x].active = false; } ranking.active = true; $http.get("/rest/rank-tracker/history", { params: { websiteid: h.siteId(), ranking_id: ranking._id.$oid, provider: 'google', } }).then(function successCallback(res) { var data = res.data; var graphs = []; var _d = {}; var d = []; for (var i in data) { for (var y in data[i].data) { _d[data[i].data[y].date] = _d[data[i].data[y].date] || { 'date': data[i].data[y].date }; _d[data[i].data[y].date][i] = data[i].data[y].pos; } graphs.push({ balloonText: data[i].url + "
position: [[value]]", "title": data[i].url, "bullet": "round", "bulletBorderAlpha": 1, "bulletColor": "#FFFFFF", "useLineColorForBulletBorder": true, "fillAlphas": 0, "lineThickness": 2, "lineAlpha": 1, "bulletSize": 7, "valueField": i, "connect": false, }) } for (var i in _d) { d.push(_d[i]); } ranking.chart = { data: d, type: 'serial', theme: 'light', marginLeft: 0, marginRight: 0, marginTop: 10, categoryField: 'date', rotate: false, pathToImages: 'vendor/amchart/images/', dataDateFormat: 'YYYYMMDD', legend: { position: "bottom", enabled: true, "useGraphSettings": true, }, categoryAxis: { gridPosition: "start", parseDates: true, labelRotation: 45 }, valueAxes: [{ reversed: true, position: "left", axisAlpha: 0, labelsEnabled: true, minimum: 1, }], graphs: graphs, } }); } // set competitors $scope.fnAddCompetitors = function() { $http.post('/rest/rank-tracker/competitors.edit', { competitors: $scope.competitors, websiteid: h.siteId() }).then(function successCallback(res) { competitors.listinsights(); competitors.listkeywords(); $scope.fnCloseModal(); }); } } ]); app.controller('ranktrackercompetitors', ['$scope', '$timeout', '$http', '$rootScope', '$filter', 'h', function($scope, $timeout, $http, $rootScope, $filter, h) { $scope.rankings = []; $scope.blazak = {}; $scope.page = { label: '', labels: {}, table: { 'curpage': 0, 'itemsperpage': 14, 'offset': 0, 'filter': {}, 'order': { 'k': 'url', 'r': false }, 'fnOrder': function(k, b) { this.order.r = (k == this.order.k) ? !this.order.r : b; this.order.k = k; }, fnSetOffset: function() { this.offset = this.itemsperpage * (this.curpage - 1); }, fnFilterProperty: function() { var pages = angular.copy($rootScope._pages); $scope.pages = $filter('filter')(pages, $scope.page.table.filter); } }, date: 'alltime', dates: ['alltime', '4weeks', '4months', 'thismonth'], tab: 'my', provider: 'google' } $scope.do = function(obj, method, param) { if (obj == 'fnSingle') { fnSingle[method](param); } else { fnMultiple[method](param); } } $scope.export = function () { $http.post("/rest/rank-tracker/exportcompetitors", { websiteid: h.siteId(), }).then(function successCallback(res) { var element = document.createElement("a"); element.setAttribute("href", "data:text/html;," + encodeURIComponent(res.data.csv)); element.setAttribute("download", "export.csv"); element.style.display = "none"; document.body.appendChild(element); element.click(); document.body.removeChild(element); }); }; var competitors = { listcompetitors: function() { $http.get("/rest/rank-tracker/competitors.list", { params: { websiteid: h.siteId(), } }).then(function successCallback(res) { $scope.competitors = res.data; if ($scope.competitors.length === 0 && $rootScope.user.site.role < 6) { $scope.fnModal('rank-tracker/modal.editcompetitors'); } }); }, listkeywords: function() { if ($scope.rankings.length > 0) { for (var i in $scope.rankings) { if ($scope.rankings[i].active) { var active = i; } } } var sorted = {}; // all active plans $scope.urls = {} $http.get('/rest/rank-tracker/competitors.listkeywords', { params: { websiteid: h.siteId(), } }).then(function successCallback(res) { $scope.page.rankinglength = res.data.length; $scope.rankings = res.data; // some stats for (var i in $scope.rankings) { var ranking = $scope.rankings[i]; ranking.summary.google.pos = ranking.summary.google.pos || 100 ranking.summary.googlemobile.pos = ranking.summary.googlemobile.pos || 100 sorted[ranking.meta.label] = sorted[ranking.meta.label] || []; sorted[ranking.meta.label].push(ranking); } $scope.sortedranking = sorted; }); } } competitors.listcompetitors(); competitors.listkeywords(); // ranking grafiek'heschiedenis $scope.fnRankingHistory = function(ranking, provider) { provider = 'google'; console.log(ranking); if (ranking.active && $scope.page.provider == provider) { ranking.active = false; return; } $scope.page.provider = provider; for (var x in $scope.rankings) { $scope.rankings[x].active = false; } ranking.active = true; $http.get("/rest/rank-tracker/competitors.history", { params: { websiteid: h.siteId(), ranking_id: ranking._id.$oid, provider: $scope.page.provider, } }).then(function successCallback(res) { var data = res.data; var graphs = []; var _d = {}; var d = []; for (var i in data) { console.log(data[i]); for (var y in data[i].data) { _d[data[i].data[y].date] = _d[data[i].data[y].date] || { 'date': data[i].data[y].date }; _d[data[i].data[y].date][i] = data[i].data[y].pos; } graphs.push({ balloonText: data[i].url + "
position: [[value]]", "title": data[i].url, "bullet": "round", "bulletBorderAlpha": 1, "bulletColor": "#FFFFFF", "useLineColorForBulletBorder": true, "fillAlphas": 0, "lineThickness": 2, "lineAlpha": 1, "bulletSize": 7, "valueField": i, "connect": false, }) } for (var i in _d) { d.push(_d[i]); } ranking.chart = { data: d, type: 'serial', theme: 'light', marginLeft: 0, marginRight: 0, marginTop: 10, fontFamily: 'Roboto', categoryField: 'date', rotate: false, pathToImages: 'vendor/amchart/images/', dataDateFormat: 'YYYYMMDD', legend: { position: "bottom", enabled: true, "useGraphSettings": true, }, categoryAxis: { gridPosition: "start", parseDates: true, labelRotation: 45 }, valueAxes: [{ reversed: true, position: "left", axisAlpha: 0, labelsEnabled: true, minimum: 1, }], graphs: graphs, } }); } // set competitors $scope.fnAddCompetitors = function() { $http.post('/rest/rank-tracker/competitors.edit', { competitors: $scope.competitors, websiteid: h.siteId() }).then(function successCallback(res) { competitors.listcompetitors(); competitors.listkeywords(); $scope.fnCloseModal(); }); } } ]); app.controller('linkbuildingassistant', ['$scope', '$http', '$rootScope', '$window', '$filter', 'h', function($scope, $http, $rootScope, $window, $filter, h) { $scope.page = { 'quickselect': {}, 'importtab': false, 'categories': [], 'tags': {}, 'dates': [], 'kdates': {}, 'newlink': { 'moz': { 'targetscope': 'page', 'n': '50' } }, 'multiplelinks': {}, 'filter': {}, table: { 'curpage': 0, 'itemsperpage': 100, 'offset': 0, fnSetOffset: function() { this.offset = this.itemsperpage * (this.curpage - 1); }, } } /* months*/ var thismonth = new Date().getMonth() + 3; for (var i = 0; i < 15; i++) { var date = new Date(new Date().getFullYear(), thismonth, 0); $scope.page.dates.push({ 'date': date }); $scope.page.kdates[date.toString()] = date; thismonth = thismonth - 1; } // do $scope.do = function(obj, method, param) { if (obj == 'fnSingle') { return fnSingle[method](param); } else if (obj == 'fnWizzard') { return fnWizzard[method](param); } else if (obj == 'fnTemplates') { return fnTemplates[method](param); } else { return fnMultiple[method](param); } } // link var fnLinks = { list: function() { $http.get("/rest/linkbuilding/link.list", { params: { websiteid: h.siteId(), } }).then(function successCallback(res) { $scope.page.multioptions = false; $scope.page.checkall = false; for (var i in res.data) { var date = new Date(res.data[i].date * 1); res.data[i].monthstring = date.toLocaleString('default', { year: 'numeric', 'month': 'long' }) res.data[i].d = res.data[i].date*1; res.data[i].date = $scope.page.kdates[date.toString()]; if (res.data[i].my.tag) { $scope.page.tags[res.data[i].my.tag] = res.data[i].my.tag; } } res.data = $filter('orderBy')(res.data, 'd', true) $scope.links = res.data; }); }, } // categories var fnCategories = { list: function() { if ($scope.categories) { return; } $http.get('/rest/linkbuilding/manager.categories.list').then(function successCallback(res) { $scope.categories = res.data; }); } } // categories var fnTemplates = { list: function() { $http.get('/rest/linkbuilding/template.list', { params: { 'websiteid': h.siteId(), } }).then(function successCallback(res) { $scope.mailTemplates = res.data; }); }, 'get_mails_by_key': function(key) { for (var i in $scope.mailTemplates) { if ($scope.mailTemplates[i].key == key) { return $scope.mailTemplates[i].mails; } } } } //wizzard var fnWizzard = { 'moz-domain': function() { $http.post("/rest/linkbuilding/wizzard.moz.domain", angular.extend({ 'domain': $scope.page.mozdomain }, { websiteid: h.siteId() }) ).then(function successCallback(res) { fnLinks.list(); }); }, 'add_url': function() { $http.post("/rest/linkbuilding/link.add.url", angular.extend($scope.page.newlink, { websiteid: h.siteId(), 'date': new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0) }) ).then(function successCallback(res) { $scope.page.newlink = { 'moz': { 'targetscope': 'page', 'n': '50' } }; fnLinks.list(); }); }, 'add_bulk': function() { $http.post("/rest/linkbuilding/link.add.bulk", angular.extend({ 'my': $scope.page.newlink.my, 'bulk': $scope.page.newlink.bulk, 'allowduplicates': $scope.page.newlink.allowduplicates }, { websiteid: h.siteId(), 'date': new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0) }) ).then(function successCallback(res) { $scope.page.newlink = { 'moz': { 'targetscope': 'page', 'n': '50' } }; fnLinks.list(); }); }, 'add_search': function() { $http.post("/rest/linkbuilding/link.add.search", angular.extend($scope.page.newlink, { websiteid: h.siteId(), 'date': new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0) }) ).then(function successCallback(res) { $scope.page.newlink = { 'moz': { 'targetscope': 'page', 'n': '50' } }; fnLinks.list(); }); }, 'add_moz': function() { $http.post("/rest/linkbuilding/link.add.moz", angular.extend($scope.page.newlink, { websiteid: h.siteId(), 'date': new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0) }) ).then(function successCallback(res) { $scope.page.newlink = { 'moz': { 'targetscope': 'page', 'n': '50' } }; fnLinks.list(); }); }, 'list_google_alerts': function() { $http.get("/rest/linkbuilding/rss.list.google.alerts", { params: { websiteid: h.siteId(), } }).then(function successCallback(res) { $scope.page.google_alerts = res.data; }); }, 'add_google_alerts': function() { $http.post("/rest/linkbuilding/rss.add.google_alerts", angular.extend({}, { 'alert': $scope.page.newlink.google_alerts }, { websiteid: h.siteId() }) ).then(function successCallback(res) { $scope.page.newlink = { 'moz': { 'targetscope': 'page', 'n': '50' } }; $scope.page.importtab = false; fnLinks.list(); }); }, 'remove_google_alert': function(alert) { $http.post("/rest/linkbuilding/rss.remove.google.alerts", angular.extend({}, { 'alert': alert }, { websiteid: h.siteId() }) ).then(function successCallback(res) { fnWizzard.list_google_alerts(); $scope.page.newlink = { 'moz': { 'targetscope': 'page', 'n': '50' } }; }); }, } $scope.fnCopyText = function(text) { var body = angular.element($window.document.body); var textarea = angular.element('