(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'
})
/* Core Web Vitals (CrUX): /crux, /crux/bezoekers, /crux/diagnose; zichtbaar via user.site.show.crux (nog niet gelanceerd).
De oude tabs van v1 (In Google, Waarom trager) gaan naar hun opvolger; vóór /crux/:tab, de eerste match telt */
.when('/crux/zoekresultaten', { redirectTo: '/crux/bezoekers' })
.when('/crux/waarom', { redirectTo: '/crux/diagnose' })
.when('/crux', {
title: 'Core Web Vitals',
templateUrl: viewfolder + '/crux/index.html?r=' + r,
nav: 'crux'
})
.when('/crux/:tab', {
title: 'Core Web Vitals',
templateUrl: viewfolder + '/crux/index.html?r=' + r,
nav: 'crux'
})
/* 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. Vandaag
// telt nog maar half (Arjen 2026-10-28: de lijn dook aan het eind omlaag) en valt weg.
var dayLine = function (rows, dateKey, metric, label) {
var iso = 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 n = new Date(), today = n.getFullYear() + '-' + ('0' + (n.getMonth() + 1)).slice(-2) + '-' + ('0' + n.getDate()).slice(-2);
rows = (rows || []).filter(function (r) { return iso(r) < today; }).sort(function (a, b) { return iso(a) < iso(b) ? -1 : 1; });
var dates = rows.map(iso);
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.dayWeek(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.dayWeek(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 op de widget (zoals het oude dashboard): even hoog naast elkaar en niets verspringt bij het laden;
// 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('');
textarea.css({
position: 'fixed',
opacity: '0'
});
textarea.val(text);
body.append(textarea);
textarea[0].select();
try {
var successful = document.execCommand('copy');
if (!successful) throw successful;
} catch (err) {
}
textarea.remove();
}
// single link actions
var fnSingle = {
'getParams': function(link) {
return {
'websiteid': h.siteId(),
'ids': [link._id.$oid],
}
},
'pre_edit': function(link) {
fnCategories.list();
$scope.page.curlink = link;
$scope.fnModal('linkbuilding/modal/single-edit')
},
'edit': function(link) {
var fields = ['partner.url', 'my.status', 'my.url', 'my.anchor', 'date', 'text', 'trade.anchor', 'trade.url', 'partner.type', 'partner.domain.contactform', 'partner.domain.email', 'partner.domain.manager.email', 'partner.domain.manager.name', 'partner.domain.manager.CatId'];
$http.post("/rest/linkbuilding/link.edit",
angular.extend(link, fnSingle.getParams(link), { 'fields': fields })
).then(function successCallback(res) { fnLinks.list(); })
},
'pre_mail': function(link) {
if (!link.mail && $scope.mailTemplates[0] !== undefined) {
link.mail = { key: $scope.mailTemplates[0].key }
}
$scope.page.curlink = link;
$scope.fnModal('linkbuilding/modal/mail')
},
'mail': function(link) {
$http.post("/rest/linkbuilding/link.mail",
angular.extend(link, fnSingle.getParams(link))
).then(function successCallback(res) {
fnLinks.list();
})
},
'contactform': function(link) {
$scope.page.curlink = link;
$scope.page.curlink.mailtemplate = $scope.mailTemplates[$scope.mailTemplates.length - 1];
$scope.fnModal('linkbuilding/modal/contactform')
},
'getmailbody': function(link) {
var text = link.mailtemplate.mails[0].body;
// strip tags
text = text.replace(/(<([^>]+)>)/ig, "");
//replace
text = text.replace(/\[partner_link\]/g, link.partner.url);
text = text.replace(/\[partner_domain\]/g, link.partner.domain.domain);
console.log(text);
return text;
},
'checkstatus': function(link) {
link.inprogress = true;
$http.get("/rest/linkbuilding/link.checkstatus", {
'params': { websiteid: h.siteId(), '_id': link['_id']['$oid'] }
}).then(function successCallback(res) {
link.inprogress = false;
link.my = res.data.my
});
},
'delete': function(link) {
$http.post("/rest/linkbuilding/link.delete",
fnSingle.getParams(link)
).then(function successCallback(res) {
fnLinks.list();
});
}
}
// multiple link actions
var fnMultiple = {
getParams: function() {
var ids = [];
for (var i in $scope.links) {
if ($scope.links[i].checked) {
ids.push($scope.links[i]._id.$oid)
}
}
return {
'websiteid': h.siteId(),
'ids': ids,
}
},
multioptions: function() {
$scope.page.multioptions = false;
for (var i in $scope.links) {
if ($scope.links[i].checked) {
$scope.page.multioptions = true;
break
}
}
},
check: function(array) {
var event = array[0];
var link = array[1];
if (event.ctrlKey || event.shiftKey) {
var forceselect = false;
var _links = $filter('filter')($scope.links, $scope.page.filter);
// var _links = $scope.links;
for (var _link in _links) {
// curlink zetten
var curlink = _links[_link];
// eingen link gevonden
if (forceselect == true) {
if (curlink == link) {
forceselect = false;
break;
}
if (curlink.checked == true) {
forceselect = false;
break;
}
curlink.checked = true;
}
// 1e selectie = true
if (forceselect == false) {
if (curlink.checked == true) {
forceselect = true;
}
if (curlink == link) {
forceselect = true;
}
}
}
}
link.checked = !link.checked;
fnMultiple.multioptions();
},
checkall: function() {
$scope.page.checkall = !$scope.page.checkall;
var _links = $filter('filter')($scope.links, $scope.page.filter);
for (var i in _links) {
_links[i].checked = $scope.page.checkall;
}
fnMultiple.multioptions();
},
quickselect: function(m) {
for (var i in $scope.links) {
var bool = true
var t = false
if ($scope.page.quickselect.email) {
t = true;
bool = ($scope.links[i].partner.domain.email) ? bool : false;
}
if ($scope.page.quickselect.contactform) {
t = true;
bool = ($scope.links[i].partner.domain.contactform) ? bool : false;
}
if ($scope.page.quickselect.nocontact) {
t = true;
bool = ($scope.links[i].partner.domain.email || $scope.links[i].partner.domain.contactform) ? false : bool;
}
if (t) {
$scope.links[i].checked = bool;
}
}
fnMultiple.multioptions();
},
_findallstatus: function() {
for (var i in $scope.links) {
if ($scope.links[i].checked) {
$http.get("/rest/linkbuilding/link.checkstatus", {
'params': { websiteid: h.siteId(), '_id': $scope.links[i]['_id']['$oid'] }
}).then(function successCallback(res) {
$scope.links[i].inprogress = false;
$scope.links[i].checked = false;
fnMultiple._findallstatus();
});
break;
}
}
},
findallstatus: function() {
for (var i in $scope.links) {
if ($scope.links[i].checked) {
$scope.links[i].inprogress = true;
}
}
fnMultiple._findallstatus();
},
_findallcontact: function() {
for (var i in $scope.links) {
if ($scope.links[i].checked) {
$http.get("/rest/linkbuilding/link.findcontact", {
'params': { websiteid: h.siteId(), '_id': $scope.links[i]['_id']['$oid'] }
}).then(function successCallback(res) {
$scope.links[i] = res.data;
var date = new Date($scope.links[i].date * 1);
$scope.links[i].monthstring = date.toLocaleString('default', { year: 'numeric', 'month': 'long' })
$scope.links[i].date = $scope.page.kdates[date.toString()];
$scope.links[i].inprogress = false;
$scope.links[i].checked = false;
fnMultiple._findallcontact();
});
break;
}
}
},
findallcontact: function() {
for (var i in $scope.links) {
if ($scope.links[i].checked) {
$scope.links[i].inprogress = true;
}
}
fnMultiple._findallcontact();
},
'status': function() {
$http.post("/rest/linkbuilding/link.edit",
angular.extend({ 'fields': ['my.status'] },
fnMultiple.getParams(),
$scope.page.multiplelinks)
).then(function successCallback(res) {
fnLinks.list();
})
},
'date': function() {
$http.post("/rest/linkbuilding/link.edit",
angular.extend({ 'fields': ['date'] },
fnMultiple.getParams(),
$scope.page.multiplelinks)
).then(function successCallback(res) {
fnLinks.list();
})
},
'tag': function() {
$http.post("/rest/linkbuilding/link.edit",
angular.extend({ 'fields': ['my.tag'] },
fnMultiple.getParams(),
$scope.page.multiplelinks)
).then(function successCallback(res) {
fnLinks.list();
})
},
pre_edit: function(link) {
},
edit: function(link) {
$http.post("/rest/rank-tracker/edit",
angular.extend({
'my.status': $scope.page.multiplelinks.status,
'fields': ['my.status']
}, fnMultiple.getParams(link))).then(function successCallback(res) {
fnLinks.list();
})
},
pre_mail: function() {
$scope.page.multiplelinks.mail = { key: $scope.mailTemplates[0].key }
$scope.fnModal('linkbuilding/modal/multiple.mail')
},
'mail': function(link) {
$http.post("/rest/linkbuilding/link.mail",
angular.extend($scope.page.multiplelinks, fnMultiple.getParams())
).then(function successCallback(res) {
fnLinks.list();
})
},
delete: function(link) {
$http.post("/rest/linkbuilding/link.delete", fnMultiple.getParams(link)).then(function successCallback(res) { fnLinks.list(); })
}
}
$scope.fnExportAll = function() {
var text = '';
var out = '';
for (var _link in $scope.links) {
var curlink = $scope.links[_link];
text = text + curlink.url + "\r\n";
out += $filter('date')(new Date(curlink.d), 'dd-MM-yyyy') + ';'
out += curlink.partner.url+';'
if(curlink.my.url){
out += curlink.my.url+';'
} else {
out += ';'
}
if(curlink.my.url){
out += curlink.my.anchor + ';'
} else {
out += ';'
}
out += curlink.my.status + ';'
out += "\n";
}
var element = document.createElement('a');
element.setAttribute('href', 'data:text/html;,' + encodeURIComponent(out));
element.setAttribute('download', 'linkbuilding_export.csv');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
// list links
fnLinks.list();
fnTemplates.list();
}
]);
app.controller('linkbuildingtemplate', ['$scope', '$timeout', '$http', '$rootScope', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $filter, h) {
//haal Template templates op
var getTemplates = function() {
$http.get('/rest/linkbuilding/template.list', {
params: {
'websiteid': h.siteId(),
}
}).then(function successCallback(res) {
$scope.mailTemplates = res.data;
});
};
getTemplates();
$scope.curTemplate = { workflow: [] };
$scope.fnNewTemplate = function() {
$scope.hideTemplateoptions = true;
$scope.templatetab = 1
$scope.curTemplate = { method: 'manual', provider:"", mails: [{ 'type': 'primary' }] };
}
$scope.fnRemoveFollowUp = function(index) {
$scope.curTemplate.mails.splice(index, 1);
}
$scope.fnAddFollowUp = function() {
$scope.curTemplate.mails.push({ method: 'manual', 'type': 'followup' });
}
$scope.fnCloseTemplate = function(curTemplate) {
$scope.hideTemplateoptions = false;
$scope.templatetab = 0
}
$scope.fnAddWorkFlow = function() {
$scope.curTemplate.workflow.push({ persoon: USER.UserId + "" });
}
$scope.fnRemoveWorkFlow = function(index) {
$scope.curTemplate.workflow.splice(index, 1);
}
$scope.fnAddTemplate = function() {
$http.post('/rest/linkbuilding/template.add', { 'websiteid': h.siteId(), 'template': $scope.curTemplate }).then(function successCallback(res) {
getTemplates();
$scope.templatetab = 0;
$scope.hideTemplateoptions = false;
});
}
$scope.fnShowEditTemplate = function(mailTemplate) {
$scope.templatetab = 1
$scope.curTemplate = mailTemplate;
}
$scope.fnDeleteTemplate = function(mailTemplate) {
$http.post('/rest/linkbuilding/template.delete', { 'websiteid': h.siteId(), 'template': mailTemplate}).then(function successCallback(res) {
getTemplates();
$scope.templatetab = 0;
$scope.hideTemplateoptions = false;
});
}
}
]);
app.controller('user', ['$scope', '$rootScope', '$http', '$location', '$translate', 'h',
function($scope, $rootScope, $http, $location, $translate, h) {
$scope.curUser = {};
$scope.page = {};
var _cb = {
loginfailed: function() {
$scope.page.message = { 'key': 'user.global.message_login_failed', class: 'text-warning' }
},
loginok: function(res) {
$rootScope.user = res.user;
//console.log($rootScope.user);
$location.path('/');
},
registrationnotvalid: function() {
$scope.page.message = { 'key': 'user.global.message_registration_failed', class: 'text-warning' }
},
registrationexists: function() {
$scope.page.message = { 'key': 'user.global.message_login_exists', class: 'text-warning' }
},
registrationok: function(res) {
$rootScope.user = res.user;
$scope.fnGoto('/sites/first');
//h.login($location.path('/sites/first'));
},
recoverfailed: function() {
$scope.page.message = { 'key': 'user.global.message_recover_failed', class: 'text-warning' }
},
recoverok: function() {
$scope.page.message = { 'key': 'user.global.message_recover_ok', class: 'text-warning' }
},
}
$scope.checklogin = function() {
if ($rootScope.user._id) {
$location.path('/');
}
if (!$rootScope.user.reseller.config.own && $location.path() == '/user/register') {
$location.path('/user/login');
}
}
$scope.userdetails = {
init: function() {
$http.get('/rest/user/user.get').then(function successCallback(res) {
$scope.curUser = res.data;
});
},
'set': function() {
$http.post('/rest/user/user.set', $scope.curUser);
},
'invoice': function(key) {
$scope.fnGoto('/rest/pay/invoice.link?type=user&key=' + key)
}
}
$scope.fnLogin = function() {
$http.post('/rest/user/login', $scope.curUser).then(function successCallback(res) {
_cb[res.data.cb](res.data)
});
}
$scope.fnRegister = function() {
$http.post('/rest/user/register', $scope.curUser).then(function successCallback(res) {
_cb[res.data.cb](res.data)
});
}
$scope.fnRecover = function() {
$http.post('/rest/user/recover', $scope.curUser).then(function successCallback(res) {
_cb[res.data.cb](res.data)
});
}
}
]);
app.controller('sites', ['$scope', '$rootScope', '$http', '$location', 'h',
function($scope, $rootScope, $http, $location, h) {
$scope.rand = function(min,max){
return Math.floor(Math.random() * (max - min + 1) + min)
}
$scope.bShowAdminDelete = function(site) {
return (site.status.expire > (Date.now() / 1000)) ? false : true;
}
$scope.page = {
pending: false,
orderstate: 'init'
};
if ($scope.orderstate) {
$scope.page.orderstate = $scope.orderstate;
}
$scope.curSite = {};
$scope.do = function(obj, method, param) {
if (obj == 'fnSite') {
fnSite[method](param);
}
}
var fnSite = {
'wemessage': function() {
$scope.fnGoto('/rest/pay/wemessage.start')
},
'list': function() {
$http.get('/rest/sites/list').then(function successCallback(res) {
$scope.sites = res.data;
});
},
'add': function() {
$http.post('/rest/sites/add', $scope.curSite).then(function successCallback(res) {
_cb[res.data.cb](res.data)
});
},
'adminupgrade': function() {
$http.post('/rest/sites/adminupgrade', { websiteid: h.siteId() })
},
'unpark': function() {
$http.post('/rest/sites/adminunpark', { websiteid: h.siteId() }).then(function successCallback(res) {
h.login(function() { $scope.fnClick('/') });
});
},
'admindelete': function(site) {
$http.post('/rest/sites/remove', site).then(function successCallback(res) {
side.hidden = true;
});
},
'delete': function(site) {
$http.post('/rest/sites/remove', $scope.curSite).then(function successCallback(res) {
$scope.curSite = {};
h.login(function() {});
fnSite.list();
});
},
'pre_edit': function(site) {
$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.curSite = site;
$scope.fnModal('sites/modal/edit');
},
'edit': function(site) {
$http.post('/rest/sites/edit', site).then(function successCallback(res) {
});
},
'order_options': function() {
$http.get('/rest/pay/options.list').then(function successCallback(res) {
$scope.options = res.data;
var index = ($rootScope.user.site.locale.lang == 'nl') ? 0 : 1;
$scope.order = { 'currency': $scope.options.currency[index], 'method': $scope.options.methods[index] };
});
},
'pre_order': function(package) {
$http.post('/rest/sites/edit', site).then(function successCallback(res) {
});
},
}
var _cb = {
domainvalid: function(site) {
h.login(function() {
$location.path('/');
});
},
domaininvalid: function() {
$scope.page.message = { html: 'The domain is invalid', class: 'text-warning' }
},
domainduplicate: function() {
$scope.page.message = { html: 'This domain is not available for trials versions.', class: 'text-warning' }
},
deletefailed: function() {
$scope.page.message = { html: 'Registration failed, the user with email adress ' + $scope.curUser.email + ' allready exists. Did you mean to ', class: 'text-danger' }
},
}
}
]);
app.controller('site', ['$scope', '$timeout', '$http', '$rootScope', '$location', '$anchorScroll', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $location, $anchorScroll, $filter, h) {
$scope.page = {
newteammember: {},
newmenu: {}
};
$scope.roles = [
{ 'role': 4, 'name': 'owner' },
{ 'role': 5, 'name': 'normal user' },
{ 'role': 6, 'name': 'guest' },
]
$scope.do = function(obj, method, param) {
if (obj == 'fnSite') {
fnSite[method](param);
} else if (obj == 'fnPage') {
fnPage[method](param);
}
}
$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;
});
var fnSite = {
get: function() {
$http.get('/rest/sites/get', {
params: {
'websiteid': h.siteId(),
}
}).then(function successCallback(res) {
if (res.data._id) {
$scope.page.tab = 'edit';
$scope.user.site.users = res.data.users;
$scope.user.site.names = res.data.names;
$scope.user.site.api = res.data.api;
} else {
$scope.page.tab = 'intro';
}
});
},
'searchcity': function(q) {
$http.get('/rest/locale/city.list', { 'params': { 'q': q } }).then(function successCallback(res) {
$scope.cities = res.data;
});
},
'setcity': function(city) {
$scope.user.site.locale.city = city.city;
$scope.user.site.locale.uule = city.uule;
$scope.cities = [];
},
'add': function() {
$http.post('/rest/sites/teammember.add',
angular.extend({ 'websiteid': h.siteId() }, $scope.page.newteammember)
).then(function successCallback(res) {
$scope.page.newteammember = {};
fnSite.get();
});
},
'park': function() {
$http.post('/rest/sites/adminpark', { websiteid: h.siteId() }).then(function successCallback(res) {
h.login(function() { $scope.fnClick('/') });
});
},
'delete': function(teammember) {
$http.post('/rest/sites/teammember.delete',
angular.extend({ 'websiteid': h.siteId() }, teammember)
).then(function successCallback(res) {
fnSite.get();
});
},
'update': function(reseller) {
$http.post('/rest/sites/teammembers.update',
angular.extend({ 'websiteid': h.siteId() }, $scope.user.site)
).then(function successCallback(res) {
fnSite.get();
});
},
'edit': function(site) {
if (!$scope.user.site.locale.city || !$scope.user.site.locale.uule) {
$scope.user.site.locale.uule = '';
$scope.user.site.locale.city = '';
}
$http.post('/rest/sites/edit',
angular.extend({ 'websiteid': h.siteId() }, $scope.user.site)
).then(function successCallback(res) {
});
},
'addmenu': function() {
$scope.user.site.menu = $scope.user.site.menu || [];
$scope.user.site.menu.push($scope.page.newmenu);
fnSite.edit($scope.user.site);
},
'removemenu': function(index) {
$scope.user.site.menu.splice(index, 1);
fnSite.edit($scope.user.site);
},
}
fnSite.get();
}
]);
// Site-instellingen, Koppelingen: elke koppeling van deze site, om te koppelen of weg te halen (rest/data/connections)
app.controller('siteConnections', ['$scope', '$http', '$translate', 'h', function ($scope, $http, $translate, h) {
var s = $scope.conn = { list: null, siteId: h.siteId() };
var load = function () {
$http.get('/rest/connections/list', { params: { websiteid: h.siteId() } }).then(function (res) {
s.list = res.data.connections || [];
});
};
// eerst een bevestiging in de rij zelf (geen browserdialoog), dan weg
s.remove = function (c) {
if (s.asking !== c.key) { s.asking = c.key; return; }
s.asking = null;
c.busy = true;
$http.post('/rest/connections/remove', { websiteid: h.siteId(), key: c.key }).then(function () {
$scope.$root.$broadcast('siteConnectionsChanged', c.key);
load();
}, function () { c.busy = false; });
};
load();
}]);
app.controller('contentlist', ['$scope', '$http', '$rootScope', '$filter', '$location', 'h', '$timeout',
function($scope, $http, $rootScope, $filter, $location, h, $timeout) {
$scope.page = { 'newmodel': { 'lang': $rootScope.user.site.locale.lang } };
var content = {
listModels: function() {
$http.get('/rest/content/model.list', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
$scope.models = res.data.data;
content.checkProgress();
});
},
addModel: function(model) {
$http.post('/rest/content/model.add', angular.extend({ websiteid: h.siteId() }, model)).then(function successCallback(res) {
$scope.page.newmodel = {}
content.listModels();
});
},
createModel: function(model) {
$http.post('/rest/content/model.create', angular.extend({ websiteid: h.siteId() }, model)).then(function successCallback(res) {
model.status = 'checking';
});
},
deleteModel: function(model) {
$http.post('/rest/content/model.delete', angular.extend({ websiteid: h.siteId() }, model)).then(function successCallback(res) {
content.listModels();
});
},
checkProgress: function() {
var l = false;
for (var i in $scope.models) {
if ($scope.models[i].status == 'pending') {
l = true;
}
}
if (l) {
$timeout(function() {
content.listModels()
}, 5000);
}
}
}
$scope.do = function(method, param) {
content[method](param);
}
content.listModels()
}
]);
app.directive('wrapInTag', function () {
return {
restrict: 'A',
link: function (scope, elm, attr) {
console.log(attr);
var tag = attr.tag || 'strong';
var words = eval(attr.words) || [];
var text = attr.text;
for (var i = 0; i < words.length; i++) {
text = text.replace(words[i], '<' + tag + '>' + words[i] + '' + tag + '>');
}
elm.html(text);
}
}
});
app.controller('contentmodel', ['$scope', '$http', '$rootScope', '$filter', '$location', 'h', '$timeout',
function ($scope, $http, $rootScope, $filter, $location, h, $timeout) {
$scope.page = {
states: {}, 'tab': 'research', 'type': 'article', 'tones': {
"informative": true,
"persuasive": false,
"emotional": false,
"witty": true,
"funny": false,
"professional": true,
"informal": false
}
};
$scope.mypage = {};
$scope.page.states.explore = { 'open': false, 'cluster': 1 }
var model = {
getModel: function () {
$http.get('/rest/content/model.get', {
params: {
'websiteid': h.siteId(),
'modelid': $scope.modelid
}
}).then(function successCallback(res) {
$scope.q = res.data.q;
$scope.clusters = res.data.clusters;
$scope.dna = res.data.dna;
$scope.wordfreq = res.data.wordfreq;
$scope.trans = res.data.trans;
$scope.r = res.data.r;
$scope.lsi = res.data.lsi;
$scope.target = res.data.target;
$scope.pages = res.data.pages;
$scope.write = res.data.write;
$scope.x = {};
for (var i in $scope.wordfreq) {
$scope.x[$scope.wordfreq[i].word] = $scope.wordfreq[i].avg;
$scope.wordfreq[i].maxminavg = $scope.wordfreq[i].max - $scope.wordfreq[i].avg
}
charts.tfchart();
charts.dnaChart();
model.getMyPage();
});
},
setMyPage: function () {
$scope.mypage.pending = true;
$http.post('/rest/content/model.setmypage', angular.extend({ websiteid: h.siteId(), 'modelid': $scope.modelid }, $scope.mypage)).then(function successCallback(res) {
model.getMyPage()
});
},
writestatus: function () {
var l = false;
$http.get('/rest/content/model.get', {
params: {
'websiteid': h.siteId(),
'modelid': $scope.modelid
}
}).then(function successCallback(res) {
if (res.data.write.status == 'pending') {
l = true;
}
if (l) {
$timeout(function () {
model.writestatus();
}, 5000);
} else {
model.getMyPage();
$scope.write = res.data.write;
}
});
},
write: function () {
$scope.write = $scope.write || {};
$scope.write.status = 'pending';
$scope.page.tab = 'write';
$http.post('/rest/content/model.pre_write', angular.extend({ websiteid: h.siteId(), 'modelid': $scope.modelid }, { 'type': $scope.page.type, 'tones': $scope.page.tones })).then(function successCallback(res) {
model.writestatus();
// need to set up checks here!
});
},
getMyPage: function () {
$scope.mypage.pending = true;
$http.get('/rest/content/model.getmypage', {
params: {
'websiteid': h.siteId(),
'modelid': $scope.modelid
}
}).then(function successCallback(res) {
$scope.mypage = res.data;
if (res.data.mna) {
for (var i in $scope.wordfreq) {
var word = $scope.wordfreq[i].word;
$scope.wordfreq[i].my = res.data.counter[word] || 0;
}
$scope.mna = res.data.mna;
charts.tfchart();
charts.dnaChart();
$scope.mypage.pending = false;
model.getSpamandSuggestions();
$scope.page.tab = 'write';
}
});
},
getSpamandSuggestions: function () {
$scope.kwspam = [];
$scope.kwsuggestions = [];
for (var i in $scope.wordfreq) {
var f = $scope.wordfreq[i];
if (f.my > f.max) {
$scope.kwspam.push({ 'word': f.word, 'my': f.my, 'max': f.max });
}
if (f.my < f.avg) {
$scope.kwsuggestions.push({ 'word': f.word, 'my': f.my, 'avg': f.avg });
}
}
},
exploreCluster(cluster) {
lines = [];
$scope.page.states.explore = { 'open': true, 'cluster': cluster }
for (var i in $scope.clusters) {
for (var y in $scope.clusters[i]['lines']) {
var line = $scope.clusters[i]['lines'][y]['text'];
var src = $scope.clusters[i]['lines'][y]['src'];
var fit = $scope.clusters[i]['lines'][y]['dna'][cluster];
if ($scope.clusters[i]['lines'][y]['text'].length > 133) {
lines.push({ 'line': line, 'fit': fit, 'src': src, 'wordscore': $scope.clusters[i]['lines'][y]['wordscore'], 'score': $scope.clusters[i]['lines'][y]['score'] });
}
}
}
$scope.page.lines = lines;
},
exploreWord: function (word) {
$scope.page.states.explore.open = false;
var s = word.split(' ');
var boldarray = [];
for (var i in s) {
for (var w in $scope.trans[s[i]]) {
boldarray.push(w)
}
}
suggestions = [];
for (var i in $scope.clusters) {
for (var y in $scope.clusters[i]['lines']) {
var sent = $scope.clusters[i]['lines'][y];
if (sent.clean.indexOf(' ' + word + ' ') !== -1) {
suggestions.push(sent)
$scope.page.states.exploreword = { 'open': true, 'word': word, 'boldarray': boldarray, 'suggestions': suggestions }
}
}
}
}
}
model.getModel();
$scope.do = function (method, param) {
model[method](param);
}
var charts = {
tfchart: function () {
var d = angular.copy($filter('orderBy')($scope.wordfreq, 'avg', true));
var d = d.map(function (c) {
c.word = ($scope.trans[c.word]) ? Object.keys($scope.trans[c.word])[0] : c.word;
return c;
})
$scope.tfidfchart = {
data: d,
type: "serial",
//autoMarginOffset: 20,
marginLeft: 16,
categoryField: "word",
legend: {
enabled: false
},
chartScrollbar: {
"enabled": true,
"hideResizeGrips": true,
"offset": 10,
"scrollbarHeight": 10
},
categoryAxis: {
gridPosition: "start",
parseDates: false,
gridThickness: 0,
"labelRotation": 45
},
valueAxes: [{
"stackType": "regular",
dashLength: 4,
color: '#dedede',
position: "top",
axisAlpha: 0,
labelsEnabled: false
}],
graphs: [{
balloonText: "AVG distribution top 10 result [[value]] ",
type: "column",
title: "Keyword value",
valueField: "avg",
fillAlphas: 1,
lineColor: '#28a745',
}, {
balloonText: "Max distribution top 10 result [[value]] ",
type: "column",
title: "Keyword value",
valueField: "maxminavg",
fillAlphas: 1,
lineColor: '#dddddd',
}, {
balloonText: "My distribution [[value]] ",
"bullet": "round",
"bulletBorderAlpha": 1,
"bulletColor": "#FFFFFF",
"useLineColorForBulletBorder": true,
"fillAlphas": 0,
"lineThickness": 2,
"lineAlpha": 1,
"bulletSize": 7,
"valueField": "my",
lineColor: '#ffc107',
}],
zoom: [0, 18]
}
},
dnaChart: function () {
var dna = [];
var mna = [];
var ordered_clusters = $filter('orderBy')($scope.clusters, 'score', true);
for (var i in ordered_clusters) {
if ($scope.mna) {
$scope.max_fit = (Math.max(...$scope.dna), Math.max(...$scope.mna))
dna.push({ 'cluster': ordered_clusters[i].cluster, 'dna': $scope.dna[ordered_clusters[i].cluster] * 100, 'mna': $scope.mna[ordered_clusters[i].cluster] * 100, 'i': (i * 1) + 1 })
} else {
$scope.max_fit = Math.max(...$scope.dna);
dna.push({ 'cluster': ordered_clusters[i].cluster, 'dna': $scope.dna[ordered_clusters[i].cluster] * 100, 'i': (i * 1) + 1 })
}
}
$scope.dnachart = {
"type": "radar",
data: dna,
"theme": "none",
"valueAxes": [{
"labelsEnabled": false,
"axisTitleOffset": 14,
"minimum": 0,
"axisAlpha": 0.15
}],
"startDuration": 2,
"graphs": [{
"balloonText": "Cluster [[i]] Goal: [[value]]%",
"bullet": "round",
"lineThickness": 2,
"fillAlphas": 0.8,
"valueField": "dna",
lineColor: '#28a745',
}, {
"balloonText": "Cluster [[i]] My page: [[value]]%",
"bullet": "round",
"color": "#dedede",
"fillAlphas": 0.3,
"lineThickness": 2,
"valueField": "mna",
}],
"categoryField": "i",
"export": {
"enabled": true
}
};
},
}
$scope.untrans = function (w) {
var s = w.split(' ');
for (var i in s) {
s[i] = Object.keys($scope.trans[s[i]])[0];
}
return s.join(' ');
}
}
]);
app.controller('report', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$window', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $window, h) {
// nieuw of huidig rapport
$scope.page = { 'newreport': { show: false }, dates: [], kdates: {},'simpleview':true };
var d = new Date();
var thismonth = d.getMonth() + 1;
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;
}
$scope.do = function(obj, method, param) {
if (obj == 'fnReport') {
fnReport[method](param);
}
}
var fnReport = {
list: function() {
$http.get('/rest/report/report.list', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
$scope.reports = res.data;
angular.forEach(res.data, function(k, v) {
var date = new Date(k.download.date * 1);
k.download.date = $scope.page.kdates[date.toString()];
k.download = k.download || { status: '' }
if (k.download.status == 'pending') {
$timeout(function() {
fnReport.list();
}, 10000);
}
})
});
},
'add': function(report) {
$http.post('/rest/report/report.add',
angular.extend({ websiteid: h.siteId() }, report)
).then(function successCallback(res) {
fnReport.list();
});
},
create: function(report) {
$http.post('/rest/report/report.generate', angular.extend({ websiteid: h.siteId() }, report))
report.download.status = 'pending'
fnReport.update(report);
},
'update': function(report) {
report.download = report.download || {};
report.download.date = report.download.date || new Date();
report.download.date = Math.round(report.download.date.getTime() / 1000)
if(report.download.enddate){
if($scope.page.simpleview == true){
delete report.download.enddate
} else {
console.log(report.download.enddate);
report.download.enddate = report.download.enddate || new Date();
report.download.enddate = Math.round(report.download.enddate.getTime() / 1000)
}
}
$http.post('/rest/report/report.update',
angular.extend({ websiteid: h.siteId() }, report)
).then(function successCallback(res) {
fnReport.list();
});
},
'download': function(report) {
console.log(report);
$window.location.href = '/rest/report/report.download/?websiteid=' + h.siteId() + '&reportid=' + report.report_id;
},
'delete': function(report) {
$http.post('/rest/report/report.delete',
angular.extend({ websiteid: h.siteId() }, report)
).then(function successCallback(res) {
fnReport.list();
});
},
}
fnReport.list();
}
]);
app.controller('reportitem', ['$scope', '$timeout', '$http', '$rootScope', '$location', '$anchorScroll', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $location, $anchorScroll, $filter, h) {
$scope.do = function(obj, method, param) {
if (obj == 'fnReport') {
fnReport[method](param);
} else if (obj == 'fnPage') {
fnPage[method](param);
}
}
// Search Insights en Rank tracker: dezelfde controllers als de pagina's, in rapportstand (custominit.gsci / .serp)
$scope.reportcategories = [{
'translate': 'gscinsights.title',
'types': [
{ 'translate': 'gscinsights.tab.overview', 'view': 'gscinsights/report_page.html', 'init': { 'gsci': true, 'urlview': 'overview' } },
{ 'translate': 'gscinsights.tab.topics', 'view': 'gscinsights/report_page.html', 'init': { 'gsci': true, 'urlview': 'topics' } },
{ 'translate': 'gscinsights.tab.tracked', 'view': 'gscinsights/report_page.html', 'init': { 'gsci': true, 'urlview': 'tracked' } },
{ 'translate': 'gscinsights.overview.actions_title', 'view': 'gscinsights/report_page.html', 'init': { 'gsci': true, 'urlview': 'actions' } },
{ 'translate': 'menu.searchconsole.keywords', 'view': 'gscinsights/report.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'keywords' } },
{ 'translate': 'menu.searchconsole.pages', 'view': 'gscinsights/report.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'pages' } },
{ 'translate': 'menu.searchconsole.country', 'view': 'gscinsights/report.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'country' } },
{ 'translate': 'menu.searchconsole.device', 'view': 'gscinsights/report.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'device' } }
]
},
{
'translate': 'menu.serp',
'types': [
{ 'translate': 'serp.tab.overview', 'view': 'serp/report.html', 'init': { 'serp': true, 'urlview': 'overview' } },
{ 'translate': 'serp.tab.keywords', 'view': 'serp/report.html', 'init': { 'serp': true, 'urlview': 'keywords' } },
{ 'translate': 'serp.tab.competitors', 'view': 'serp/report.html', 'init': { 'serp': true, 'urlview': 'competitors' } }
]
},
/* {
'translate': 'menu.linkbuilding.linkbuilding',
'types': [
{ 'translate': 'menu.linkbuilding.assistant', 'view': 'linkbuilding/assistant.html', 'init': '' },
]
},*/
{
'translate': 'menu.onpage.onpage',
'types': [
{ 'translate': 'menu.onpage.overview', 'view': 'onpage/index.status.html', 'init': { 'viewrulelabel': 'all' } }
]
},
{
'translate': 'menu.analytics4.analytics',
'types': [
{ 'parent': 'menu.analytics.channels.channels', 'translate': 'menu.analytics.channels.all', 'view': 'api/analytics4.html', 'init': { 'urlreport': 'channels', 'urlview': 'all' } },
{ 'parent': 'menu.analytics.channels.channels', 'translate': 'menu.analytics.channels.organic', 'view': 'api/analytics4.html', 'init': { 'urlreport': 'channels', 'urlview': 'organic' } },
{ 'parent': 'menu.analytics.channels.channels', 'translate': 'menu.analytics.channels.paid', 'view': 'api/analytics4.html', 'init': { 'urlreport': 'channels', 'urlview': 'paid' } },
{ 'parent': 'menu.analytics.channels.channels', 'translate': 'menu.analytics.channels.display', 'view': 'api/analytics4.html', 'init': { 'urlreport': 'channels', 'urlview': 'display' } },
{ 'parent': 'menu.analytics.channels.channels', 'translate': 'menu.analytics.channels.social', 'view': 'api/analytics4.html', 'init': { 'urlreport': 'channels', 'urlview': 'social' } },
{ 'parent': 'menu.analytics.channels.channels', 'translate': 'menu.analytics.channels.referral', 'view': 'api/analytics4.html', 'init': { 'urlreport': 'channels', 'urlview': 'referral' } },
{ 'parent': 'menu.analytics.channels.channels', 'translate': 'menu.analytics.channels.email', 'view': 'api/analytics4.html', 'init': { 'urlreport': 'channels', 'urlview': 'email' } },
{ 'parent': 'menu.analytics.channels.channels', 'translate': 'menu.analytics.channels.other', 'view': 'api/analytics4.html', 'init': { 'urlreport': 'channels', 'urlview': 'other' } },
]
},
{
'translate': 'menu.googleads.googleads',
'types': [
{ 'translate': 'menu.googleads.all', 'view': 'api/googlexads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'all' } },
{ 'translate': 'menu.googleads.adgroups', 'view': 'api/googlexads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'adgroups' } },
{ 'translate': 'menu.googleads.keywords', 'view': 'api/googlexads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'keywords' } },
{ 'translate': 'menu.googleads.ads', 'view': 'api/googlexads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'ads' } },
{ 'translate': 'menu.googleads.conversions', 'view': 'api/googlexads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'conversions' } },
]
},
{
'translate': 'menu.facebookads.facebookads',
'types': [
{ 'translate': 'menu.facebookads.all', 'view': 'api/facebookxads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'all' } },
{ 'translate': 'menu.facebookads.adsets', 'view': 'api/facebookxads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'adsets' } },
{ 'translate': 'menu.facebookads.ads', 'view': 'api/facebookxads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'ads' } },
{ 'translate': 'menu.facebookads.keywords', 'view': 'api/facebookxads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'keywords' } },
{ 'translate': 'menu.facebookads.age', 'view': 'api/facebookxads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'age' } },
{ 'translate': 'menu.facebookads.gender', 'view': 'api/facebookxads.html', 'init': { 'urlreport': 'campaigns', 'urlview': 'gender' } },
]
},
{
'translate': 'menu.facebookinsights.facebookinsights',
'types': [
{ 'translate': 'menu.facebookinsights.fans', 'view': 'api/facebookinsights.html', 'init': { 'urlreport': 'page', 'urlview': 'fans' } },
{ 'translate': 'menu.facebookinsights.engagements', 'view': 'api/facebookinsights.html', 'init': { 'urlreport': 'page', 'urlview': 'engagements' } },
{ 'translate': 'menu.facebookinsights.reach', 'view': 'api/facebookinsights.html', 'init': { 'urlreport': 'page', 'urlview': 'reach' } },
{ 'translate': 'menu.facebookinsights.posts', 'view': 'api/facebookinsights.html', 'init': { 'urlreport': 'page', 'urlview': 'posts' } }
]
},
{
'translate': 'menu.mybusiness.mybusiness',
'types': [
{ 'translate': 'menu.mybusiness.all', 'view': 'api/mybusiness.html', 'init': { 'urlreport': 'business', 'urlview': 'all' } },
{ 'translate': 'menu.mybusiness.reviews', 'view': 'api/mybusiness.html', 'init': { 'urlreport': 'business', 'urlview': 'reviews' } },
]
},
{
'translate': 'menu.planning',
'types': [
{ 'translate': 'menu.planning', 'view': 'planning/index.html', 'init': '' },
]
},
{
'translate': 'menu.linkbuilding',
'types': [
{ 'translate': 'menu.linkbuilding', 'view': 'linkbuilding/assistant.html', 'init': '' },
]
},
];
var fnPage = {
'new': function() {
return {};
},
'type': function(o) {
o.page.type = false;
$timeout(function() {
o.page.type = o.type;
o.page.changetype = false;
fnReport.update();
}, 500);
}
}
var fnReport = {
get: function() {
$http.get('/rest/report/report.get', {
params: {
websiteid: h.siteId(),
report_id: $scope.report_id,
}
}).then(function successCallback(res) {
console.log('ORIG DATE TS', res.data.download.date)
// Handle both seconds and milliseconds timestamps
var dateTs = res.data.download.date * 1;
if (dateTs < 10000000000) dateTs = dateTs * 1000; // Convert seconds to milliseconds
var date = new Date(dateTs);
if(res.data.download.enddate){
console.log('END DATE TS', res.data.download.enddate)
// Handle both seconds and milliseconds timestamps
var endDateTs = res.data.download.enddate * 1;
if (endDateTs < 10000000000) endDateTs = endDateTs * 1000;
var enddate = new Date(endDateTs);
var endts = new Date(enddate.getFullYear(), enddate.getMonth(), 0).getTime() / 1000;
} else {
var endts = new Date(date.getFullYear(), date.getMonth() + 1, 0).getTime() / 1000;
}
res.data.download.date = date;
$rootScope.reportdates = {
/* api */
'end': endts,
'start': new Date(date.getFullYear(), date.getMonth(), 1).getTime() / 1000,
/* planning */
'datestring': date.toLocaleString('default', { year: 'numeric', 'month': 'long' })
}
console.log('REPORTDATES', $rootScope.reportdates);
$scope.report = res.data;
});
},
getsettings: function() {
$http.get('/rest/report/settings.get', {
params: {
websiteid: h.siteId(),
report_id: $scope.report_id,
}
}).then(function successCallback(res) {
$scope.settings = res.data;
});
},
'addpage': function(index) {
console.log(index, 'is index');
$scope.report.pages = $scope.report.pages || [];
$scope.report.pages.splice(index, 0, fnPage.new());
$timeout(function() {
angular.element('html, body').animate({ scrollTop: angular.element('#page' + index).offset().top - 120 }, 400);
}, 200);
},
'deletepage': function(index) {
$scope.report.pages = $scope.report.pages || [];
$scope.report.pages.splice(index, 1);
fnReport.update();
},
'update': function(report) {
report = $scope.report;
report.download = report.download || {};
report.download.date = report.download.date || new Date();
report.download.date = Math.round(report.download.date.getTime() / 1000)
$http.post('/rest/report/report.update',
angular.extend({ websiteid: h.siteId() }, $scope.report)
).then(function successCallback(res) {
fnReport.get();
});
},
}
fnReport.get();
fnReport.getsettings();
}
]);
app.controller('reportpage', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$window', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $window, h) {
}
]);
app.controller('coreapi', ['$scope', '$timeout', '$http', '$rootScope', '$filter', 'h',
function ($scope, $timeout, $http, $rootScope, $filter, h) {
$scope.charts = {};
$scope.progresschart = {};
$scope.viewfilters = {};
var colorindex = 0;
$scope.dateoptions = [
{ 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' },
]
$scope.compareoptions = [
{ 'compare': false, 'comparedescription': 'Do not compare' },
{ 'compare': 'PERIOD', 'comparedescription': 'Compare with previous period' },
{ 'compare': 'YEAR', 'comparedescription': 'Compare with previous year' },
]
$scope.fnCustomDate = {
'init': function () {
var d = new Date()
d.setMonth(d.getMonth() - 1);
$scope.customdate = { 'start': d, 'end': new Date(), 'show': false }
},
'set': function () {
console.log('------------------- AHHHHH ');
$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';
getReportData();
}
}
$scope.fnCustomDate.init();
$scope.table = {
'curpage': 0,
'itemsperpage': 20,
'offset': 0,
fnSetOffset: function () {
this.offset = this.itemsperpage * (this.curpage - 1);
}
}
if (!$rootScope.reportdates) {
$rootScope.reportdates = { 'start': '30DAYSAGO', 'end': 'TODAY', 'description': 'Past 30 days', 'compare': false, 'comparedescription': 'Do not compare' };
}
$scope.fnDateSelection = function (reportdates) {
$rootScope.reportdates.start = reportdates.start;
$rootScope.reportdates.end = reportdates.end;
$rootScope.reportdates.description = reportdates.description;
getReportData();
}
$scope.fnCompareSelection = function (reportdates) {
$rootScope.reportdates.compare = reportdates.compare;
$rootScope.reportdates.comparedescription = reportdates.comparedescription;
getReportData();
}
$scope.getCustomer = function () {
if ($rootScope.user.site.role < 6) {
$http.get('/rest/' + ($scope.cfg.restbase || $scope.cfg.urlbase) + '/getcustomer', {
params: {
action: 'getproperties',
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
$scope.customers = res.data;
$scope.fnModal('api/modal/' + $scope.cfg.urlbase + '.selectcustomer');
});
}
}
$scope.setCustomer = function (data) {
$http.get('/rest/' + ($scope.cfg.restbase || $scope.cfg.urlbase) + '/setcustomer', {
params: {
websiteid: h.siteId(),
customer: data
}
}).then(function successCallback(res) {
getReportData();
});
}
$scope.useApiGrant = function (apigrant) {
$http.get('/rest/api-core/apigrants.set', {
params: {
websiteid: h.siteId(),
'type': $scope.cfg.urlbase,
'grantid': apigrant.grantid,
}
}).then(function successCallback(res) {
getReportData();
});
}
$scope.fnOptions = function () {
if ($scope.view.viewfilters) {
$http.get('/rest/' + $scope.cfg.urlbase + '/getviewfilters', {
params: {
websiteid: h.siteId(),
viewfilters: $scope.view.viewfilters.join(',')
}
}).then(function successCallback(res) {
$scope.viewfilteroptions = res.data.data
});
}
if ($scope.cfg.switchclient) {
$http.get('/rest/' + ($scope.cfg.restbase || $scope.cfg.urlbase) + '/getcustomer', {
params: {
action: 'getproperties',
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
$scope.customers = res.data;
});
}
$scope.fnModal('api/modal/' + $scope.cfg.urlbase + '.options');
}
$scope.pullCustomer = function () {
$http.get('/rest/' + ($scope.cfg.restbase || $scope.cfg.urlbase) + '/pullcustomer', {
params: {
websiteid: h.siteId()
}
}).then(function successCallback(res) {
getReportData();
});
}
var _fnFetchData = function (i) {
if ($scope.view[i]) {
var vv = {};
var metrics = ($scope.view[i].metrics) ? $scope.view[i].metrics : $scope.metrics;
if ($scope.viewfilters) {
for (var y in $scope.viewfilters) {
vv[y] = $scope.viewfilters[y]['viewfilter'];
}
}
// een view kan een eigen url hebben (de oude Search Console-subrapporten, tot die pagina weg is)
$http.get($scope.view[i].url || $scope.cfg.report_endpoint, {
params: {
websiteid: h.siteId(),
metrics: metrics.join(','),
dimension: $scope.view[i].dimension,
dimension2: $scope.view[i].dimension2,
report: $scope.view[i].report,
channel: $scope.view[i].channel,
context: $scope.view[i].context,
viewfilters: vv,
compare: $scope.view[i].compare || $rootScope.reportdates.compare,
allowcomp: $scope.view[i].allowcomp,
level: $scope.view[i].level,
endpoint: $scope.view[i].endpoint,
summary: $scope.view[i].summary,
breakdowns: $scope.view[i].breakdowns,
filterdimension: $scope.view[i].filterdimension,
filterexpression: $scope.view[i].filterexpression,
filteroperator: $scope.view[i].filteroperator,
sort: $scope.view[i].sort,
sortorder: $scope.view[i].sortorder,
start: ($scope.view[i].start) ? $scope.view[i].start : $rootScope.reportdates.start,
end: ($scope.view[i].end) ? $scope.view[i].end : $rootScope.reportdates.end,
limit: $scope.view[i].limit,
status: $scope.view[i].status,
campaignid: $scope.view[i].campaignid,
filter: $scope.view[i].filter,
campaign: $scope.view[i].campaign,
adgroup: $scope.view[i].adgroup,
network: $scope.view[i].network,
zeroimpressions: $scope.view[i].zeroimpressions,
}
}).then(function successCallback(res) {
if ($scope.view[i].callback) {
$scope.view[i].callback(res.data);
}
if (res.data.error) {
$scope.fnModal('api/modal/global.norights');
} else if (res.data.action == 'connect') {
$scope.fnModal('api/modal/' + $scope.cfg.urlbase + '.connect');
$http.get('/rest/api-core/apigrants.get', {
params: {
websiteid: h.siteId(),
'type': $scope.cfg.urlbase,
}
}).then(function successCallback(res) {
if (res.data.data !== null) {
$scope.apigrants = res.data.data;
}
});
if (res.data.sets) {
for (var set in res.data.sets) {
$scope.analytics[set] = res.data.sets[set];
for (var y in $scope.charts) {
if ($scope.charts[y].dataset == set) {
fnMakeChart($scope.charts[y]);
}
}
}
$scope.analytics[i] = { totals: res.data.totals };
} else {
$scope.analytics[i] = res.data;
for (var y in $scope.charts) {
if ($scope.charts[y].dataset == i) {
fnMakeChart($scope.charts[y]);
}
}
}
} else if (res.data.action == 'getcustomer') {
$scope.getCustomer();
} else if (res.data.sets) {
for (var set in res.data.sets) {
$scope.analytics[set] = res.data.sets[set];
for (var y in $scope.charts) {
if ($scope.charts[y].dataset == set) {
fnMakeChart($scope.charts[y]);
}
}
}
$scope.analytics[i] = { totals: res.data.totals };
} else {
$scope.analytics[i] = res.data;
for (var y in $scope.charts) {
if ($scope.charts[y].dataset == i) {
fnMakeChart($scope.charts[y]);
}
}
}
}, function errorCallback() {
$scope.fnModal('api/modal/global.error');
});
}
}
var getReportData = function () {
$scope.fnCloseModal();
$scope.analytics = {};
$scope.pagecharts = {};
_fnFetchData('first');
_fnFetchData('second');
_fnFetchData('third');
_fnFetchData('fourth');
}
var fnMakeChart = function (chart) {
console.log(chart.dataset);
var dataset = $scope.analytics[chart.dataset].data;
var dimension = (chart.dimension) ? chart.dimension : ($scope.cfg.dimensionprefix + $scope.view[chart.dataset].dimension);
var metric = (chart.metric) ? chart.metric : $scope.metric;
$scope.progresschart[chart.chart] = false;
if (chart['type'] == 'line') {
_charts.fnLineChart(chart, dataset, dimension, metric);
} else if (chart['type'] == 'bar') {
_charts.fnBarChart(chart, dataset, dimension, metric);
} else if (chart['type'] == 'pie') {
_charts.fnPieChart(chart, dataset, dimension, metric);
} else if (chart['type'] == 'linemulti') {
_charts.fnLineChartMultiple(chart, dataset, dimension, metric);
} else if (chart['type'] == 'bubble') {
_charts.fnBubbleChart(chart, dataset, dimension, metric);
} else if (chart['type'] == 'progress') {
_charts.fnProgressChart(chart, dataset, dimension, metric);
}
}
$scope.colors = ['#E91E63', '#3F51B5', '#00BCD4', '#8BC34A', '#FF9800', '#795548', '#607D8B', '#00CC00', '#0000CC', '#DDDDDD', '#999999', '#333333', '#990000']
var _charts = {
fnBubbleChart: function (chart, dataset, dimension, metric) {
return false;
},
fnProgressChart: function (chart, dataset, dimension, metric) {
$scope.pagecharts[chart.chart] = false
var b = $filter('orderBy')(dataset, metric, $scope.order.reverse).slice(0, 5);
$scope.progresschart[chart.chart] = b;
},
fnLineChart: function (chart, dataset, dimension, metric) {
$scope.gloMetrics[metric] = $scope.gloMetrics[metric] || {};
var datamerge = $scope.gloMetrics[metric].datemerge;
var datesubstr = ($scope.cfg.dateformat == 'YYYYMMDD') ? [0, 4, 4, 6, 6, 8] : [0, 4, 5, 7, 8, 10];
var data = {};
// if (dataset.length > 62 && datamerge) {
// for (var i in dataset) {
// // todo date format YYYY-MM-DD vs YYYYMMDD
// var year = dataset[i][dimension].toString().substring(datesubstr[0], datesubstr[1]);
// var month = dataset[i][dimension].toString().substring(datesubstr[2], datesubstr[3]);
// var day = dataset[i][dimension].toString().substring(datesubstr[4], datesubstr[5]);
// var date = new Date(year, month - 1, day);
// onejan = new Date(date.getFullYear(), 0, 1);
// week = Math.ceil((((date - onejan) / 86400000) + onejan.getDay() + 1) / 7);
// var yw = year + '_' + week;
// if (data[yw]) {
// data[yw][metric] += dataset[i][metric];
// data[yw]['n'] += 1;
// } else {
// data[yw] = dataset[i];
// data[yw]['n'] = 1;
// }
// }
// // todo calc average
// dataset = $filter('orderBy')(Object.values(data), 'dimension', false);
// if (datamerge == 'avg') {
// dataset = dataset.map(function (c) {
// c[metric] = c[metric] / c['n'];
// return c;
// })
// }
// }
// A new function to merge data into 31-day buckets, starting from the last entry.
if (dataset.length > 62 && datamerge) { // Condition changed to 31 for one bucket period.
// Assuming the dataset is sorted by date, with the most recent date at the end.
// If not sorted, you would need to find the maximum date first.
const lastEntry = dataset[dataset.length - 1];
const endDateYear = lastEntry[dimension].toString().substring(datesubstr[0], datesubstr[1]);
const endDateMonth = lastEntry[dimension].toString().substring(datesubstr[2], datesubstr[3]);
const endDateDay = lastEntry[dimension].toString().substring(datesubstr[4], datesubstr[5]);
// Set the anchor date to the end of the day of the last entry.
const anchorDate = new Date(endDateYear, endDateMonth - 1, endDateDay);
anchorDate.setHours(23, 59, 59, 999); // Use end of day to ensure the last day is included.
const data = {};
const MS_PER_DAY = 86400000; // Milliseconds in one day (24 * 60 * 60 * 1000)
let period = 7;
// Loop backwards from the end of the dataset.
for (let i = dataset.length - 1; i >= 0; i--) {
const item = dataset[i];
// Parse the date for the current item.
const year = item[dimension].toString().substring(datesubstr[0], datesubstr[1]);
const month = item[dimension].toString().substring(datesubstr[2], datesubstr[3]);
const day = item[dimension].toString().substring(datesubstr[4], datesubstr[5]);
const currentDate = new Date(year, month - 1, day);
// Calculate how many days ago this date was from our anchor date.
const daysDifference = (anchorDate.getTime() - currentDate.getTime()) / MS_PER_DAY;
// Determine the bucket index. Each bucket is a 31-day period.
// Math.floor(0 / 31) = 0, Math.floor(30.9 / 31) = 0, Math.floor(31 / 31) = 1
const bucketIndex = Math.floor(daysDifference / period);
// Create a representative date for the bucket's start day for a consistent key.
const bucketStartDate = new Date(anchorDate);
bucketStartDate.setDate(anchorDate.getDate() - (bucketIndex + 1) * period + 1);
// Format the bucket key to match the API's date format.
const bYear = bucketStartDate.getFullYear();
const bMonth = ('0' + (bucketStartDate.getMonth() + 1)).slice(-2);
const bDay = ('0' + bucketStartDate.getDate()).slice(-2);
const bucketKey = ($scope.cfg.dateformat === 'YYYYMMDD')
? '' + bYear + bMonth + bDay
: bYear + '-' + bMonth + '-' + bDay;
// Aggregate data into the bucket.
if (data[bucketKey]) {
data[bucketKey][metric] += item[metric];
data[bucketKey]['n'] += 1;
} else {
// Create a new entry for the bucket.
data[bucketKey] = { ...item }; // Copy item to avoid modifying original
data[bucketKey][dimension] = bucketKey; // Set the dimension to our bucket's start date
data[bucketKey]['n'] = 1;
}
}
// Convert the aggregated data object back to an array and sort it.
dataset = $filter('orderBy')(Object.values(data), dimension, false);
// If 'avg' is specified, calculate the average for each bucket.
if (datamerge === 'avg') {
dataset = dataset.map(function (c) {
c[metric] = c[metric] / c['n'];
return c;
});
}
}
$scope.pagecharts[chart.chart] = {
data: dataset,
type: 'serial',
theme: 'light',
marginLeft: 0,
marginRight: 0,
marginTop: 10,
fontFamily: 'Roboto',
categoryField: dimension,
rotate: false,
pathToImages: 'vendor/amchart/images/',
dataDateFormat: $scope.cfg.dateformat,
categoryAxis: {
gridPosition: "start",
parseDates: true,
gridThickness: 0
},
valueAxes: [{
position: "left",
axisAlpha: 0,
labelsEnabled: !chart.hideAxisLabels,
dashLength: 4,
color: '#dedede',
}],
graphs: [{
balloonText: $filter('translate')('metrics.' + metric) + " [[value]] ",
"bullet": "round",
"bulletBorderAlpha": 1,
"bulletColor": "#FFFFFF",
"useLineColorForBulletBorder": true,
"fillAlphas": 0.2,
"lineThickness": 2,
"lineAlpha": 1,
"bulletSize": 7,
"connect": true,
"type": "smoothedLine",
"title": "Expenses",
"valueField": metric,
lineColor: $scope.colors[colorindex],
}],
}
colorindex += 1;
},
fnLineChartMultiple: function (chart, dataset, dimension, metric) {
var d = [];
var dimension2 = (chart.dimension2) ? chart.dimension2 : $scope.cfg.dimensionprefix + $scope.view[chart.dataset].dimension2;
var datamerge = ($scope.gloMetrics[metric] || {}).datemerge;
if (dataset && dataset.length > 62 && datamerge) {
var datesubstr = ($scope.cfg.dateformat == 'YYYYMMDD') ? [0, 4, 4, 6, 6, 8] : [0, 4, 5, 7, 8, 10];
var lastEntry = dataset[dataset.length - 1];
if (lastEntry) {
var lastYear = lastEntry[dimension].toString().substring(datesubstr[0], datesubstr[1]);
var lastMonth = lastEntry[dimension].toString().substring(datesubstr[2], datesubstr[3]);
var lastDay = lastEntry[dimension].toString().substring(datesubstr[4], datesubstr[5]);
var anchorDate = new Date(lastYear, lastMonth - 1, lastDay);
anchorDate.setHours(23, 59, 59, 999);
var MS_PER_DAY = 86400000;
var period = 7;
var bucketMap = {};
var formatBucketKey = function (dateObj) {
var year = dateObj.getFullYear();
var month = ('0' + (dateObj.getMonth() + 1)).slice(-2);
var day = ('0' + dateObj.getDate()).slice(-2);
if ($scope.cfg.dateformat === 'YYYYMMDD') {
return '' + year + month + day;
}
return year + '-' + month + '-' + day;
};
for (var idx = dataset.length - 1; idx >= 0; idx--) {
var row = dataset[idx];
var keyYear = row[dimension].toString().substring(datesubstr[0], datesubstr[1]);
var keyMonth = row[dimension].toString().substring(datesubstr[2], datesubstr[3]);
var keyDay = row[dimension].toString().substring(datesubstr[4], datesubstr[5]);
var currentDate = new Date(keyYear, keyMonth - 1, keyDay);
var daysDifference = (anchorDate.getTime() - currentDate.getTime()) / MS_PER_DAY;
var bucketIndex = Math.floor(daysDifference / period);
var bucketStart = new Date(anchorDate);
bucketStart.setDate(anchorDate.getDate() - (bucketIndex + 1) * period + 1);
var bucketKey = formatBucketKey(bucketStart);
var segment = row[dimension2];
if (!segment) {
continue;
}
bucketMap[bucketKey] = bucketMap[bucketKey] || {};
bucketMap[bucketKey][segment] = bucketMap[bucketKey][segment] || { sum: 0, count: 0 };
bucketMap[bucketKey][segment].sum += parseFloat(row[metric]) || 0;
bucketMap[bucketKey][segment].count += 1;
}
var mergedDataset = [];
var bucketKeys = Object.keys(bucketMap).sort();
for (var kIndex = 0; kIndex < bucketKeys.length; kIndex++) {
var bucketKey = bucketKeys[kIndex];
var segments = bucketMap[bucketKey];
for (var segmentName in segments) {
if (!segments.hasOwnProperty(segmentName)) {
continue;
}
var entry = segments[segmentName];
var value = (datamerge === 'avg' && entry.count > 0) ? entry.sum / entry.count : entry.sum;
if (!isFinite(value)) {
value = 0;
}
var mergedRow = {};
mergedRow[dimension] = bucketKey;
mergedRow[dimension2] = segmentName;
mergedRow[metric] = value;
mergedDataset.push(mergedRow);
}
}
dataset = mergedDataset;
}
}
if (!chart.graphs || !chart.graphs.length) {
var totalsByDimension2 = {};
dataset.forEach(function (row) {
var groupKey = row[dimension2];
if (!groupKey) {
return;
}
var value = parseFloat(row[metric]);
if (isNaN(value)) {
return;
}
totalsByDimension2[groupKey] = (totalsByDimension2[groupKey] || 0) + value;
});
chart.graphs = Object.keys(totalsByDimension2)
.map(function (key) {
return { name: key, total: totalsByDimension2[key] };
})
.sort(function (a, b) { return b.total - a.total; })
.slice(0, 5)
.map(function (item) { return item.name; });
}
for (var i in dataset) {
var row = dataset[i];
d[row[dimension]] = d[row[dimension]] || {};
d[row[dimension]].c = row[dimension];
if (chart.graphs) {
for (var y in chart.graphs) {
if (chart.graphs[y] == row[dimension2]) {
d[row[dimension]]['t' + chart.graphs[y]] = row[metric];
}
}
}
}
console.log('chart', chart);
var b = [];
Object.keys(d).sort().forEach(function (key) {
b.push(d[key]);
});
var graphs = [];
for (var y in chart.graphs) {
// *** FIX: Changed 'var' to 'let' to correctly scope the variable for the closure ***
let segmentName = chart.graphs[y];
graphs.push({
balloonText: chart.graphs[y] + ' ' + $filter('translate')('metrics.' + metric) + " [[value]] ",
"bullet": "round",
"bulletBorderAlpha": 1,
"bulletColor": "#FFFFFF",
"useLineColorForBulletBorder": true,
"fillAlphas": 0,
"lineThickness": 2,
"lineAlpha": 1,
"bulletSize": 7,
"title": "Expenses",
"valueField": 't' + chart.graphs[y],
lineColor: $scope.colors[y],
stacked: false,
balloonFunction: function (item) {
var dateLabel = $filter('date')(item.category, 'yyyy-MM-dd');
var rawValue = item.values.value;
var formatted = $filter('useFilter')(
rawValue,
$scope.gloMetrics[metric].typefilter,
$scope.gloMetrics[metric].filtercomparator
);
var suffix = $scope.gloMetrics[metric].after || '';
return (
'' +
'
' + segmentName + '
' + // This will now use the correct segmentName
'
' + dateLabel + '
' +
'
' +
$filter('translate')('metrics.' + metric) +
': ' + formatted + suffix + ' ' +
'
' +
'
'
);
}
});
if (chart.graphs[y] == 'male') {
graphs[y].lineColor = '#3F51B5';
}
if (chart.graphs[y] == 'female') {
graphs[y].lineColor = '#E91E63';
}
}
$scope.pagecharts[chart.chart] = {
data: b,
type: 'serial',
theme: 'light',
marginLeft: 0,
marginRight: 0,
marginTop: 10,
fontFamily: 'Roboto',
categoryField: 'c',
rotate: false,
pathToImages: 'vendor/amchart/images/',
dataDateFormat: $scope.cfg.dateformat,
legend: {
enabled: false
},
categoryAxis: {
gridPosition: "start",
parseDates: true,
labelRotation: 45
},
valueAxes: [{
position: "left",
axisAlpha: 0,
labelsEnabled: true,
}],
graphs: graphs,
};
if (chart.stacked) {
$scope.pagecharts[chart.chart].valueAxes[0].stackType = "regular";
}
},
fnBarChart: function (chart, dataset, dimension, metric) {
//inject color attribute with value
if (typeof $scope.analytics[chart.dataset].data != undefined) {
for (var i = 0; i < $scope.analytics[chart.dataset].data.length; i++) { $scope.analytics[chart.dataset].data[i].color = $scope.colors[i]; }
}
$scope.pagecharts[chart.chart] = {
data: dataset.slice(0, 6),
type: 'serial',
theme: 'light',
marginLeft: 0,
marginRight: 0,
marginTop: 10,
fontFamily: 'Roboto',
categoryField: dimension,
rotate: false,
pathToImages: 'vendor/amchart/images/',
dataDateFormat: $scope.cfg.dateformat,
legend: {
enabled: false
},
categoryAxis: {
gridPosition: "start",
parseDates: false,
labelRotation: 0,
gridThickness: 0,
labelFunction: function (value, valueText, axis) { return (value.length > 19) ? value.substr(0, 17) + '...' : value },
},
valueAxes: [{
position: "left",
axisAlpha: 0,
labelsEnabled: true,
dashLength: 4,
color: '#dedede',
}],
graphs: [{
fillColorsField: "color",
balloonText: "[[category]]: [[value]] ",
type: "column",
columnWidth: 0.40,
title: "Zoekwoord waarde",
valueField: metric,
fillAlphas: 1,
lineColor: '#009688',
autoColor: true,
}],
}
},
fnPieChart: function (chart, dataset, dimension, metric) {
$scope.pagecharts[chart.chart] = {
data: dataset.slice(0, 9),
type: 'pie',
theme: 'light',
marginLeft: 0,
marginRight: 0,
marginTop: 10,
fontFamily: 'Roboto',
categoryField: dimension,
pathToImages: 'vendor/amchart/images/',
"precision": 0,
"decimalSeparator": ",",
"thousandsSeparator": ".",
legend: {
enabled: true,
position: 'right',
autoMargins: false,
marginRight: 100,
},
titleField: dimension,
valueField: metric,
labelRadius: 5,
radius: '42%',
innerRadius: '60%',
labelText: '[[title]]',
}
}
}
$scope.fnAddChart = function (name, functionname) {
_charts[name] = functionname;
}
// Grafieken voor een metric. Heeft de view er geen eigen grafiek voor (veel views noemen alleen een paar
// metrics in view.charts), dan dezelfde grafieken als de standaardmetric van de view, met de naam van deze
// metric; een taart wordt een staaf bij een gemiddelde of percentage.
var fnChartsFor = function (metric) {
if (!$scope.view || !$scope.view.charts) {
return {};
}
if ($scope.view.charts[metric]) {
return $scope.view.charts[metric];
}
var base = $scope.view.charts[$scope.view.selectedmetric] || $scope.view.charts[Object.keys($scope.view.charts)[0]];
if (!Array.isArray(base)) {
return {};
}
var label = $filter('translate')('metrics.' + metric);
if (label === 'metrics.' + metric) {
label = (($scope.gloMetrics || {})[metric] || {}).name || metric;
}
var avg = ((($scope.gloMetrics || {})[metric]) || {}).datemerge === 'avg';
$scope.view.charts[metric] = base.map(function (chart) {
var copy = angular.extend({}, chart, {
name: chart.name === '_' ? '_' : label,
type: (chart.type === 'pie' && avg) ? 'bar' : chart.type
});
delete copy.metric;
return copy;
});
return $scope.view.charts[metric];
}
$scope.fnSwitchMetric = function (selectedmetric) {
$scope.metric = selectedmetric;
$scope.charts = fnChartsFor($scope.metric);
if ($scope.view && $scope.view.fullchart && Array.isArray($scope.charts)) {
var hasActive = false;
for (var idx = 0; idx < $scope.charts.length; idx++) {
if ($scope.charts[idx].active) {
hasActive = true;
break;
}
}
if (!hasActive && $scope.charts.length) {
$scope.charts[0].active = true;
}
}
var reverse = ($scope.gloMetrics[selectedmetric].reverse === true || $scope.gloMetrics[selectedmetric].reverse === false) ? $scope.gloMetrics[selectedmetric].reverse : true;
$scope.order = { order: selectedmetric, 'reverse': reverse };
for (var i in $scope.charts) {
fnMakeChart($scope.charts[i]);
}
}
$scope.fnActivateChart = function (chart) {
if (!$scope.charts || !chart) {
return;
}
angular.forEach($scope.charts, function (item) {
item.active = false;
});
chart.active = true;
fnMakeChart(chart);
}
$scope.fnConnect = function () {
window.location.replace($scope.cfg.authurl + '&h=/' + $scope.cfg.urlbase + '/' + $scope.urlreport + '/' + $scope.urlview);
}
$scope.getReportData = function () {
getReportData();
}
$scope.fnMakeChart = function (chart) {
fnMakeChart(chart);
}
$scope.fnPctChange = function (o, n) {
if (n == 0) {
return '0% ';
}
var pct = ((n - o) / o) * 100;
var npct = $filter('number')(pct, 2);
if (pct < 0) {
return '' + npct + '% ';
}
return '+ ' + npct + '% ';
}
$scope.fnSaveSelectedMetrics = function () {
$http.post('/rest/api-core/selectedmetrics.set', {
websiteid: h.siteId(),
'type': $scope.cfg.urlbase,
'view': $scope.urlreport + '-' + $scope.urlview,
'selectedmetrics': $scope.selectedmetrics,
});
}
$scope.fnRemoveViewfilter = function (type) {
delete $scope.viewfilters[type];
getReportData();
}
$scope.init = function () {
if ($scope.custominit) {
$scope.urlreport = $scope.custominit.urlreport;
$scope.urlview = $scope.custominit.urlview;
}
/* set selection of view metric and chart*/
$scope.views = $scope.reportconfig[$scope.urlreport].views;
$scope.view = $scope.reportconfig[$scope.urlreport].views[$scope.urlview];
/* selected wmetrics ophalen ...... aparte functie ... ?*/
$scope.selectedmetrics = $scope.view.selectedmetrics;
$http.get('/rest/api-core/selectedmetrics.get', {
params: {
websiteid: h.siteId(),
'type': $scope.cfg.urlbase,
'view': $scope.urlreport + '-' + $scope.urlview,
}
}).then(function successCallback(res) {
if (res.data.data !== null) {
$scope.selectedmetrics = res.data.data;
}
});
$scope.metrics = $scope.view.metrics;
$scope.metric = $scope.view.selectedmetric;
$scope.order = { order: $scope.view.selectedmetric, reverse: true };
$scope.charts = fnChartsFor($scope.metric);
getReportData();
}
var l = {
"ab": "Abkhaz",
"aa": "Afar",
"af": "Afrikaans",
"ak": "Akan",
"sq": "Albanian",
"am": "Amharic",
"ar": "Arabic",
"an": "Aragonese",
"hy": "Armenian",
"as": "Assamese",
"av": "Avaric",
"ae": "Avestan",
"ay": "Aymara",
"az": "Azerbaijani",
"bm": "Bambara",
"ba": "Bashkir",
"eu": "Basque",
"be": "Belarusian",
"bn": "Bengali; Bangla",
"bh": "Bihari",
"bi": "Bislama",
"bs": "Bosnian",
"br": "Breton",
"bg": "Bulgarian",
"my": "Burmese",
"ca": "Catalan; Valencian",
"ch": "Chamorro",
"ce": "Chechen",
"ny": "Chichewa; Chewa; Nyanja",
"zh": "Chinese",
"cv": "Chuvash",
"kw": "Cornish",
"co": "Corsican",
"cr": "Cree",
"hr": "Croatian",
"cs": "Czech",
"da": "Danish",
"dv": "Divehi; Dhivehi; Maldivian;",
"nl": "Nederlands",
"dz": "Dzongkha",
"en": "English",
"eo": "Esperanto",
"et": "Estonian",
"ee": "Ewe",
"fo": "Faroese",
"fj": "Fijian",
"fi": "Finnish",
"fr": "French",
"ff": "Fula; Fulah; Pulaar; Pular",
"gl": "Galician",
"ka": "Georgian",
"de": "German",
"el": "Greek, Modern",
"gn": "GuaranÃ",
"gu": "Gujarati",
"ht": "Haitian; Haitian Creole",
"ha": "Hausa",
"he": "Hebrew (modern)",
"hz": "Herero",
"hi": "Hindi",
"ho": "Hiri Motu",
"hu": "Hungarian",
"ia": "Interlingua",
"id": "Indonesian",
"ie": "Interlingue",
"ga": "Irish",
"ig": "Igbo",
"ik": "Inupiaq",
"io": "Ido",
"is": "Icelandic",
"it": "Italian",
"iu": "Inuktitut",
"ja": "Japanese",
"jv": "Javanese",
"kl": "Kalaallisut, Greenlandic",
"kn": "Kannada",
"kr": "Kanuri",
"ks": "Kashmiri",
"kk": "Kazakh",
"km": "Khmer",
"ki": "Kikuyu, Gikuyu",
"rw": "Kinyarwanda",
"ky": "Kyrgyz",
"kv": "Komi",
"kg": "Kongo",
"ko": "Korean",
"ku": "Kurdish",
"kj": "Kwanyama, Kuanyama",
"la": "Latin",
"lb": "Luxembourgish, Letzeburgesch",
"lg": "Ganda",
"li": "Limburgish, Limburgan, Limburger",
"ln": "Lingala",
"lo": "Lao",
"lt": "Lithuanian",
"lu": "Luba-Katanga",
"lv": "Latvian",
"gv": "Manx",
"mk": "Macedonian",
"mg": "Malagasy",
"ms": "Malay",
"ml": "Malayalam",
"mt": "Maltese",
"mi": "MÄori",
"mr": "Marathi (MarÄá¹hÄ«)",
"mh": "Marshallese",
"mn": "Mongolian",
"na": "Nauru",
"nv": "Navajo, Navaho",
"nb": "Norwegian Bokmål",
"nd": "North Ndebele",
"ne": "Nepali",
"ng": "Ndonga",
"nn": "Norwegian Nynorsk",
"no": "Norwegian",
"ii": "Nuosu",
"nr": "South Ndebele",
"oc": "Occitan",
"oj": "Ojibwe, Ojibwa",
"cu": "Old Church Slavonic, Church Slavic, Church Slavonic, Old Bulgarian, Old Slavonic",
"om": "Oromo",
"or": "Oriya",
"os": "Ossetian, Ossetic",
"pa": "Panjabi, Punjabi",
"pi": "PÄli",
"fa": "Persian (Farsi)",
"pl": "Polish",
"ps": "Pashto, Pushto",
"pt": "Portuguese",
"qu": "Quechua",
"rm": "Romansh",
"rn": "Kirundi",
"ro": "Romanian",
"ru": "Russian",
"sa": "Sanskrit (Saá¹ská¹›ta)",
"sc": "Sardinian",
"sd": "Sindhi",
"se": "Northern Sami",
"sm": "Samoan",
"sg": "Sango",
"sr": "Serbian",
"gd": "Scottish Gaelic; Gaelic",
"sn": "Shona",
"si": "Sinhala, Sinhalese",
"sk": "Slovak",
"sl": "Slovene",
"so": "Somali",
"st": "Southern Sotho",
"az": "South Azerbaijani",
"es": "Spanish; Castilian",
"su": "Sundanese",
"sw": "Swahili",
"ss": "Swati",
"sv": "Swedish",
"ta": "Tamil",
"te": "Telugu",
"tg": "Tajik",
"th": "Thai",
"ti": "Tigrinya",
"bo": "Tibetan Standard, Tibetan, Central",
"tk": "Turkmen",
"tl": "Tagalog",
"tn": "Tswana",
"to": "Tonga (Tonga Islands)",
"tr": "Turkish",
"ts": "Tsonga",
"tt": "Tatar",
"tw": "Twi",
"ty": "Tahitian",
"ug": "Uyghur, Uighur",
"uk": "Ukrainian",
"ur": "Urdu",
"uz": "Uzbek",
"ve": "Venda",
"vi": "Vietnamese",
"vo": "Volapük",
"wa": "Walloon",
"cy": "Welsh",
"wo": "Wolof",
"fy": "Western Frisian",
"xh": "Xhosa",
"yi": "Yiddish",
"yo": "Yoruba",
"za": "Zhuang, Chuang",
"zu": "Zulu"
};
var c = {
"AF": "Afghanistan",
"AX": "Åland",
"AL": "Albanië",
"DZ": "Algerije",
"VI": "Amerikaanse Maagdeneilanden",
"AS": "Amerikaans-Samoa",
"AD": "Andorra",
"AO": "Angola",
"AI": "Anguilla",
"AQ": "Antarctica",
"AG": "Antigua en Barbuda",
"AR": "Argentinië",
"AM": "Armenië",
"AW": "Aruba",
"AU": "Australië",
"AZ": "Azerbeidzjan",
"BS": "Bahama's",
"BH": "Bahrein",
"BD": "Bangladesh",
"BB": "Barbados",
"BE": "België",
"BZ": "Belize",
"BJ": "Benin",
"BM": "Bermuda",
"BT": "Bhutan",
"BO": "Bolivia",
"BQ": "Bonaire, Sint Eustatius en Saba",
"BA": "Bosnië en Herzegovina",
"BW": "Botswana",
"BV": "Bouvet",
"BR": "Brazilië",
"VG": "Britse Maagdeneilanden",
"IO": "Brits Indische Oceaanterritorium",
"BN": "Brunei",
"BG": "Bulgarije",
"BF": "Burkina Faso",
"BI": "Burundi",
"KH": "Cambodja",
"CA": "Canada",
"CF": "Centraal-Afrikaanse Republiek",
"CL": "Chili",
"CN": "China",
"CX": "Christmaseiland",
"CC": "Cocoseilanden",
"CO": "Colombia",
"KM": "Comoren",
"CG": "Congo-Brazzaville",
"CD": "Congo-Kinshasa",
"CK": "Cookeilanden",
"CR": "Costa Rica",
"CU": "Cuba",
"CW": "Curaçao",
"CY": "Cyprus",
"DK": "Denemarken",
"DJ": "Djibouti",
"DM": "Dominica",
"DO": "Dominicaanse Republiek",
"DE": "Duitsland",
"EC": "Ecuador",
"EG": "Egypte",
"SV": "El Salvador",
"GQ": "Equatoriaal-Guinea",
"ER": "Eritrea",
"EE": "Estland",
"ET": "Ethiopië",
"FO": "Faeröer",
"FK": "Falklandeilanden",
"FJ": "Fiji",
"PH": "Filipijnen",
"FI": "Finland",
"FR": "Frankrijk",
"TF": "Franse Zuidelijke en Antarctische Gebiede",
"GF": "Frans-Guyana",
"PF": "Frans-Polynesië",
"GA": "Gabon",
"GM": "Gambia",
"GE": "Georgië",
"GH": "Ghana",
"GI": "Gibraltar",
"GD": "Grenada",
"GR": "Griekenland",
"GL": "Groenland",
"GP": "Guadeloupe",
"GU": "Guam",
"GT": "Guatemala",
"GG": "Guernsey",
"GN": "Guinee",
"GW": "Guinee-Bissau",
"GY": "Guyana",
"HT": "Haïti",
"HM": "Heard en McDonaldeilanden",
"HN": "Honduras",
"HU": "Hongarije",
"HK": "Hongkong",
"IE": "Ierland",
"IS": "IJsland",
"IN": "India",
"ID": "Indonesië",
"IQ": "Irak",
"IR": "Iran",
"IL": "Israël",
"IT": "Italië",
"CI": "Ivoorkust",
"JM": "Jamaica",
"JP": "Japan",
"YE": "Jemen",
"JE": "Jersey",
"JO": "Jordanië",
"KY": "Kaaimaneilanden",
"CV": "Kaapverdië",
"CM": "Kameroen",
"KZ": "Kazachstan",
"KE": "Kenia",
"KG": "Kirgizië",
"KI": "Kiribati",
"UM": "Kleine afgelegen eilanden van de Verenigde Staten",
"KW": "Koeweit",
"XK": "Kosovo",
"HR": "Kroatië",
"LA": "Laos",
"LS": "Lesotho",
"LV": "Letland",
"LB": "Libanon",
"LR": "Liberia",
"LY": "Libië",
"LI": "Liechtenstein",
"LT": "Litouwen",
"LU": "Luxemburg",
"MO": "Macau",
"MK": "Macedonië",
"MG": "Madagaskar",
"MW": "Malawi",
"MV": "Maldiven",
"MY": "Maleisië",
"ML": "Mali",
"MT": "Malta",
"IM": "Man",
"MA": "Marokko",
"MH": "Marshalleilanden",
"MQ": "Martinique",
"MR": "Mauritanië",
"MU": "Mauritius",
"YT": "Mayotte",
"MX": "Mexico",
"FM": "Micronesia",
"MD": "Moldavië",
"MC": "Monaco",
"MN": "Mongolië",
"ME": "Montenegro",
"MS": "Montserrat",
"MZ": "Mozambique",
"MM": "Myanmar",
"NA": "Namibië",
"NR": "Nauru",
"NL": "Nederland",
"NP": "Nepal",
"NI": "Nicaragua",
"NC": "Nieuw-Caledonië",
"NZ": "Nieuw-Zeeland",
"NE": "Niger",
"NG": "Nigeria",
"NU": "Niue",
"MP": "Noordelijke Marianen",
"KP": "Noord-Korea",
"NO": "Noorwegen",
"NF": "Norfolk",
"UG": "Oeganda",
"UA": "Oekraïne",
"UZ": "Oezbekistan",
"OM": "Oman",
"AT": "Oostenrijk",
"TL": "Oost-Timor",
"PK": "Pakistan",
"PW": "Palau",
"PS": "Palestina",
"PA": "Panama",
"PG": "Papua-Nieuw-Guinea",
"PY": "Paraguay",
"PE": "Peru",
"PN": "Pitcairneilanden",
"PL": "Polen",
"PT": "Portugal",
"PR": "Puerto Rico",
"QA": "Qatar",
"RE": "Réunion",
"RO": "Roemenië",
"RU": "Rusland",
"RW": "Rwanda",
"BL": "Saint-Barthélemy",
"KN": "Saint Kitts en Nevis",
"LC": "Saint Lucia",
"PM": "Saint-Pierre en Miquelon",
"VC": "Saint Vincent en de Grenadines",
"SB": "Salomonseilanden",
"WS": "Samoa",
"SM": "San Marino",
"SA": "Saoedi-Arabië",
"ST": "Sao Tomé en Principe",
"SN": "Senegal",
"RS": "Servië",
"SC": "Seychellen",
"SL": "Sierra Leone",
"SG": "Singapore",
"SH": "Sint-Helena, Ascension en Tristan da Cunha",
"MF": "Sint-Maarten",
"SX": "Sint Maarten",
"SI": "Slovenië",
"SK": "Slowakije",
"SD": "Soedan",
"SO": "Somalië",
"ES": "Spanje",
"SJ": "Spitsbergen en Jan Mayen",
"LK": "Sri Lanka",
"SR": "Suriname",
"SZ": "Swaziland",
"SY": "Syrië",
"TJ": "Tadzjikistan",
"TW": "Taiwan",
"TZ": "Tanzania",
"TH": "Thailand",
"TG": "Togo",
"TK": "Tokelau",
"TO": "Tonga",
"TT": "Trinidad en Tobago",
"TD": "Tsjaad",
"CZ": "Tsjechië",
"TN": "Tunesië",
"TR": "Turkije",
"TM": "Turkmenistan",
"TC": "Turks- en Caicoseilanden",
"TV": "Tuvalu",
"UY": "Uruguay",
"VU": "Vanuatu",
"VA": "Vaticaanstad",
"VE": "Venezuela",
"AE": "Verenigde Arabische Emiraten",
"US": "Verenigde Staten",
"GB": "Verenigd Koninkrijk",
"VN": "Vietnam",
"WF": "Wallis en Futuna",
"EH": "Westelijke Sahara",
"BY": "Wit-Rusland",
"ZM": "Zambia",
"ZW": "Zimbabwe",
"ZA": "Zuid-Afrika",
"GS": "Zuid-Georgia en de Zuidelijke Sandwicheilanden",
"KR": "Zuid-Korea",
"SS": "Zuid-Soedan",
"SE": "Zweden",
"CH": "Zwitserland",
};
$scope.fnLC = function (a) {
var b = a.split("_");
return l[b[0]];
}
$scope.fnC = function (a) {
return c[a];
}
}
]);
app.controller('analytics', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.pageowl.app/rest/analytics/connect?websiteid=' + h.siteId(),
dateformat: "YYYYMMDD",
dimensionprefix: 'ga:',
urlbase: 'analytics',
switchclient: true,
report_endpoint: '/rest/analytics/v4',
}
$scope.gloMetrics = h.metrics('analytics');
/* $scope.gloMetrics = {
sessions: { name: 'Sessions', typefilter: 'number', filtercomparator: 0 ,'datemerge':'sum'},
users: { 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'},
avgSessionDuration: { 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'},
};*/
$scope.selectedmetrics = {};
$scope.reportconfig = {
channels: {
views: {
all: {
name: 'All',
view: 'all',
viewfilters: ['goals'],
first: { dimension: 'date' },
second: { dimension: 'channelGrouping', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / channel', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Users / channel', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'Pagina weergaven', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Pageviews / channel', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Pageviews per session /channel', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
organic: {
name: 'Organic',
view: 'organic',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Organic Search' },
second: { dimension: 'keyword', channel: 'Organic Search', sort: 'users', sortorder: 'DESCENDING' },
third: { dimension: 'source', channel: 'Organic Search', sort: 'users', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
},
},
paid: {
name: 'Betaalde zoekopdrachten',
view: 'paid',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Paid Search' },
second: { dimension: 'keyword', channel: 'Paid Search', sort: 'sessions', sortorder: 'DESCENDING' },
third: { dimension: 'year', channel: 'Paid Search', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
display: {
name: 'Display',
view: 'display',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Display' },
second: { dimension: 'source', channel: 'Display', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
social: {
name: 'Social',
view: 'social',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Social' },
second: { dimension: 'socialNetwork', channel: 'Social', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
referral: {
name: 'Referral',
view: 'referral',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Referral', sort: 'date', sortorder: 'ASCENDING', },
second: { dimension: 'source', channel: 'Referral', sort: 'sessions', sortorder: 'DESCENDING' },
third: { dimension: 'year', channel: 'Referral', sort: 'year', sortorder: 'ASCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
email: {
name: 'E-mail',
view: 'email',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Email' },
second: { dimension: 'source', channel: 'Email', sort: 'sessions', sortorder: 'DESCENDING', },
third: { dimension: 'year', channel: 'Email', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
other: {
name: 'Overig',
view: 'other',
viewfilters: ['goals'],
first: { dimension: 'date', channel: '(Other)' },
second: { dimension: 'source', channel: '(Other)', sort: 'sessions', sortorder: 'DESCENDING', },
third: { dimension: 'year', channel: '(Other)', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
},
},
audience: {
views: {
local: {
name: 'Lokaal',
view: 'local',
viewfilters: ['goals'],
first: { dimension: 'country', sort: 'sessions', sortorder: 'DESCENDING' },
second: { dimension: 'country', sort: 'sessions', sortorder: 'DESCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'first' },
panels: { dataset: 'second' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
},
},
age: {
name: 'Leeftijd',
view: 'age',
viewfilters: ['goals'],
first: { dimension: 'date', dimension2: 'userAgeBracket' },
second: { dimension: 'userAgeBracket', sort: 'userAgeBracket', sortorder: 'ASCENDING', },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
gender: {
name: 'Geslacht',
view: 'gender',
viewfilters: ['goals'],
first: { dimension: 'date', dimension2: 'userGender', sort: 'userGender', sortorder: 'ASCENDING', },
second: { dimension: 'userGender', sort: 'userGender', sortorder: 'ASCENDING', },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'linemulti', dataset: 'first', graphs: ['male', 'female'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'linemulti', dataset: 'first', graphs: ['male', 'female'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'linemulti', dataset: 'first', graphs: ['male', 'female'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'linemulti', dataset: 'first', graphs: ['male', 'female'] }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'linemulti', dataset: 'first', graphs: ['male', 'female'] }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'linemulti', dataset: 'first', graphs: ['male', 'female'] }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'linemulti', dataset: 'first', graphs: ['male', 'female'] }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'linemulti', dataset: 'first', graphs: ['male', 'female'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'linemulti', dataset: 'first', graphs: ['male', 'female'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'linemulti', dataset: 'first', graphs: ['male', 'female'] }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
device: {
name: 'Apparaten',
view: 'device',
viewfilters: ['goals'],
first: { dimension: 'date', dimension2: 'deviceCategory' },
second: { dimension: 'deviceCategory', sort: 'deviceCategory', sortorder: 'ASCENDING', },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
mobile: {
name: 'Mobiel',
view: 'mobile',
viewfilters: ['goals'],
first: { dimension: 'mobileDeviceInfo', sort: 'sessions', sortorder: 'DESCENDING' },
second: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'users': true, 'pageviews': true, 'pageviewsPerSession': true, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true },
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll'],
table: { dataset: 'first' },
panels: { dataset: 'second' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'progress', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'progress', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'progress', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'progress', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'progress', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'progress', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'progress', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'progress', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'progress', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'progress', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'] }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
},
}
},
},
conversions: {
views: {
goals: {
name: 'Doelen',
view: 'goals',
first: { dimension: 'date', },
second: { dimension: 'goalCompletionsAll' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'goalCompletionsAll',
selectedmetrics: { 'goalCompletionsAll': true, 'goalValueAll': true, 'goalConversionRateAll': true, 'goalAbandonRateAll': true },
metrics: ['goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll', 'goalAbandonRateAll'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalAbandonRateAll: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
/* DIT KLOPT NIET, HEB NOG GEEN DOELEN OF ECOMMERSE DATA*/
ecommerce: {
name: 'E-commerce',
view: 'ecommerce',
first: { dimension: 'date' },
second: { dimension: 'adMatchedQuery', },
third: { dimension: 'channelGrouping' },
selectedmetric: 'transactions',
selectedmetrics: { 'transactions': true, 'uniquePurchases': true, 'totalValue': true, 'revenuePerTransaction': true, 'itemQuantity': true, 'itemsPerPurchase': true },
metrics: ['transactions', 'uniquePurchases', 'totalValue', 'revenuePerTransaction', 'itemQuantity', 'itemsPerPurchase'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
transactions: [{ 'chart': 'c1', 'name': 'transactions', type: 'line', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
uniquePurchases: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
totalValue: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
revenuePerTransaction: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
itemQuantity: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
itemsPerPurchase: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
},
},
pages: {
views: {
all: {
name: 'Alle pagina\'s',
view: 'all',
viewfilters: ['goals', 'channels'],
first: { dimension: 'date', },
second: { dimension: 'pagePath', sort: 'pageviews', sortorder: 'DESCENDING', },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'pageviews',
selectedmetrics: { 'pageviews': true, 'uniquePageviews': true, 'pageviewsPerSession': true, 'avgTimeOnPage': true, 'entrances': true, 'bounceRate': true, 'exitRate': true, 'pageValue': true },
metrics: ['pageviews', 'uniquePageviews', 'pageviewsPerSession', 'avgTimeOnPage', 'entrances', 'bounceRate', 'exitRate', 'pageValue'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
uniquePageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
avgTimeOnPage: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
entrances: [{ 'chart': 'c1', 'name': 'Ingangen', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
exitRate: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
pageValue: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
},
},
/* NIET HELAAM LEKKER */
landing: {
name: 'Landingspagina\'s',
view: 'landing',
first: { dimension: 'date' },
second: { dimension: 'landingPagePath', sort: 'pageviews', sortorder: 'DESCENDING', },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'pageviews',
selectedmetrics: { 'pageviews': true, 'uniquePageviews': true, 'pageviewsPerSession': true, 'avgTimeOnPage': true, 'entrances': true, 'bounceRate': true, 'exitRate': true, 'pageValue': true },
metrics: ['pageviews', 'uniquePageviews', 'pageviewsPerSession', 'avgTimeOnPage', 'entrances', 'bounceRate', 'exitRate', 'pageValue'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
uniquePageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
avgTimeOnPage: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
entrances: [{ 'chart': 'c1', 'name': 'Ingangen', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
exitRate: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
pageValue: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
},
},
exit: {
name: 'Exit Pagina\'s',
view: 'exit',
first: { dimension: 'date' },
second: { dimension: 'exitPagePath', sort: 'exits', sortorder: 'DESCENDING', },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'exits',
selectedmetrics: { 'exits': true, 'uniquePageviews': true, 'avgTimeOnPage': true, 'exitRate': true },
metrics: ['exits', 'uniquePageviews', 'avgTimeOnPage', 'exitRate'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
exits: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
uniquePageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
avgTimeOnPage: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
exitRate: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'progress', dataset: 'second' }],
},
},
},
},
events: {
views: {
all: {
name: 'Alle pagina\'s',
view: 'all',
first: { dimension: 'date', },
second: { dimension: 'pagePath', sort: 'pageviews', sortorder: 'DESCENDING', },
selectedmetric: 'pageviews',
selectedmetrics: { 'pageviews': true, 'uniquePageviews': true, 'pageviewsPerSession': true, 'avgTimeOnPage': true, 'entrances': true, 'bounceRate': true, 'exitRate': true, 'pageValue': true },
metrics: ['pageviews', 'uniquePageviews', 'pageviewsPerSession', 'avgTimeOnPage', 'entrances', 'bounceRate', 'exitRate', 'pageValue'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
uniquePageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgTimeOnPage: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
entrances: [{ 'chart': 'c1', 'name': 'Ingangen', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
exitRate: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
pageValue: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
/* NIET HELAAM LEKKER */
landing: {
name: 'Landingspagina\'s',
view: 'landing',
first: { dimension: 'date' },
second: { dimension: 'landingPagePath', sort: 'pageviews', sortorder: 'DESCENDING', },
selectedmetric: 'pageviews',
selectedmetrics: { 'pageviews': true, 'uniquePageviews': true, 'pageviewsPerSession': true, 'avgTimeOnPage': true, 'entrances': true, 'bounceRate': true, 'exitRate': true, 'pageValue': true },
metrics: ['pageviews', 'uniquePageviews', 'pageviewsPerSession', 'avgTimeOnPage', 'entrances', 'bounceRate', 'exitRate', 'pageValue'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
uniquePageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgTimeOnPage: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
entrances: [{ 'chart': 'c1', 'name': 'Ingangen', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
exitRate: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
pageValue: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
exit: {
name: 'Exit Pagina\'s',
view: 'exit',
first: { dimension: 'date' },
second: { dimension: 'exitPagePath', sort: 'exits', sortorder: 'DESCENDING', },
selectedmetric: 'exits',
selectedmetrics: { 'exits': true, 'uniquePageviews': true, 'avgTimeOnPage': true, 'exitRate': true },
metrics: ['exits', 'uniquePageviews', 'avgTimeOnPage', 'exitRate'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
exits: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
uniquePageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgTimeOnPage: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
exitRate: [{ 'chart': 'c1', 'name': '_', type: 'line', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
},
}
}
$scope.setAnalyticsCustomer = function(data) {
$http.get('/rest/' + $scope.cfg.urlbase + '/profileid.set', {
params: {
websiteid: h.siteId(),
profileid: data.profileid,
accountId: data.accountId,
webPropertyId: data.webPropertyId,
}
}).then(function successCallback(res) {
$scope.getReportData();
});
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('analytics4', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function ($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.marketingtracer.com/rest/analytics4/connect?websiteid=' + h.siteId(),
dateformat: "YYYYMMDD",
dimensionprefix: '',
urlbase: 'analytics4',
switchclient: true,
report_endpoint: '/rest/analytics4/report',
}
$scope.checkDataAvailability = function (data) {
if(data.data && data.data.length == 0){
$scope.msg = 'Google Signals Must Be Active: Age and gender data are only collected if you have Google Signals enabled for the GA4 property. If it\'s not active, the API will return no data. You must enable this in your GA4 Admin settings (Data Settings > Data Collection).';
}
}
if($rootScope.user.reseller?.config?.authdomain){
$scope.cfg.authurl = 'https://' + $rootScope.user.reseller.config.authdomain + '/rest/analytics4/connect?websiteid=' + h.siteId();
}
//$scope.gloMetrics = h.metrics('analytics');
$scope.gloMetrics = {
sessions: { name: 'Sessions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' },
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' },
totalRevenue: { name: 'Total Revenue', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' },
};
$scope.selectedmetrics = {};
$scope.reportconfig = {
channels: {
views: {
all: {
name: 'All',
view: 'all',
viewfilters: ['goals'],
first: { dimension: 'date' },
second: { dimension: 'sessionDefaultChannelGrouping', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / channel', type: 'pie', dataset: 'second' }],
totalUsers: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Users / channel', type: 'pie', dataset: 'second' }],
screenPageViews: [{ 'chart': 'c1', 'name': 'Pagina weergaven', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'screenPageViews / channel', type: 'pie', dataset: 'second' }],
screenPageViewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'screenPageViewsPerSession per session /channel', type: 'bar', dataset: 'second' }],
averageSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
newUsers: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
eventCount: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
eventCountPerUser: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
},
},
organic: {
name: 'Organic',
view: 'organic',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Organic Search' },
second: { dimension: 'landingPage', channel: 'Organic Search', sort: 'users', sortorder: 'DESCENDING' },
third: { dimension: 'sessionSource', channel: 'Organic Search', sort: 'users', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'third' }],
},
},
// AI-assistenten (ChatGPT, Perplexity, Gemini, Copilot): GA4-kanaal 'AI Assistant'
ai: {
name: 'AI assistants',
view: 'ai',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'AI Assistant' },
second: { dimension: 'landingPage', channel: 'AI Assistant', sort: 'sessions', sortorder: 'DESCENDING' },
third: { dimension: 'sessionSource', channel: 'AI Assistant', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
},
},
// Direct: vaak veel botverkeer, daarom per land
direct: {
name: 'Direct',
view: 'direct',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Direct' },
second: { dimension: 'landingPage', channel: 'Direct', sort: 'sessions', sortorder: 'DESCENDING' },
third: { dimension: 'country', channel: 'Direct', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
},
},
paid: {
name: 'Betaalde zoekopdrachten',
view: 'paid',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Paid Search' },
second: { dimension: 'landingPage', channel: 'Paid Search', sort: 'sessions', sortorder: 'DESCENDING' },
third: { dimension: 'year', channel: 'Paid Search', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
display: {
name: 'Display',
view: 'display',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Display' },
second: { dimension: 'source', channel: 'Display', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true },
metrics: ['sessions', 'totalUsers'],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
social: {
name: 'Social',
view: 'social',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Organic Social,Paid Social' },
second: { dimension: 'sessionSource', channel: 'Organic Social,Paid Social', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
referral: {
name: 'Referral',
view: 'referral',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Referral', sort: 'date', sortorder: 'ASCENDING', },
second: { dimension: 'sessionSource', channel: 'Referral', sort: 'sessions', sortorder: 'DESCENDING' },
third: { dimension: 'year', channel: 'Referral', sort: 'year', sortorder: 'ASCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
email: {
name: 'E-mail',
view: 'email',
viewfilters: ['goals'],
first: { dimension: 'date', channel: 'Email' },
second: { dimension: 'sessionSource', channel: 'Email', sort: 'sessions', sortorder: 'DESCENDING', },
third: { dimension: 'year', channel: 'Email', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
other: {
name: 'Overig',
view: 'other',
viewfilters: ['goals'],
first: { dimension: 'date', channel: '!Direct,Organic Search,AI Assistant,Paid Search,Display,Organic Social,Paid Social,Referral,Email' },
second: { dimension: 'sessionSource', channel: '!Direct,Organic Search,AI Assistant,Paid Search,Display,Organic Social,Paid Social,Referral,Email', sort: 'sessions', sortorder: 'DESCENDING', },
third: { dimension: 'year', channel: '!Direct,Organic Search,AI Assistant,Paid Search,Display,Organic Social,Paid Social,Referral,Email', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
},
},
audience: {
views: {
local: {
name: 'Lokaal',
view: 'local',
viewfilters: ['goals'],
first: { dimension: 'country', sort: 'sessions', sortorder: 'DESCENDING' },
second: { dimension: 'country', sort: 'sessions', sortorder: 'DESCENDING' },
third: { dimension: 'country', sort: 'sessions', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'first' },
panels: { dataset: 'second' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'third' }],
},
},
age: {
name: 'Leeftijd',
view: 'age',
viewfilters: ['goals'],
first: { dimension: 'date', dimension2: 'userAgeBracket',callback:$scope.checkDataAvailability },
second: { dimension: 'userAgeBracket', sort: 'userAgeBracket', sortorder: 'ASCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'linemulti', dataset: 'first', graphs: ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
gender: {
name: 'Geslacht',
view: 'gender',
viewfilters: ['goals'],
first: { dimension: 'date', dimension2: 'userGender' ,callback:$scope.checkDataAvailability},
second: { dimension: 'userGender', sort: 'userGender', sortorder: 'ASCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'linemulti', dataset: 'first', graphs: ['male', 'female', 'unknown'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'linemulti', dataset: 'first', graphs: ['male', 'female', 'unknown'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'linemulti', dataset: 'first', graphs: ['male', 'female', 'unknown'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'linemulti', dataset: 'first', graphs: ['male', 'female', 'unknown'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'linemulti', dataset: 'first', graphs: ['male', 'female', 'unknown'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'linemulti', dataset: 'first', graphs: ['male', 'female', 'unknown'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'linemulti', dataset: 'first', graphs: ['male', 'female', 'unknown'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'linemulti', dataset: 'first', graphs: ['male', 'female', 'unknown'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'linemulti', dataset: 'first', graphs: ['male', 'female', 'unknown'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'linemulti', dataset: 'first', graphs: ['male', 'female', 'unknown'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
device: {
name: 'Apparaten',
view: 'device',
viewfilters: ['goals'],
first: { dimension: 'date', dimension2: 'deviceCategory' },
second: { dimension: 'deviceCategory', sort: 'deviceCategory', sortorder: 'ASCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'], date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'linemulti', dataset: 'first', graphs: ['desktop', 'mobile', 'tablet'], date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
mobile: {
name: 'Mobiel',
view: 'mobile',
viewfilters: ['goals'],
first: { dimension: 'mobileDeviceBranding', sort: 'sessions', sortorder: 'DESCENDING' },
second: { dimension: 'mobileDeviceBranding', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'screenPageViews': true, 'screenPageViewsPerSession': true, 'averageSessionDuration': true, 'newUsers': true, 'bounceRate': true, 'eventCount': true, 'eventCountPerUser': true },
metrics: ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
table: { dataset: 'first' },
panels: { dataset: 'second' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
users: [{ 'chart': 'c1', 'name': 'Users', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
pageviews: [{ 'chart': 'c1', 'name': 'PageViews', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
pageviewsPerSession: [{ 'chart': 'c1', 'name': 'Pageviews per session', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
avgSessionDuration: [{ 'chart': 'c1', 'name': 'Average session duration', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', 'type': 'pie', dataset: 'first' }],
percentNewSessions: [{ 'chart': 'c1', 'name': 'Percentage new Sessions', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
bounceRate: [{ 'chart': 'c1', 'name': 'Bounce ratio', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
goalCompletionsAll: [{ 'chart': 'c1', 'name': 'Goals', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
goalValueAll: [{ 'chart': 'c1', 'name': 'Goalvalue', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
goalConversionRateAll: [{ 'chart': 'c1', 'name': 'Goalconverions ratio', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
},
}
},
},
conversions: {
views: {
goals: {
name: 'Conversies',
view: 'goals',
first: { dimension: 'date' },
second: { dimension: 'eventName', sort: 'conversions', sortorder: 'DESCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'conversions',
selectedmetrics: { 'conversions': true, 'sessionConversionRate': true, 'eventValue': true },
metrics: ['conversions', 'sessionConversionRate', 'eventValue'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
conversions: [{ 'chart': 'c1', 'name': 'Conversies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversies per gebeurtenis', type: 'pie', dataset: 'second' }],
sessionConversionRate: [{ 'chart': 'c1', 'name': 'Sessie conversieratio', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversieratio per gebeurtenis', type: 'bar', dataset: 'second' }],
eventValue: [{ 'chart': 'c1', 'name': 'Gebeurteniswaarde', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Waarde per gebeurtenis', type: 'pie', dataset: 'second' }],
},
},
ecommerce: {
name: 'Productprestaties',
view: 'ecommerce',
first: { dimension: 'date' },
second: { dimension: 'itemName', sort: 'itemsPurchased', sortorder: 'DESCENDING' },
third: { dimension: 'sessionDefaultChannelGrouping', sort: 'itemRevenue', sortorder: 'DESCENDING' },
selectedmetric: 'itemsPurchased',
// Metrics are now item-scoped to be compatible with itemName
selectedmetrics: { 'itemsPurchased': true, 'itemRevenue': true },
metrics: ['itemsPurchased', 'itemRevenue'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
itemsPurchased: [{ 'chart': 'c1', 'name': 'Aantal gekochte items', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Items per product', type: 'bar', dataset: 'second' }],
itemRevenue: [{ 'chart': 'c1', 'name': 'Productopbrengst', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Opbrengst per product', type: 'pie', dataset: 'second' }],
},
},
ecommerce_page: {
name: 'Ecommerce per Pagina',
view: 'ecommerce_page',
first: { dimension: 'date' },
second: { dimension: 'landingPage', sort: 'totalRevenue', sortorder: 'DESCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'totalRevenue',
selectedmetrics: { 'sessions': true, 'totalRevenue': true },
metrics: ['sessions', 'totalRevenue'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessies per pagina', type: 'bar', dataset: 'second' }],
totalRevenue: [{ 'chart': 'c1', 'name': 'Totale opbrengst', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Totale opbrengst per pagina', type: 'pie', dataset: 'second' }]
},
},
ecommerce_channel: {
name: 'Ecommerce per Kanaal',
view: 'ecommerce_channel',
first: { dimension: 'date' },
second: { dimension: 'sessionDefaultChannelGrouping', sort: 'totalRevenue', sortorder: 'DESCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'totalRevenue',
selectedmetrics: { 'sessions': true, 'totalRevenue': true },
metrics: ['sessions', 'totalRevenue'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessies per kanaal', type: 'bar', dataset: 'second' }],
totalRevenue: [{ 'chart': 'c1', 'name': 'Totale opbrengst', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Totale opbrengst per kanaal', type: 'pie', dataset: 'second' }]
},
},
},
},
pages: {
views: {
all: {
name: 'Alle Pagina\'s',
view: 'all',
viewfilters: ['goals', 'channels'],
first: { dimension: 'date' },
second: { dimension: 'landingPage', sort: 'screenPageViews', sortorder: 'DESCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'screenPageViews',
selectedmetrics: { 'screenPageViews': true, 'totalUsers': true, 'userEngagementDuration': true, 'engagementRate': true, 'conversions': true },
metrics: ['screenPageViews', 'totalUsers', 'userEngagementDuration', 'engagementRate', 'conversions'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
screenPageViews: [{ 'chart': 'c1', 'name': 'Paginaweergaven', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pagina\'s', type: 'progress', dataset: 'second' }],
totalUsers: [{ 'chart': 'c1', 'name': 'Gebruikers', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pagina\'s', type: 'progress', dataset: 'second' }],
userEngagementDuration: [{ 'chart': 'c1', 'name': 'Gemiddelde Betrokkenheidsduur', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pagina\'s', type: 'progress', dataset: 'second' }],
engagementRate: [{ 'chart': 'c1', 'name': 'Betrokkenheidspercentage', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pagina\'s', type: 'progress', dataset: 'second' }],
conversions: [{ 'chart': 'c1', 'name': 'Conversies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pagina\'s', type: 'progress', dataset: 'second' }],
},
},
landing: {
name: 'Landingspagina\'s',
view: 'landing',
first: { dimension: 'date' },
second: { dimension: 'landingPage', sort: 'sessions', sortorder: 'DESCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'sessions',
selectedmetrics: { 'sessions': true, 'totalUsers': true, 'averageSessionDuration': true, 'engagementRate': true, 'conversions': true },
metrics: ['sessions', 'totalUsers', 'averageSessionDuration', 'engagementRate', 'conversions'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
sessions: [{ 'chart': 'c1', 'name': 'Sessies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Landingspagina\'s', type: 'progress', dataset: 'second' }],
totalUsers: [{ 'chart': 'c1', 'name': 'Gebruikers', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Landingspagina\'s', type: 'progress', dataset: 'second' }],
averageSessionDuration: [{ 'chart': 'c1', 'name': 'Gemiddelde Sessieduur', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Landingspagina\'s', type: 'progress', dataset: 'second' }],
engagementRate: [{ 'chart': 'c1', 'name': 'Betrokkenheidspercentage', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Landingspagina\'s', type: 'progress', dataset: 'second' }],
conversions: [{ 'chart': 'c1', 'name': 'Conversies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Landingspagina\'s', type: 'progress', dataset: 'second' }],
},
},
titles: {
name: 'Paginatitels',
view: 'titles',
first: { dimension: 'date' },
second: { dimension: 'pageTitle', sort: 'screenPageViews', sortorder: 'DESCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'screenPageViews',
selectedmetrics: { 'screenPageViews': true, 'totalUsers': true, 'userEngagementDuration': true, 'engagementRate': true, 'conversions': true },
metrics: ['screenPageViews', 'totalUsers', 'userEngagementDuration', 'engagementRate', 'conversions'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
screenPageViews: [{ 'chart': 'c1', 'name': 'Paginaweergaven', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pagina\'s', type: 'progress', dataset: 'second' }],
totalUsers: [{ 'chart': 'c1', 'name': 'Gebruikers', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pagina\'s', type: 'progress', dataset: 'second' }],
userEngagementDuration: [{ 'chart': 'c1', 'name': 'Gemiddelde Betrokkenheidsduur', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pagina\'s', type: 'progress', dataset: 'second' }],
engagementRate: [{ 'chart': 'c1', 'name': 'Betrokkenheidspercentage', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pagina\'s', type: 'progress', dataset: 'second' }],
conversions: [{ 'chart': 'c1', 'name': 'Conversies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pagina\'s', type: 'progress', dataset: 'second' }],
},
},
},
},
events: {
views: {
all: {
name: 'Alle Gebeurtenissen',
view: 'all',
first: { dimension: 'date' },
second: { dimension: 'eventName', sort: 'eventCount', sortorder: 'DESCENDING' },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'eventCount',
selectedmetrics: { 'eventCount': true, 'totalUsers': true, 'eventCountPerUser': true, 'eventValue': true },
metrics: ['eventCount', 'totalUsers', 'eventCountPerUser', 'eventValue'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
eventCount: [{ 'chart': 'c1', 'name': 'Aantal Gebeurtenissen', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Gebeurtenissen', type: 'bar', dataset: 'second' }],
totalUsers: [{ 'chart': 'c1', 'name': 'Totaal Aantal Gebruikers', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Gebeurtenissen', type: 'pie', dataset: 'second' }],
eventCountPerUser: [{ 'chart': 'c1', 'name': 'Gebeurtenissen per Gebruiker', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Gebeurtenissen', type: 'bar', dataset: 'second' }],
eventValue: [{ 'chart': 'c1', 'name': 'Gebeurteniswaarde', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Gebeurtenissen', type: 'pie', dataset: 'second' }],
},
},
conversions: {
name: 'Conversies',
view: 'conversions',
first: { dimension: 'date', filter: { 'isConversionEvent': true } },
second: { dimension: 'eventName', sort: 'eventCount', sortorder: 'DESCENDING', filter: { 'isConversionEvent': true } },
third: { dimension: 'year', sort: 'year', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'eventCount',
selectedmetrics: { 'eventCount': true, 'totalUsers': true, 'eventCountPerUser': true, 'eventValue': true },
metrics: ['eventCount', 'totalUsers', 'eventCountPerUser', 'eventValue'],
table: { dataset: 'second' },
panels: { dataset: 'third' },
charts: {
eventCount: [{ 'chart': 'c1', 'name': 'Aantal Conversies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Conversies', type: 'bar', dataset: 'second' }],
totalUsers: [{ 'chart': 'c1', 'name': 'Totaal Aantal Gebruikers', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Conversies', type: 'pie', dataset: 'second' }],
eventCountPerUser: [{ 'chart': 'c1', 'name': 'Conversies per Gebruiker', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Conversies', type: 'bar', dataset: 'second' }],
eventValue: [{ 'chart': 'c1', 'name': 'Conversiewaarde', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Conversies', type: 'pie', dataset: 'second' }],
},
},
},
}
}
$scope.setAnalyticsCustomer = function (data) {
$http.get('/rest/' + $scope.cfg.urlbase + '/profileid.set', {
params: {
websiteid: h.siteId(),
property: data.property
}
}).then(function successCallback(res) {
$scope.getReportData();
});
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('matomo', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function ($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
dateformat: "YYYY-MM-DD",
dimensionprefix: '',
urlbase: 'matomo',
switchclient: true,
report_endpoint: '/rest/matomo/report',
}
$scope.gloMetrics = {
nb_visits: { name: 'Sessions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' },
nb_actions: { name: 'Actions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' },
nb_actions_per_visit: { name: 'Actions/Visit', typefilter: 'number', filtercomparator: 2, 'datemerge': 'avg' },
bounce_rate: { name: 'Bounce Rate', typefilter: 'number', filtercomparator: 0, 'after': '%', 'datemerge': 'avg' },
avg_time_on_site: { name: 'Avg Session Duration', typefilter: 'secondsToHHmmss', filtercomparator: 0, 'datemerge': 'avg' },
nb_hits: { name: 'Pageviews', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' },
avg_time_on_page: { name: 'Avg Time on Page', typefilter: 'secondsToHHmmss', filtercomparator: 0, 'datemerge': 'avg' },
nb_conversions: { name: 'Conversions', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' },
revenue: { name: 'Revenue', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' },
nb_searches: { name: 'Site Searches', typefilter: 'number', filtercomparator: 0, 'datemerge': 'sum' },
};
$scope.selectedmetrics = {};
$scope.reportconfig = {
visitors: {
views: {
all: {
name: 'All',
view: 'all',
first: { dimension: 'date' },
second: { dimension: 'referrerType', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true, 'nb_actions_per_visit': true, 'avg_time_on_site': true, 'bounce_rate': true },
metrics: ['nb_visits', 'nb_actions', 'nb_actions_per_visit', 'avg_time_on_site', 'bounce_rate'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / referrer', type: 'pie', dataset: 'second' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Actions / referrer', type: 'pie', dataset: 'second' }],
nb_actions_per_visit: [{ 'chart': 'c1', 'name': 'Actions per Visit', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Actions/Visit / referrer', type: 'bar', dataset: 'second' }],
avg_time_on_site: [{ 'chart': 'c1', 'name': 'Avg Session Duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
geographic: {
name: 'Geographic',
view: 'geographic',
first: { dimension: 'country', sort: 'nb_visits', sortorder: 'DESCENDING' },
second: { dimension: 'country', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true, 'nb_actions_per_visit': true, 'avg_time_on_site': true, 'bounce_rate': true },
metrics: ['nb_visits', 'nb_actions', 'nb_actions_per_visit', 'avg_time_on_site', 'bounce_rate'],
table: { dataset: 'first' },
panels: { dataset: 'second' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
nb_actions_per_visit: [{ 'chart': 'c1', 'name': 'Actions per Visit', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
avg_time_on_site: [{ 'chart': 'c1', 'name': 'Avg Session Duration', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
},
},
device: {
name: 'Device',
view: 'device',
first: { dimension: 'deviceType', sort: 'nb_visits', sortorder: 'DESCENDING' },
second: { dimension: 'deviceType', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true, 'nb_actions_per_visit': true, 'avg_time_on_site': true, 'bounce_rate': true },
metrics: ['nb_visits', 'nb_actions', 'nb_actions_per_visit', 'avg_time_on_site', 'bounce_rate'],
table: { dataset: 'first' },
panels: { dataset: 'second' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
nb_actions_per_visit: [{ 'chart': 'c1', 'name': 'Actions per Visit', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
avg_time_on_site: [{ 'chart': 'c1', 'name': 'Avg Session Duration', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'bar', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
},
},
browser: {
name: 'Browser',
view: 'browser',
first: { dimension: 'browser', sort: 'nb_visits', sortorder: 'DESCENDING' },
second: { dimension: 'browser', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true, 'bounce_rate': true },
metrics: ['nb_visits', 'nb_actions', 'bounce_rate'],
table: { dataset: 'first' },
panels: { dataset: 'second' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
},
},
os: {
name: 'Operating System',
view: 'os',
first: { dimension: 'os', sort: 'nb_visits', sortorder: 'DESCENDING' },
second: { dimension: 'os', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true, 'bounce_rate': true },
metrics: ['nb_visits', 'nb_actions', 'bounce_rate'],
table: { dataset: 'first' },
panels: { dataset: 'second' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'progress', dataset: 'first' }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'first' }],
},
},
},
},
channels: {
views: {
all: {
name: 'All',
view: 'all',
first: { dimension: 'date' },
second: { dimension: 'referrerType', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true, 'nb_actions_per_visit': true, 'avg_time_on_site': true, 'bounce_rate': true },
metrics: ['nb_visits', 'nb_actions', 'nb_actions_per_visit', 'avg_time_on_site', 'bounce_rate'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / channel', type: 'pie', dataset: 'second' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Actions / channel', type: 'pie', dataset: 'second' }],
nb_actions_per_visit: [{ 'chart': 'c1', 'name': 'Actions per Visit', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
avg_time_on_site: [{ 'chart': 'c1', 'name': 'Avg Session Duration', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
organic: {
name: 'Organic',
view: 'organic',
first: { dimension: 'date' },
second: { dimension: 'searchEngine', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true, 'bounce_rate': true },
metrics: ['nb_visits', 'nb_actions', 'bounce_rate'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
social: {
name: 'Social',
view: 'social',
first: { dimension: 'date' },
second: { dimension: 'socialNetwork', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true, 'bounce_rate': true },
metrics: ['nb_visits', 'nb_actions', 'bounce_rate'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
referral: {
name: 'Referral',
view: 'referral',
first: { dimension: 'date' },
second: { dimension: 'website', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true, 'bounce_rate': true },
metrics: ['nb_visits', 'nb_actions', 'bounce_rate'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
campaigns: {
name: 'Campaigns',
view: 'campaigns',
first: { dimension: 'date' },
second: { dimension: 'campaign', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true, 'bounce_rate': true },
metrics: ['nb_visits', 'nb_actions', 'bounce_rate'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'pie', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': '_', type: 'bar', dataset: 'second' }],
},
},
},
},
pages: {
views: {
all: {
name: 'All Pages',
view: 'all',
first: { dimension: 'date' },
second: { dimension: 'pageUrl', sort: 'nb_hits', sortorder: 'DESCENDING' },
selectedmetric: 'nb_hits',
selectedmetrics: { 'nb_hits': true, 'nb_visits': true, 'bounce_rate': true, 'avg_time_on_page': true },
metrics: ['nb_hits', 'nb_visits', 'bounce_rate', 'avg_time_on_page'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_hits: [{ 'chart': 'c1', 'name': 'Pageviews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pages', type: 'progress', dataset: 'second' }],
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pages', type: 'progress', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pages', type: 'progress', dataset: 'second' }],
avg_time_on_page: [{ 'chart': 'c1', 'name': 'Avg Time on Page', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pages', type: 'progress', dataset: 'second' }],
},
},
titles: {
name: 'Page Titles',
view: 'titles',
first: { dimension: 'date' },
second: { dimension: 'pageTitle', sort: 'nb_hits', sortorder: 'DESCENDING' },
selectedmetric: 'nb_hits',
selectedmetrics: { 'nb_hits': true, 'nb_visits': true, 'bounce_rate': true, 'avg_time_on_page': true },
metrics: ['nb_hits', 'nb_visits', 'bounce_rate', 'avg_time_on_page'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_hits: [{ 'chart': 'c1', 'name': 'Pageviews', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pages', type: 'progress', dataset: 'second' }],
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pages', type: 'progress', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pages', type: 'progress', dataset: 'second' }],
avg_time_on_page: [{ 'chart': 'c1', 'name': 'Avg Time on Page', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Pages', type: 'progress', dataset: 'second' }],
},
},
entry: {
name: 'Entry Pages',
view: 'entry',
first: { dimension: 'date' },
second: { dimension: 'entryPage', sort: 'nb_visits', sortorder: 'DESCENDING' },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'bounce_rate': true },
metrics: ['nb_visits', 'bounce_rate'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Entry Pages', type: 'progress', dataset: 'second' }],
bounce_rate: [{ 'chart': 'c1', 'name': 'Bounce Rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Entry Pages', type: 'progress', dataset: 'second' }],
},
},
exit: {
name: 'Exit Pages',
view: 'exit',
first: { dimension: 'date' },
second: { dimension: 'exitPage', sort: 'nb_visits', sortorder: 'DESCENDING' },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true },
metrics: ['nb_visits'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Exit Pages', type: 'progress', dataset: 'second' }],
},
},
},
},
events: {
views: {
categories: {
name: 'Event Categories',
view: 'categories',
first: { dimension: 'date' },
second: { dimension: 'eventCategory', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true },
metrics: ['nb_visits', 'nb_actions'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Categories', type: 'bar', dataset: 'second' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Categories', type: 'pie', dataset: 'second' }],
},
},
actions: {
name: 'Event Actions',
view: 'actions',
first: { dimension: 'date' },
second: { dimension: 'eventAction', sort: 'nb_visits', sortorder: 'DESCENDING', 'allowcomp': true },
selectedmetric: 'nb_visits',
selectedmetrics: { 'nb_visits': true, 'nb_actions': true },
metrics: ['nb_visits', 'nb_actions'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
nb_visits: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Actions', type: 'bar', dataset: 'second' }],
nb_actions: [{ 'chart': 'c1', 'name': 'Actions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Top Actions', type: 'pie', dataset: 'second' }],
},
},
},
},
};
// Object for modal form binding (dot rule for ng-include child scope)
$scope.matomoForm = { url: '', token: '', error: null, connecting: false };
// Custom connect function for Matomo (form-based, not OAuth)
$scope.fnMatomoConnect = function () {
$scope.matomoForm.error = null;
$scope.matomoForm.connecting = true;
$http.post('/rest/matomo/connect', {
websiteid: h.siteId(),
url: $scope.matomoForm.url,
token_auth: $scope.matomoForm.token,
}).then(function (res) {
$scope.matomoForm.connecting = false;
if (res.data.error) {
$scope.matomoForm.error = res.data.error;
} else {
$scope.getCustomer();
}
}, function () {
$scope.matomoForm.connecting = false;
$scope.matomoForm.error = 'Connection failed. Please check the URL and try again.';
});
}
// Custom site selector for Matomo
$scope.setMatomoCustomer = function (data) {
$http.get('/rest/matomo/profileid.set', {
params: {
websiteid: h.siteId(),
idsite: data.idsite
}
}).then(function () {
$scope.getReportData();
});
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('copernica', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.pageowl.app/rest/copernica/connect?websiteid=' + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: '',
urlbase: 'copernica',
report_endpoint: '/rest/copernica/report',
}
$scope.gloMetrics = h.metrics('copernica');
/* $scope.gloMetrics = {
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 },
};
*/
//'page_fans','page_fans_country','page_fans_by_like_source','page_fans_gender_age'
$scope.reportconfig = {
mail: {
views: {
publishercampaigns: {
name: 'Publisher Campagnes',
view: 'publishercampaigns',
first: { dimension: 'subject', 'report': 'publishercampaigns' },
second: { dimension: 'title', 'report': 'publishercampaignsclicks' },
third: { dimension: 'title', 'report': 'publishercampaignsimpressions' },
selectedmetric: 'Placeholder',
dimension: 'title',
charts: {
Placeholder: [
{ 'chart': 'c0', 'name': 'Clicks', type: 'line', dataset: 'second', metric: 'clicks', dimension: 'date' },
{ 'chart': 'c1', 'name': 'Impressions', type: 'line', dataset: 'third', metric: 'impressions', dimension: 'date' },
],
},
metrics: [ 'type', 'destinations'],
table: { dataset: 'first' },
},
publishercampaign: {
name: 'Campagne',
view: 'publishercampaign',
hide: true,
first: { dimension: 'title', 'report': 'publishertimestats', filterexpression: $scope.urlfilter },
second: { dimension: 'title', 'report': 'publisherstats', filterexpression: $scope.urlfilter },
selectedmetric: 'Placeholder',
dimension: 'title',
metrics: ['destinations', 'abuses', 'clicks', 'uniqueclicks', 'deliveries', 'errors', 'impressions','uniqueimpressions','retries','unsubscribes'],
panels: { dataset: 'second' },
charts: {
Placeholder: [
{ 'chart': 'c0', 'name': 'Clicks', type: 'line', dataset: 'first', metric: 'clicks', dimension: 'date' },
{ 'chart': 'c1', 'name': 'Errors', type: 'line', dataset: 'first', metric: 'errors', dimension: 'date' },
{ 'chart': 'c2', 'name': 'Impressions', type: 'line', dataset: 'first', metric: 'impressions', dimension: 'date' },
{ 'chart': 'c3', 'name': 'Unsubscribes', type: 'line', dataset: 'first', metric: 'unsubscribes', dimension: 'date' },
],
},
},
mscampaigns: {
name: 'Campagnes',
view: 'mscampaigns',
first: { dimension: 'subject', 'report': 'mscampaigns' },
second: { dimension: 'title', 'report': 'mscampaignsclicks' },
third: { dimension: 'title', 'report': 'mscampaignsimpressions' },
selectedmetric: 'Placeholder',
dimension: 'subject',
charts: {
Placeholder: [
{ 'chart': 'c0', 'name': 'Clicks', type: 'line', dataset: 'second', metric: 'clicks', dimension: 'date' },
{ 'chart': 'c1', 'name': 'Impressions', type: 'line', dataset: 'third', metric: 'impressions', dimension: 'date' },
],
},
metrics: [ 'type', 'destinations'],
table: { dataset: 'first' },
},
mscampaign: {
name: 'Campagne',
view: 'mscampaign',
hide: true,
first: { dimension: 'title', 'report': 'mstimestats', filterexpression: $scope.urlfilter },
second: { dimension: 'title', 'report': 'msstats', filterexpression: $scope.urlfilter },
selectedmetric: 'Placeholder',
dimension: 'title',
metrics: ['destinations', 'abuses', 'clicks', 'uniqueclicks', 'deliveries', 'errors', 'impressions','uniqueimpressions','retries','unsubscribes'],
panels: { dataset: 'second' },
charts: {
Placeholder: [
{ 'chart': 'c0', 'name': 'Clicks', type: 'line', dataset: 'first', metric: 'clicks', dimension: 'date' },
{ 'chart': 'c1', 'name': 'Errors', type: 'line', dataset: 'first', metric: 'errors', dimension: 'date' },
{ 'chart': 'c2', 'name': 'Impressions', type: 'line', dataset: 'first', metric: 'impressions', dimension: 'date' },
{ 'chart': 'c3', 'name': 'Unsubscribes', type: 'line', dataset: 'first', metric: 'unsubscribes', dimension: 'date' },
],
},
},
},
},
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('youtube', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.pageowl.app/rest/youtube/connect?site_id=' + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: '',
urlbase: 'youtube',
report_endpoint: '/rest/youtube/report',
}
$scope.gloMetrics = h.metrics('youtube');
/* $scope.gloMetrics = {
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' },
};
*/
$scope.fnFormatAd = function(d) {
return d.HeadlinePart1 + d.HeadlinePart2 + d.Description + d.CreativeDestinationUrl + d.CreativeFinalUrls;
}
$scope.reportconfig = {
channel: {
views: {
overview: {
name: 'Overview',
view: 'overview',
second: { dimension: 'Date', report: 'analytics', 'allowcomp': true },
third: { dimension: 'insightTrafficSourceType', report: 'analytics', metrics: ['views', 'estimatedMinutesWatched'] },
selectedmetric: 'views',
selectedmetrics: { 'Clicks': true, 'Impressions': true, 'Cost': true, 'AverageCpc': true, 'Ctr': true, 'Conversions': true, 'CostPerConversion': true, 'ConversionRate': true, 'AbsoluteTopImpressionPercentage': true, 'ConversionValue': true, 'SearchImpressionShare': true, 'roas': true },
metrics: ['views', 'shares', 'likes', 'dislikes', 'comments', 'subscribersGained', 'subscribersLost', 'estimatedMinutesWatched', 'annotationCloseRate', 'annotationClickThroughRate', 'averageViewDuration'],
//table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
views: [{ 'chart': 'c1', 'name': 'views', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
shares: [{ 'chart': 'c1', 'name': 'Shares', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
likes: [{ 'chart': 'c1', 'name': 'Likes', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
dislikes: [{ 'chart': 'c1', 'name': 'Dislikes', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
comments: [{ 'chart': 'c1', 'name': 'Comments', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
subscribersGained: [{ 'chart': 'c1', 'name': 'Subscribers Gained', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
subscribersLost: [{ 'chart': 'c1', 'name': 'Subscribers Lost', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
estimatedMinutesWatched: [{ 'chart': 'c1', 'name': 'Minutes Watched', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
AbsoluteTopImpressionPercentage: [{ 'chart': 'c1', 'name': 'Average position', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
ConversionValue: [{ 'chart': 'c1', 'name': 'Conversion value', type: 'line', dataset: 'second', date: true },{ 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
},
},
anderekak: {
name: 'Overview',
view: 'anderekak',
first: { dimension: 'Date', report: 'channelstats' },
selectedmetric: 'views',
selectedmetrics: { 'Clicks': true, 'Impressions': true, 'Cost': true, 'AverageCpc': true, 'Ctr': true, 'Conversions': true, 'CostPerConversion': true, 'ConversionRate': true, 'AbsoluteTopImpressionPercentage': true, 'ConversionValue': true, 'SearchImpressionShare': true, 'roas': true },
metrics: ['commentCount', 'subscriberCount', 'videoCount', 'viewCount'],
//table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
views: [{ 'chart': 'c1', 'name': 'views', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
shares: [{ 'chart': 'c1', 'name': 'Shares', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
likes: [{ 'chart': 'c1', 'name': 'Likes', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
dislikes: [{ 'chart': 'c1', 'name': 'Dislikes', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
comments: [{ 'chart': 'c1', 'name': 'Comments', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
subscribersGained: [{ 'chart': 'c1', 'name': 'Subscribers Gained', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
subscribersLost: [{ 'chart': 'c1', 'name': 'Subscribers Lost', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
estimatedMinutesWatched: [{ 'chart': 'c1', 'name': 'Minutes Watched', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
AbsoluteTopImpressionPercentage: [{ 'chart': 'c1', 'name': 'Average position', type: 'line', dataset: 'second', date: true }, { 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
ConversionValue: [{ 'chart': 'c1', 'name': 'Conversion value', type: 'line', dataset: 'second', date: true },{ 'chart': 'c2', 'name': 'Views / Source', 'metric':'views', type: 'bar', dataset: 'third' }],
},
},
videos: {
name: 'Ad Groups',
view: 'videos',
first: { dimension: 'Date', report: 'videos' },
//second: { dimension: 'AverageCpm', sort: 'sessions', sortorder: 'DESCENDING', },
selectedmetric: 'Clicks',
metrics: ['Clicks'],
},
},
}
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('googleads', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function ($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.marketingtracer.com/rest/googlexads/connect?websiteid=' + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: '',
urlbase: 'googlexads',
switchclient: true,
report_endpoint: '/rest/googlexads/report',
}
if ($rootScope.user.reseller?.config?.authdomain) {
$scope.cfg.authurl = 'https://' + $rootScope.user.reseller.config.authdomain + '/rest/googlexads/connect?websiteid=' + h.siteId();
}
$scope.gloMetrics = h.metrics('googlexads');
/* $scope.gloMetrics = {
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'},
};*/
$scope.fnFormatAd = function (d) {
return d.HeadlinePart1 + d.HeadlinePart2 + d.Description + d.CreativeDestinationUrl + d.CreativeFinalUrls;
}
$scope.reportconfig = {
campaigns: {
views: {
all: {
name: 'Campaigns',
view: 'all',
first: { dimension: 'Date', report: 'ACCOUNT_PERFORMANCE_REPORT' },
second: { dimension: 'CampaignName', report: 'CAMPAIGN_PERFORMANCE_REPORT', 'allowcomp': true },
selectedmetric: 'Clicks',
selectedmetrics: { 'Clicks': true, 'Impressions': true, 'Cost': true, 'AverageCpc': true, 'Ctr': true, 'Conversions': true, 'CostPerConversion': true, 'ConversionRate': true, 'AbsoluteTopImpressionPercentage': true, 'ConversionValue': true, 'SearchImpressionShare': true, 'roas': true },
metrics: ['Clicks', 'Impressions', 'Cost', 'AverageCpc', 'Ctr', 'Conversions', 'CostPerConversion', 'ConversionRate', 'AbsoluteTopImpressionPercentage', 'ConversionValue', 'roas'],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
Clicks: [{ 'chart': 'c1', 'name': 'Clicks', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Clicks / Campaign', type: 'progress', dataset: 'second' }],
Impressions: [{ 'chart': 'c1', 'name': 'Impressions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Impressions / Campaign', type: 'progress', dataset: 'second' }],
Cost: [{ 'chart': 'c1', 'name': 'Kosten', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Campaign', type: 'progress', dataset: 'second' }],
AverageCpc: [{ 'chart': 'c1', 'name': 'Avg cost / Click', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'CPC / Campaign', type: 'progress', dataset: 'second' }],
Ctr: [{ 'chart': 'c1', 'name': 'Ctr', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Ctr / Campaign', type: 'progress', dataset: 'second' }],
Conversions: [{ 'chart': 'c1', 'name': 'Conversies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversies / Campaign', type: 'progress', dataset: 'second' }],
CostPerConversion: [{ 'chart': 'c1', 'name': 'Cost / conversion', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Cost / conversion / Campaign', type: 'progress', dataset: 'second' }],
ConversionRate: [{ 'chart': 'c1', 'name': 'Conversion rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversion rate / Campaign', type: 'progress', dataset: 'second' }],
AbsoluteTopImpressionPercentage: [{ 'chart': 'c1', 'name': 'Average position', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Average position / Campaign', type: 'progress', dataset: 'second' }],
ConversionValue: [{ 'chart': 'c1', 'name': 'Conversion value', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversion value / Campaign', type: 'progress', dataset: 'second' }],
},
},
adgroups: {
name: 'Ad Groups',
view: 'adgroups',
first: { dimension: 'Date', report: 'ACCOUNT_PERFORMANCE_REPORT' },
second: { dimension: 'AdGroupName', report: 'ADGROUP_PERFORMANCE_REPORT', 'allowcomp': true },
//second: { dimension: 'AverageCpm', sort: 'sessions', sortorder: 'DESCENDING', },
selectedmetric: 'Clicks',
selectedmetrics: { 'Clicks': true, 'Impressions': true, 'Cost': true, 'AverageCpc': true, 'Ctr': true, 'Conversions': true, 'CostPerConversion': true, 'ConversionRate': true, 'AbsoluteTopImpressionPercentage': true, 'ConversionValue': true, 'SearchImpressionShare': true, 'roas': true },
metrics: ['Clicks', 'Impressions', 'Cost', 'AverageCpc', 'Ctr', 'Conversions', 'CostPerConversion', 'ConversionRate', 'AbsoluteTopImpressionPercentage', 'ConversionValue', 'roas'],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
Clicks: [{ 'chart': 'c1', 'name': 'Clicks', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Clicks / Adgroup', type: 'progress', dataset: 'second' }],
Impressions: [{ 'chart': 'c1', 'name': 'Impressions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Impressions / Adgroup', type: 'progress', dataset: 'second' }],
Cost: [{ 'chart': 'c1', 'name': 'Kosten', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Adgroup', type: 'progress', dataset: 'second' }],
AverageCpc: [{ 'chart': 'c1', 'name': 'Avg cost / Click', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'CPC / Adgroup', type: 'progress', dataset: 'second' }],
Ctr: [{ 'chart': 'c1', 'name': 'Ctr', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Ctr / Adgroup', type: 'progress', dataset: 'second' }],
Conversions: [{ 'chart': 'c1', 'name': 'Conversies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversies / Adgroup', type: 'progress', dataset: 'second' }],
CostPerConversion: [{ 'chart': 'c1', 'name': 'Cost / conversion', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Cost / conversion / Adgroup', type: 'progress', dataset: 'second' }],
ConversionRate: [{ 'chart': 'c1', 'name': 'Conversion rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversion rate / Adgroup', type: 'progress', dataset: 'second' }],
AbsoluteTopImpressionPercentage: [{ 'chart': 'c1', 'name': 'Average position', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Average position / Adgroup', type: 'progress', dataset: 'second' }],
ConversionValue: [{ 'chart': 'c1', 'name': 'Conversion value', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversion value / Adgroup', type: 'progress', dataset: 'second' }],
},
},
keywords: {
name: 'Keywords',
view: 'keywords',
first: { dimension: 'Date', report: 'ACCOUNT_PERFORMANCE_REPORT' },
second: { dimension: 'Query', report: 'SEARCH_QUERY_PERFORMANCE_REPORT', 'allowcomp': true },
selectedmetric: 'Clicks',
selectedmetrics: { 'Clicks': true, 'Impressions': true, 'Cost': true, 'AverageCpc': true, 'Ctr': true, 'Conversions': true, 'CostPerConversion': true, 'ConversionRate': true, 'AbsoluteTopImpressionPercentage': true, 'ConversionValue': true, 'roas': true },
metrics: ['Clicks', 'Impressions', 'Cost', 'AverageCpc', 'Ctr', 'Conversions', 'CostPerConversion', 'ConversionRate', 'AbsoluteTopImpressionPercentage', 'ConversionValue', 'roas'],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
Clicks: [{ 'chart': 'c1', 'name': 'Clicks', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Clicks / Keyword', type: 'bar', dataset: 'second' }],
Impressions: [{ 'chart': 'c1', 'name': 'Impressions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Impressions / Keyword', type: 'bar', dataset: 'second' }],
Cost: [{ 'chart': 'c1', 'name': 'Kosten', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Keyword', type: 'bar', dataset: 'second' }],
AverageCpc: [{ 'chart': 'c1', 'name': 'Avg cost / Click', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'CPC / Keyword', type: 'bar', dataset: 'second' }],
Ctr: [{ 'chart': 'c1', 'name': 'Ctr', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Ctr / Keyword', type: 'bar', dataset: 'second' }],
Conversions: [{ 'chart': 'c1', 'name': 'Conversies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversies / Keyword', type: 'bar', dataset: 'second' }],
CostPerConversion: [{ 'chart': 'c1', 'name': 'Cost / conversion', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Cost / conversion / Keyword', type: 'bar', dataset: 'second' }],
ConversionRate: [{ 'chart': 'c1', 'name': 'Conversion rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversion rate / Keyword', type: 'bar', dataset: 'second' }],
AbsoluteTopImpressionPercentage: [{ 'chart': 'c1', 'name': 'Average position', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Average position / Keyword', type: 'bar', dataset: 'second' }],
ConversionValue: [{ 'chart': 'c1', 'name': 'Conversion value', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversion value / Keyword', type: 'bar', dataset: 'second' }],
},
},
// ads: {
// name: 'Ads',
// view: 'ads',
// first: { dimension: 'Date', report: 'ACCOUNT_PERFORMANCE_REPORT', 'allowcomp': true },
// second: { dimension: 'HeadlinePart1,HeadlinePart2,Description,CreativeDestinationUrl,CreativeFinalUrls', function: 'fnFormatAd', report: 'AD_PERFORMANCE_REPORT' },
// selectedmetric: 'Clicks',
// selectedmetrics: { 'Clicks': true, 'Impressions': true, 'Cost': true, 'AverageCpc': true, 'Ctr': true, 'Conversions': true, 'CostPerConversion': true, 'ConversionRate': true, 'AbsoluteTopImpressionPercentage': true, 'ConversionValue': true, 'roas': true },
// metrics: ['Clicks', 'Impressions', 'Cost', 'AverageCpc', 'Ctr', 'Conversions', 'CostPerConversion', 'ConversionRate', 'AbsoluteTopImpressionPercentage', 'ConversionValue', 'roas'],
// table: { dataset: 'second' },
// panels: { dataset: 'second' },
// charts: {
// Clicks: [{ 'chart': 'c1', 'name': 'Clicks', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Clicks / Ad', type: 'bar', dataset: 'second' }],
// Impressions: [{ 'chart': 'c1', 'name': 'Impressions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Impressions / Ad', type: 'bar', dataset: 'second' }],
// Cost: [{ 'chart': 'c1', 'name': 'Kosten', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Ad', type: 'bar', dataset: 'second' }],
// AverageCpc: [{ 'chart': 'c1', 'name': 'Avg cost / Click', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'CPC / Ad', type: 'bar', dataset: 'second' }],
// Ctr: [{ 'chart': 'c1', 'name': 'Ctr', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Ctr / Ad', type: 'bar', dataset: 'second' }],
// Conversions: [{ 'chart': 'c1', 'name': 'Conversies', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversies / Ad', type: 'bar', dataset: 'second' }],
// CostPerConversion: [{ 'chart': 'c1', 'name': 'Cost / conversion', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Cost / conversion / Ad', type: 'bar', dataset: 'second' }],
// ConversionRate: [{ 'chart': 'c1', 'name': 'Conversion rate', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversion rate / Ad', type: 'bar', dataset: 'second' }],
// AbsoluteTopImpressionPercentage: [{ 'chart': 'c1', 'name': 'Average position', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Average position / Ad', type: 'bar', dataset: 'second' }],
// ConversionValue: [{ 'chart': 'c1', 'name': 'Conversion value', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Conversion value / Ad', type: 'bar', dataset: 'second' }],
// },
// },
// conversions: {
// name: 'Conversions',
// view: 'conversions',
// first: { dimension: 'Date', report: 'ACCOUNT_PERFORMANCE_REPORT' },
// second: { dimension: 'ConversionTypeName', report: 'ACCOUNT_PERFORMANCE_REPORT', 'allowcomp': true },
// selectedmetric: 'Conversions',
// selectedmetrics: { 'Conversions': true, 'ConversionValue': true },
// //metrics: ['Conversions', 'CostPerConversion', 'ConversionRate', 'ConversionValue'],
// metrics: ['Conversions','ConversionValue'],
// table: { dataset: 'second' },
// panels: { dataset: 'second' },
// charts: {
// Clicks: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Channel', type: 'bar', dataset: 'second' }],
// Impressions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Channel', type: 'bar', dataset: 'second' }],
// Cost: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Channel', type: 'bar', dataset: 'second' }],
// AverageCpc: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Channel', type: 'bar', dataset: 'second' }],
// Ctr: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Channel', type: 'bar', dataset: 'second' }],
// Conversions: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Channel', type: 'bar', dataset: 'second' }],
// CostPerConversion: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Channel', type: 'bar', dataset: 'second' }],
// ConversionRate: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Channel', type: 'bar', dataset: 'second' }],
// AbsoluteTopImpressionPercentage: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Channel', type: 'bar', dataset: 'second' }],
// ConversionValue: [{ 'chart': 'c1', 'name': 'Sessions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Sessions / Channel', type: 'bar', dataset: 'second' }],
// },
// },
},
}
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('facebookads', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.marketingtracer.com/rest/facebookads/connect?websiteid=' + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: '',
urlbase: 'facebookads',
report_endpoint: '/rest/facebookads/report',
}
$scope.gloMetrics = h.metrics('facebookads');
/* $scope.gloMetrics = {
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: $filter('translate')('metrics.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'},
};
*/
$scope.reportconfig = {
campaigns: {
views: {
all: {
name: 'Campagnes',
view: 'all',
viewfilters: ['campaigns'],
first: { dimension: 'date', level: 'account', 'endpoint': 'insights' },
second: { dimension: 'campaign_name', level: 'campaign', 'endpoint': 'insights', 'summary': 'clicks,impressions,cpc,cpm', 'allowcomp': true },
selectedmetric: 'clicks',
selectedmetrics: { 'clicks': true, 'impressions': true, 'cpc': true, 'cpm': true },
metrics: ['clicks', 'impressions', 'cpc', 'cpm','conversion_values','conversions','cost_per_conversion','website_ctr','website_purchase_roas'],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
clicks: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.clicks') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.clicks') + ' / Campaign', type: 'bar', dataset: 'second' }],
impressions: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.impressions') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.impressions') + ' / Campaign', type: 'bar', dataset: 'second' }],
cpc: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.cpc') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpc') + ' / Campaign', type: 'bar', dataset: 'second' }],
cpm: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.cpm') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpm') + ' / Campaign', type: 'bar', dataset: 'second' }],
},
},
adsets: {
name: 'Advertentie Sets',
view: 'adsets',
viewfilters: ['campaigns'],
first: { dimension: 'date', level: 'account', 'endpoint': 'insights', 'summary': 'clicks,impressions,cpc,cpm,spend,ctr', 'allowcomp': true },
second: { dimension: 'adset_name', level: 'adset', endpoint: 'insights' },
third: { dimension: 'date', level: 'account', 'endpoint': 'insights' },
selectedmetric: 'clicks',
selectedmetrics: { 'clicks': true, 'impressions': true, 'cpc': true, 'cpm': true, 'ctr': true, 'spend': true },
metrics: ['clicks', 'impressions', 'cpc', 'cpm', 'ctr', 'spend'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
clicks: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.clicks') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.clicks') + ' / ad set', type: 'bar', dataset: 'second' }],
impressions: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.impressions') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.impressions') + ' / ad set', type: 'bar', dataset: 'second' }],
cpc: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.cpc') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpc') + ' / ad set', type: 'bar', dataset: 'second' }],
cpm: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.cpm') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpm') + ' / ad set', type: 'bar', dataset: 'second' }],
ctr: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.ctr') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.ctr') + ' / ad set', type: 'bar', dataset: 'second' }],
spend: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.spend') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.spend') + ' / ad set', type: 'bar', dataset: 'second' }],
},
},
ads: {
name: 'Advertenties',
view: 'ads',
viewfilters: ['adsets', 'campaigns', 'ads'],
first: { dimension: 'date', level: 'account', 'endpoint': 'insights', 'summary': 'clicks,impressions,cpc,cpm,spend,ctr', 'allowcomp': true },
second: { dimension: 'ad_name', level: 'ad', endpoint: 'insights' },
selectedmetric: 'clicks',
selectedmetrics: { 'clicks': true, 'impressions': true, 'cpc': true, 'cpm': true, 'ctr': true, 'spend': true },
metrics: ['clicks', 'impressions', 'cpc', 'cpm', 'ctr', 'spend'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
clicks: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.clicks') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.clicks') + ' / ad', type: 'bar', dataset: 'second' }],
impressions: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.impressions') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.impressions') + ' / ad', type: 'bar', dataset: 'second' }],
cpc: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.cpc') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpc') + ' / ad', type: 'bar', dataset: 'second' }],
cpm: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.cpm') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpm') + ' / ad', type: 'bar', dataset: 'second' }],
ctr: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.ctr') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.ctr') + ' / ad', type: 'bar', dataset: 'second' }],
spend: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.spend') + ' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.spend') + ' / ad', type: 'bar', dataset: 'second' }],
},
},
age: {
name: 'Leeftijd',
view: 'age',
viewfilters: ['adsets', 'campaigns', 'ads'],
first: { level: 'account', 'endpoint': 'insights', 'breakdowns': 'age', 'dimension': 'date', 'dimension2': 'age', 'summary': 'clicks,impressions,cpc,cpm', 'allowcomp': true },
second: { level: 'account', 'endpoint': 'insights', 'breakdowns': 'age', 'dimension': 'age' },
selectedmetric: 'clicks',
selectedmetrics: { 'clicks': true, 'impressions': true, 'cpc': true, 'cpm': true },
metrics: ['clicks', 'impressions', 'cpc', 'cpm'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
clicks: [{ 'chart': 'c1', 'name': 'Klikken, leeftijd en tijd', type: 'linemulti', dataset: 'first', date: true, 'graphs': ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'] }, { 'chart': 'c2', 'name': $filter('translate')('metrics.clicks') + ' / age group', type: 'bar', dataset: 'second' }],
impressions: [{ 'chart': 'c1', 'name': 'Impressies,leeftijd en tijd', type: 'linemulti', dataset: 'first', date: true, 'graphs': ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'] }, { 'chart': 'c2', 'name': $filter('translate')('metrics.impressions') + ' / age group', type: 'bar', dataset: 'second' }],
cpc: [{ 'chart': 'c1', 'name': 'CPC,leeftijd en tijd', type: 'linemulti', dataset: 'first', date: true, 'graphs': ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'] }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpm') + ' / age group', type: 'bar', dataset: 'second' }],
cpm: [{ 'chart': 'c1', 'name': 'CPM,leeftijd en tijd', type: 'linemulti', dataset: 'first', date: true, 'graphs': ['18-24', '25-34', '35-44', '45-54', '55-64', '65+'] }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpc') + ' / age group', type: 'bar', dataset: 'second' }],
},
},
gender: {
name: 'Geslacht',
view: 'gender',
viewfilters: ['adsets', 'campaigns', 'ads'],
first: { level: 'account', 'endpoint': 'insights', 'breakdowns': 'gender', 'dimension': 'date', 'dimension2': 'gender', 'summary': 'clicks,impressions,cpc,cpm', 'allowcomp': true },
second: { level: 'account', 'endpoint': 'insights', 'breakdowns': 'gender', 'dimension': 'gender' },
selectedmetric: 'clicks',
selectedmetrics: { 'clicks': true, 'impressions': true, 'cpc': true, 'cpm': true },
metrics: ['clicks', 'impressions', 'cpc', 'cpm'],
table: { dataset: 'second' },
panels: { dataset: 'first' },
charts: {
clicks: [{ 'chart': 'c1', 'name': 'Klikken, geslacht per dag', type: 'linemulti', dataset: 'first', date: true, 'graphs': ['male', 'female'] }, { 'chart': 'c2', 'name': $filter('translate')('metrics.clicks') + ' / Gender', type: 'bar', dataset: 'second' }],
impressions: [{ 'chart': 'c1', 'name': 'Impressies, geslacht per dag', type: 'linemulti', dataset: 'first', date: true, 'graphs': ['male', 'female'] }, { 'chart': 'c2', 'name': $filter('translate')('metrics.impressions') + ' / Gender', type: 'bar', dataset: 'second' }],
cpc: [{ 'chart': 'c1', 'name': 'CPC, geslacht per dag', type: 'linemulti', dataset: 'first', date: true, 'graphs': ['male', 'female'] }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpc') + ' / Gender', type: 'bar', dataset: 'second' }],
cpm: [{ 'chart': 'c1', 'name': 'CPM, geslacht per dag', type: 'linemulti', dataset: 'first', date: true, 'graphs': ['male', 'female'] }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpm') + ' / Gender', type: 'bar', dataset: 'second' }],
},
},
},
},
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('facebookinsights', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.marketingtracer.com/rest/facebookinsights/connect?websiteid=' + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: '',
urlbase: 'facebookinsights',
report_endpoint: '/rest/facebookinsights/report',
}
$scope.gloMetrics = h.metrics('facebookinsights');
/* $scope.gloMetrics = {
fans: { name: 'Fans', typefilter: 'number', filtercomparator: 0 },
interacties: { name: 'Interactions', typefilter: 'number', filtercomparator: 0 },
bereik: { name: 'Bereik', typefilter: 'number', filtercomparator: 0 },
};*/
//'page_fans','page_fans_country','page_fans_by_like_source','page_fans_gender_age'
$scope.reportconfig = {
page: {
views: {
fans: {
name: 'Fans',
view: 'fans',
first: { 'report': 'timeline', 'endpoint': 'insights', 'dimension': 'date' },
second: { 'report': 'fans', 'endpoint': 'insights', 'dimension': 'date' },
selectedmetric: 'fans',
metrics: [],
charts: {
fans: [
{ 'chart': 'c1', 'name': 'Total fans', type: 'line', dataset: 'page_fans_total', date: true, 'dimension': 'date', 'metric': 'change' },
{ 'chart': 'c2', 'dimension2': 'type', 'name': 'Lost and new fans', type: 'linemulti', dataset: 'page_fans_delta', date: true, 'dimension': 'date', 'metric': 'change', 'graphs': ['page_fan_removes', 'page_fans_adds_paid', 'page_fans_adds_unpaid'] },
{ 'chart': 'c3', 'name': 'Female fans / age ', type: 'bar', dataset: 'page_fans_female_age', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c4', 'name': 'Male fans / age ', type: 'bar', dataset: 'page_fans_male_age', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c5', 'name': 'Distribution Male and Female fans', type: 'pie', dataset: 'page_fans_age', 'dimension': 'key', 'metric': 'val' },
],
},
},
engagements: {
name: 'Interactie',
view: 'engagements',
first: { 'report': 'timeline', 'endpoint': 'insights', 'dimension': 'date' },
second: { 'report': 'engagements', 'endpoint': 'insights', 'dimension': 'date' },
selectedmetric: 'interacties',
metrics: [],
charts: {
interacties: [
{ 'chart': 'c1', 'dimension2': 'type', 'name': 'Total interactions / day', type: 'linemulti', dataset: 'engagements', date: true, 'dimension': 'date', 'metric': 'value', 'graphs': ['engagements'] },
{ 'chart': 'c2', 'dimension2': 'type', 'name': 'Interactions / type', type: 'linemulti', dataset: 'engagements', date: true, 'dimension': 'date', 'metric': 'value', 'graphs': ['engagements_comments', 'engagements_likes', 'engagements_links', 'engagements_others'] },
{ 'chart': 'c3', 'name': 'Female interactions / age ', type: 'bar', dataset: 'page_content_activity_female_age', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c4', 'name': 'Male interactions / age ', type: 'bar', dataset: 'page_content_activity_male_age', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c5', 'name': 'Interactions / age ', type: 'pie', dataset: 'page_content_activity_age', 'dimension': 'key', 'metric': 'val' },
],
},
},
reach: {
name: 'Bereik',
view: 'reach',
first: { 'report': 'timeline', 'endpoint': 'insights', 'dimension': 'date' },
second: { 'report': 'reach', 'endpoint': 'insights', 'dimension': 'date' },
selectedmetric: 'bereik',
metrics: [],
charts: {
bereik: [
{ 'chart': 'c1', 'name': 'Total bereik', type: 'line', dataset: 'page_impressions', date: true, 'dimension': 'date', 'metric': 'change' },
{ 'chart': 'c2', 'dimension2': 'type', 'name': 'Bereik organisch en betaald', type: 'linemulti', dataset: 'page_impressions_type', date: true, 'dimension': 'date', 'metric': 'value', 'graphs': ['page_impressions_paid', 'page_impressions_organic'] },
{ 'chart': 'c3', 'name': 'Female reach / age ', type: 'bar', dataset: 'page_impressions_female_age', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c4', 'name': 'Male reach / age ', type: 'bar', dataset: 'page_impressions_male_age', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c5', 'name': 'Reach / gender', type: 'pie', dataset: 'page_impressions_age', 'dimension': 'key', 'metric': 'val' },
],
},
},
posts: {
name: 'Posts',
view: 'posts',
first: { 'report': 'posts' },
selectedmetric: 'posts',
metrics: [],
charts: {
},
},
},
},
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('instagram', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.pageowl.app/rest/instagram/connect?websiteid=' + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: '',
urlbase: 'instagram',
report_endpoint: '/rest/instagram/report',
}
$scope.gloMetrics = h.metrics('instagram');
/*$scope.gloMetrics = {
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 },
};*/
//'page_fans','page_fans_country','page_fans_by_like_source','page_fans_gender_age'
$scope.reportconfig = {
page: {
views: {
insights: {
name: 'insights',
view: 'insights',
first: { 'report': 'timeline', 'endpoint': 'insights', 'dimension': 'date' },
//second: { 'report': 'fans', 'endpoint': 'insights', 'dimension': 'date' },
selectedmetric: 'impressions',
metrics: ['impressions', 'reach', 'follower_count'],
panels: { dataset: 'first' },
charts: {
impressions: [
{ 'chart': 'c1', 'name': 'Follower count', type: 'line', dataset: 'follower_count', date: true, 'dimension': 'date', 'metric': 'change' },
{ 'chart': 'c2', 'name': 'Reach', type: 'line', dataset: 'reach', date: true, 'dimension': 'date', 'metric': 'change' },
{ 'chart': 'c3', 'name': 'Impressions', type: 'line', dataset: 'impressions', date: true, 'dimension': 'date', 'metric': 'change' },
],
},
},
engagements: {
name: 'Interactie',
view: 'engagements',
first: { 'report': 'timeline', 'endpoint': 'insights', 'dimension': 'date' },
//second: { 'report': 'engagements', 'endpoint': 'insights', 'dimension': 'date' },
selectedmetric: 'profile_views',
metrics: ['profile_views', 'website_clicks', 'email_contacts', 'get_directions_clicks', 'phone_call_clicks', 'text_message_clicks'],
panels: { dataset: 'first' },
charts: {
profile_views: [
{ 'chart': 'c1', 'name': 'Profile views', type: 'line', dataset: 'profile_views', date: true, 'dimension': 'date', 'metric': 'change' },
{ 'chart': 'c2', 'name': 'Website clicks', type: 'line', dataset: 'website_clicks', date: true, 'dimension': 'date', 'metric': 'change' },
{ 'chart': 'c3', 'name': 'Email contacts', type: 'line', dataset: 'email_contacts', date: true, 'dimension': 'date', 'metric': 'change' },
],
},
},
audience: {
name: 'Bereik',
view: 'audience',
//first: { 'report': 'timeline', 'endpoint': 'insights', 'dimension': 'date' },
second: { 'report': 'audience', 'endpoint': 'insights', 'dimension': 'date' },
selectedmetric: 'bereik',
metrics: [],
charts: {
bereik: [
{ 'chart': 'c1', 'name': 'Female audience / age ', type: 'bar', dataset: 'audience_gender_female_age', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c2', 'name': 'Male audience / age ', type: 'bar', dataset: 'audience_gender_male_age', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c3', 'name': 'Gender', type: 'pie', dataset: 'audience_gender', 'dimension': 'type', 'metric': 'val' },
],
},
},
posts: {
name: 'Posts',
view: 'posts',
first: { 'report': 'posts' },
selectedmetric: 'posts',
metrics: [],
},
stories: {
name: 'Stories',
view: 'stories',
first: { 'report': 'stories' },
selectedmetric: 'stories',
metrics: [],
},
},
},
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller("linkedin", [
"$scope",
"$timeout",
"$http",
"$rootScope",
"$controller",
"$filter",
"h",
function ($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: "https://app.pageowl.app/rest/linkedin/connect?websiteid=" + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: "",
urlbase: "linkedin",
report_endpoint: "/rest/linkedin/report",
};
$scope.gloMetrics = h.metrics("linkedin");
// Added pageViews and uniqueVisitors to global metrics
$scope.gloMetrics["pageViews"] = { typefilter: "number", filtercomparator: 0, datemerge: "sum" };
$scope.gloMetrics["uniqueVisitors"] = { typefilter: "number", filtercomparator: 0, datemerge: "sum" };
//'page_fans','page_fans_country','page_fans_by_like_source','page_fans_gender_age'
$scope.reportconfig = {
page: {
views: {
interactions: {
name: "Interactions",
view: "interactions",
second: { dimension: "title", report: "followerstatistics-time" },
third: { dimension: "title", report: "sharestatistics" },
selectedmetric: "likeCount",
metrics: ["likeCount", "shareCount", "engagement", "clickCount", "impressionCount", "commentCount"],
panels: { dataset: "third" },
charts: {
likeCount: [
{ chart: "c0", name: "Likes / days", type: "line", dataset: "third", dimension: "date" },
{ chart: "c1", name: "New followers by type", type: "pie", dataset: "newfollowerdist", dimension: "key", metric: "val" },
{ chart: "c2", metric: "abc", dimension2: "type", name: "Lost and new followers", type: "linemulti", dataset: "newfollowerdelta", date: true, dimension: "date", metric: "value", graphs: ["Organic", "Paid"] },
],
shareCount: [
{ chart: "c0", name: "Shares / Day", type: "line", dataset: "third", dimension: "date" },
{ chart: "c1", name: "New followers by type", type: "pie", dataset: "newfollowerdist", dimension: "key", metric: "val" },
{ chart: "c2", dimension2: "type", name: "Lost and new followers", type: "linemulti", dataset: "newfollowerdelta", date: true, dimension: "date", metric: "value", graphs: ["Organic", "Paid"] },
],
engagement: [
{ chart: "c0", name: "Engagement / day", type: "line", dataset: "third", dimension: "date" },
{ chart: "c1", name: "New followers by type", type: "pie", dataset: "newfollowerdist", dimension: "key", metric: "val" },
{ chart: "c2", dimension2: "type", name: "Lost and new followers", type: "linemulti", dataset: "newfollowerdelta", date: true, dimension: "date", metric: "value", graphs: ["Organic", "Paid"] },
],
clickCount: [
{ chart: "c0", name: "Clicks / day", type: "line", dataset: "third", dimension: "date" },
{ chart: "c1", name: "New followers by type", type: "pie", dataset: "newfollowerdist", dimension: "key", metric: "val" },
{ chart: "c2", dimension2: "type", name: "Lost and new followers", type: "linemulti", dataset: "newfollowerdelta", date: true, dimension: "date", metric: "value", graphs: ["Organic", "Paid"] },
],
impressionCount: [
{ chart: "c0", name: "Impressions / day", type: "line", dataset: "third", dimension: "date" },
{ chart: "c1", name: "New followers by type", type: "pie", dataset: "newfollowerdist", dimension: "key", metric: "val" },
{ chart: "c2", dimension2: "type", name: "Lost and new followers", type: "linemulti", dataset: "newfollowerdelta", date: true, dimension: "date", metric: "value", graphs: ["Organic", "Paid"] },
],
commentCount: [
{ chart: "c0", name: "Comments / day", type: "line", dataset: "third", dimension: "date" },
{ chart: "c1", name: "New followers by type", type: "pie", dataset: "newfollowerdist", dimension: "key", metric: "val" },
{ chart: "c2", dimension2: "type", name: "Lost and new followers", type: "linemulti", dataset: "newfollowerdelta", date: true, dimension: "date", metric: "value", graphs: ["Organic", "Paid"] },
],
},
},
network: {
name: "Network",
view: "network",
first: { dimension: "title", report: "networksize" },
second: { dimension: "title", report: "followerstatistics" },
selectedmetric: "val",
metrics: ["val"],
table: { dataset: "region" },
charts: {
val: [
{ chart: "c0", name: "Followers / Staff size", type: "bar", dataset: "staffcount", dimension: "key", metric: "val" },
{ chart: "c1", name: "Followers / Role", type: "pie", dataset: "seniority", dimension: "key", metric: "val" },
{ chart: "c2", name: "Followers / Country", type: "progress", dataset: "country", dimension: "key", metric: "val" },
{ chart: "c3", name: "Folowers / Industry", type: "progress", dataset: "industry", dimension: "key", metric: "val" },
{ chart: "c4", name: "Folowers / Function", type: "progress", dataset: "function", dimension: "key", metric: "val" },
],
},
},
// NEW VIEW ADDED FOR VISITORS
visitors: {
hide: true,
name: "Visitors",
view: "visitors",
first: { dimension: "title", report: "visitorstatistics" },
selectedmetric: "val",
metrics: ["pageViews", "uniqueVisitors"],
panels: { dataset: "first" },
table: { dataset: "country" },
charts: {
val: [
{ chart: "c0", name: "Visitors / Seniority", type: "pie", dataset: "seniority", dimension: "key", metric: "val" },
{ chart: "c1", name: "Visitors / Industry", type: "bar", dataset: "industry", dimension: "key", metric: "val" },
{ chart: "c2", name: "Visitors / Staff Size", type: "progress", dataset: "staffcount", dimension: "key", metric: "val" },
],
},
},
posts: {
name: "Posts",
view: "posts",
first: { dimension: "title", report: "posts" },
selectedmetric: "Placeholder",
dimension: "title",
metrics: ["open_rate", "click_rate", "emails_sent", "unsubscribed", "bounces", "total_orders", "total_revenue"],
panels: { dataset: "stats" },
charts: {
Placeholder: [
{ chart: "c0", name: "Emails sent", type: "line", dataset: "timeseries", metric: "emails_sent", dimension: "date" },
{ chart: "c1", name: "Emails opened", type: "line", dataset: "timeseries", metric: "unique_opens", dimension: "date" },
{ chart: "c2", name: "Clicks", type: "line", dataset: "timeseries", metric: "recipient_clicks", dimension: "date" },
],
},
},
personalposts: {
hide: true,
name: "Personal Posts",
view: "personalposts",
hide: true,
first: { dimension: "title", report: "personalposts" },
selectedmetric: "Placeholder",
dimension: "title",
metrics: ["open_rate", "click_rate", "emails_sent", "unsubscribed", "bounces", "total_orders", "total_revenue"],
panels: { dataset: "stats" },
charts: {
Placeholder: [
{ chart: "c0", name: "Emails sent", type: "line", dataset: "timeseries", metric: "emails_sent", dimension: "date" },
{ chart: "c1", name: "Emails opened", type: "line", dataset: "timeseries", metric: "unique_opens", dimension: "date" },
{ chart: "c2", name: "Clicks", type: "line", dataset: "timeseries", metric: "recipient_clicks", dimension: "date" },
],
},
},
},
},
};
$controller("coreapi", { $scope: $scope });
$scope.init();
},
]);
app.controller('linkedinxads', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.pageowl.app/rest/linkedinxads/connect?websiteid=' + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: '',
urlbase: 'linkedinxads',
report_endpoint: '/rest/linkedinxads/report',
}
$scope.gloMetrics = h.metrics('linkedinxads');
/*
$scope.gloMetrics = {
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' },
};
*/
//'page_fans','page_fans_country','page_fans_by_like_source','page_fans_gender_age'
$scope.reportconfig = {
campaigns: {
views: {
all: {
name: 'Campagnes',
view: 'all',
first: { dimension: 'date', report: 'campaigns-time' },
second: { dimension: 'pivotValues', report: 'campaigns-all' },
selectedmetric: 'impressions' ,
selectedmetrics: { 'impressions': true, 'clicks': true, 'oneClickLeads': true, 'costInLocalCurrency': true, 'externalWebsiteConversions': true },
metrics: ['impressions', 'clicks', 'oneClickLeads','costInLocalCurrency','externalWebsiteConversions'],
panels: { dataset: 'second' },
table: { dataset: 'second' },
charts: {
impressions: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.impressions')+' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.impressions')+' / Campaign', type: 'bar', dataset: 'second' }],
clicks: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.clicks')+' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.clicks')+' / Campaign', type: 'bar', dataset: 'second' }],
oneClickLeads: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.clicks')+' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.clicks')+' / Campaign', type: 'bar', dataset: 'second' }],
costInLocalCurrency: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.impressions')+' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.impressions')+' / Campaign', type: 'bar', dataset: 'second' }],
externalWebsiteConversions: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.cpc')+' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpc')+' / Campaign', type: 'bar', dataset: 'second' }],
cpm: [{ 'chart': 'c1', 'name': $filter('translate')('metrics.cpm')+' / Day', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': $filter('translate')('metrics.cpm')+' / Campaign', type: 'bar', dataset: 'second' }],
},
},
network: {
name: 'Campagnes',
view: 'network',
//first: { dimension: 'title', 'report': 'networksize' },
second: { dimension: 'title', 'report': 'followerstatistics' },
selectedmetric: 'val',
metrics: ['val'],
table: { dataset: 'region' },
charts: {
val: [
{ 'chart': 'c0', 'name': 'Followers / Staff size', type: 'bar', dataset: 'staffcount', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c1', 'name': 'Followers / Role', type: 'pie', dataset: 'seniority', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c2', 'name': 'Followers / Country', type: 'progress', dataset: 'country', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c3', 'name': 'Folowers / Industry', type: 'progress', dataset: 'industry', 'dimension': 'key', 'metric': 'val' },
{ 'chart': 'c4', 'name': 'Folowers / Function', type: 'progress', dataset: 'function', 'dimension': 'key', 'metric': 'val' },
],
},
},
posts: {
name: 'Campagne',
view: 'posts',
first: { dimension: 'title', 'report': 'posts', },
selectedmetric: 'Placeholder',
dimension: 'title',
metrics: ['open_rate', 'click_rate', 'emails_sent', 'unsubscribed', 'bounces', 'total_orders', 'total_revenue'],
panels: { dataset: 'stats' },
charts: {
Placeholder: [
{ 'chart': 'c0', 'name': 'Emails sent', type: 'line', dataset: 'timeseries', metric: 'emails_sent', dimension: 'date' },
{ 'chart': 'c1', 'name': 'Emails opened', type: 'line', dataset: 'timeseries', metric: 'unique_opens', dimension: 'date' },
{ 'chart': 'c2', 'name': 'Clicks', type: 'line', dataset: 'timeseries', metric: 'recipient_clicks', dimension: 'date' },
],
},
},
personalposts: {
hide: true,
name: 'Campagne',
view: 'personalposts',
hide: true,
first: { dimension: 'title', 'report': 'personalposts', },
selectedmetric: 'Placeholder',
dimension: 'title',
metrics: ['open_rate', 'click_rate', 'emails_sent', 'unsubscribed', 'bounces', 'total_orders', 'total_revenue'],
panels: { dataset: 'stats' },
charts: {
Placeholder: [
{ 'chart': 'c0', 'name': 'Emails sent', type: 'line', dataset: 'timeseries', metric: 'emails_sent', dimension: 'date' },
{ 'chart': 'c1', 'name': 'Emails opened', type: 'line', dataset: 'timeseries', metric: 'unique_opens', dimension: 'date' },
{ 'chart': 'c2', 'name': 'Clicks', type: 'line', dataset: 'timeseries', metric: 'recipient_clicks', dimension: 'date' },
],
},
},
},
},
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('mailchimp', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.marketingtracer.com/rest/mailchimp/connect?websiteid=' + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: '',
urlbase: 'mailchimp',
report_endpoint: '/rest/mailchimp/report',
}
$scope.gloMetrics = h.metrics('mailchimp');
/*
$scope.gloMetrics = {
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': '€' },
};
*/
//'page_fans','page_fans_country','page_fans_by_like_source','page_fans_gender_age'
$scope.reportconfig = {
mail: {
views: {
campaigns: {
name: 'Campagnes',
view: 'campaigns',
second: { dimension: 'title', 'report': 'campaigns' },
selectedmetric: 'member_count',
dimension: 'title',
metrics: ['open_rate', 'click_rate', 'emails_sent', 'send_time', 'unsubscribed', 'bounces'],
table: { dataset: 'second' },
charts: {},
},
campaign: {
name: 'Campagne',
view: 'campaign',
hide: true,
first: { dimension: 'title', 'report': 'campaign.sent_to', filterexpression: $scope.urlfilter },
second: { dimension: 'title', 'report': 'campaign', filterexpression: $scope.urlfilter },
selectedmetric: 'Placeholder',
dimension: 'title',
metrics: ['open_rate', 'click_rate', 'emails_sent', 'unsubscribed', 'bounces', 'total_orders', 'total_revenue'],
panels: { dataset: 'stats' },
charts: {
Placeholder: [
{ 'chart': 'c0', 'name': 'Emails sent', type: 'line', dataset: 'timeseries', metric: 'emails_sent', dimension: 'date' },
{ 'chart': 'c1', 'name': 'Emails opened', type: 'line', dataset: 'timeseries', metric: 'unique_opens', dimension: 'date' },
{ 'chart': 'c2', 'name': 'Clicks', type: 'line', dataset: 'timeseries', metric: 'recipient_clicks', dimension: 'date' },
],
},
},
lists: {
name: 'Lijsten',
view: 'lists',
second: { dimension: 'name', 'report': 'lists' },
selectedmetric: 'member_count',
dimension: 'name',
metrics: ['member_count', 'click_rate', 'open_rate', 'list_rating'],
table: { dataset: 'second' },
charts: {},
},
list: {
name: 'List',
view: 'list',
hide: true,
first: { dimension: 'title', 'report': 'list', filterexpression: $scope.urlfilter },
second: { dimension: 'title', 'report': 'list.activity', filterexpression: $scope.urlfilter },
third: { dimension: 'title', 'report': 'list.members', filterexpression: $scope.urlfilter },
selectedmetric: 'Placeholder',
dimension: 'title',
metrics: ['open_rate', 'click_rate', 'member_count', 'unsubscribe_count', 'cleaned_count'],
panels: { dataset: 'first' },
charts: {
Placeholder: [
{ 'chart': 'c0', 'name': 'Emails sent', type: 'line', dataset: 'second', metric: 'emails_sent', dimension: 'date' },
{ 'chart': 'c1', 'name': 'Emails opened', type: 'line', dataset: 'second', metric: 'unique_opens', dimension: 'date' },
{ 'chart': 'c2', 'name': 'Clicks', type: 'line', dataset: 'second', metric: 'recipient_clicks', dimension: 'date' },
],
},
},
},
},
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('mybusiness', ['$scope', '$timeout', '$http', '$rootScope', '$controller', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: 'https://app.marketingtracer.com/rest/mybusiness/connect?websiteid=' + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: '',
urlbase: 'mybusiness',
report_endpoint: '/rest/mybusiness/report',
}
$scope.gloMetrics = h.metrics('mybusiness');
/* $scope.gloMetrics = {
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': '€' },
};
*/
$scope.fnFormatAd = function(d) {
return d.HeadlinePart1 + d.HeadlinePart2 + d.Description + d.CreativeDestinationUrl + d.CreativeFinalUrls;
}
$scope.reportconfig = {
business: {
views: {
all: {
name: 'Overzicht',
view: 'all',
first: { report: 'total' },
second: { report: 'daily' },
selectedmetric: 'Placeholder',
metrics: [],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
Placeholder: [
{ 'chart': 'c1', 'name': 'Search type', type: 'bar', dataset: 'queries', 'dimension': 'type', 'metric': 'queries' },
{ 'chart': 'c2', 'name': 'Searches / day', type: 'pie', type: 'line', dataset: 'second', metric: 'QUERIES', dimension: 'date' },
{ 'chart': 'c3', 'name': 'Impressions / type', type: 'bar', dataset: 'views', 'dimension': 'type', 'metric': 'views' },
{ 'chart': 'c4', 'name': 'Impressions / day', type: 'pie', type: 'line', dataset: 'second', metric: 'VIEWS', dimension: 'date' },
{ 'chart': 'c5', 'name': 'Available Photo\'s', type: 'pie', dataset: 'photocount', 'dimension': 'type', 'metric': 'photocount' },
{ 'chart': 'c6', 'name': 'Photo Impressions', type: 'pie', dataset: 'photoview', 'dimension': 'type', 'metric': 'photoview' },
{ 'chart': 'c7', 'name': 'Website clicks', type: 'pie', type: 'line', dataset: 'second', metric: 'ACTIONS_WEBSITE', dimension: 'date' },
{ 'chart': 'c8', 'name': 'Phone contact', type: 'pie', type: 'line', dataset: 'second', metric: 'ACTIONS_PHONE', dimension: 'date' },
{ 'chart': 'c9', 'name': 'Driving Directions', type: 'pie', type: 'line', dataset: 'second', metric: 'ACTIONS_DRIVING_DIRECTIONS', dimension: 'date' },
],
},
},
reviews: {
name: 'Reviews',
view: 'reviews',
first: { report: 'reviews' },
selectedmetric: 'Placeholder',
metrics: [],
table: { dataset: 'second' },
panels: { dataset: 'second' },
charts: {
Placeholder: [
{ 'chart': 'c1', 'name': 'Search type', type: 'bar', dataset: 'queries', 'dimension': 'type', 'metric': 'queries' },
{ 'chart': 'c2', 'name': 'Zoekopdrachten / day', type: 'pie', type: 'line', dataset: 'second', metric: 'QUERIES', dimension: 'date' },
{ 'chart': 'c3', 'name': 'Impressions / type', type: 'bar', dataset: 'views', 'dimension': 'type', 'metric': 'views' },
{ 'chart': 'c4', 'name': 'Impressions / day', type: 'pie', type: 'line', dataset: 'second', metric: 'VIEWS', dimension: 'date' },
{ 'chart': 'c5', 'name': 'Photo\'s', type: 'pie', dataset: 'photocount', 'dimension': 'type', 'metric': 'photocount' },
{ 'chart': 'c6', 'name': 'Photo Impressions', type: 'pie', dataset: 'photoview', 'dimension': 'type', 'metric': 'photoview' },
{ 'chart': 'c7', 'name': 'Website clicks', type: 'pie', type: 'line', dataset: 'second', metric: 'ACTIONS_WEBSITE', dimension: 'date' },
{ 'chart': 'c8', 'name': 'Phone contact', type: 'pie', type: 'line', dataset: 'second', metric: 'ACTIONS_PHONE', dimension: 'date' },
{ 'chart': 'c9', 'name': 'Driving Directions', type: 'pie', type: 'line', dataset: 'second', metric: 'ACTIONS_DRIVING_DIRECTIONS', dimension: 'date' },
],
},
},
},
}
}
$controller('coreapi', { $scope: $scope });
$scope.init();
}
]);
app.controller('keywordtool', ['$scope', '$http', '$rootScope', '$filter', '$location', 'h',
function($scope, $http, $rootScope, $filter, $location, h) {
$scope.suggest = { 'q': '', result: [] };
$scope.order = {};
$scope.fnOrder = function(field) {
var reverse = (field == $scope.order.field) ? true : false;
$scope.order = { 'field': field, 'reverse': reverse };
}
var lang = {
"de": [
{ key: 'was', 'value': 'was ', 'type': 'vraag' },
{ key: 'was ist', 'value': 'was ist ', 'type': 'vraag' },
{ key: 'wie', 'value': 'wie ', 'type': 'vraag' },
{ key: 'wann', 'value': 'wann ', 'type': 'vraag' },
{ key: 'welche', 'value': 'welche ', 'type': 'vraag' },
{ key: 'warum', 'value': 'warum ', 'type': 'vraag' },
],
"nl": [
{ key: 'wat', 'value': 'wat ', 'type': 'vraag' },
{ key: 'wat is', 'value': 'wat is ', 'type': 'vraag' },
{ key: 'hoe', 'value': 'hoe ', 'type': 'vraag' },
{ key: 'wanneer', 'value': 'wanneer ', 'type': 'vraag' },
{ key: 'welke', 'value': 'welke ', 'type': 'vraag' },
{ key: 'waarom', 'value': 'waarom ', 'type': 'vraag' },
],
"en": [
{ key: 'what', 'value': 'what ', 'type': 'vraag' },
{ key: 'what is', 'value': 'what is ', 'type': 'vraag' },
{ key: 'how', 'value': 'how ', 'type': 'vraag' },
{ key: 'when', 'value': 'when ', 'type': 'vraag' },
{ key: 'which', 'value': 'which ', 'type': 'vraag' },
{ key: 'why', 'value': 'why ', 'type': 'vraag' },
]
}
var letters = [
{ key: 'a', 'value': ' a' },
{ key: 'b', 'value': ' b' },
{ key: 'c', 'value': ' c' },
{ key: 'd', 'value': ' d' },
{ key: 'e', 'value': ' e' },
{ key: 'f', 'value': ' f' },
{ key: 'g', 'value': ' g' },
{ key: 'h', 'value': ' h' },
{ key: 'i', 'value': ' i' },
{ key: 'j', 'value': ' j' },
{ key: 'k', 'value': ' k' },
{ key: 'l', 'value': ' l' },
{ key: 'm', 'value': ' m' },
{ key: 'n', 'value': ' n' },
{ key: 'o', 'value': ' o' },
{ key: 'p', 'value': ' p' },
{ key: 'q', 'value': ' q' },
{ key: 'r', 'value': ' r' },
{ key: 's', 'value': ' s' },
{ key: 't', 'value': ' t' },
{ key: 'u', 'value': ' u' },
{ key: 'v', 'value': ' v' },
{ key: 'w', 'value': ' w' },
{ key: 'x', 'value': ' x' },
{ key: 'y', 'value': ' y' },
{ key: 'z', 'value': ' z' },
]
$scope.letters = lang[$rootScope.user.site.locale.lang].concat(letters);
var current = { 'key': '', value: '' }
$scope.fnTrackKeyword = function(kw) {
$http.post('/rest/rank-tracker/add', {
keywords: kw,
websiteid: h.siteId()
}).then(function successCallback(res) {
});
}
$scope.fnExploreKeyword = function(q) {
$scope.q = q;
$scope.showsuggesties = true;
$scope.suggest.result = [];
current = { 'key': 'base', value: '' }
$http.get('/rest/keywordtool/get', {
params: {
q: $scope.q,
l: 'long',
}
}).then(function successCallback(res) {
$scope.suggest.result['base'] = res.data;
});
$scope.kwplanner = false;
var next = false;
var go = false;
var mbreak = true;
$http.get('/rest/googlexads/keywordplanner', {
params: {
q: $scope.q,
cc: $scope.user.site.locale.cc,
lc: $scope.user.site.locale.lc,
}
}).then(function successCallback(res) {
$scope.kwplanner = res.data;
});
angular.forEach($scope.letters, function(value, key) {
if (value.type == 'vraag') {
$http.get('/rest/keywordtool/get', {
params: {
q: value.value + $scope.q,
cc: $scope.user.site.locale.cc,
lc: $scope.user.site.locale.lc,
}
}).then(function successCallback(res) {
if (res.data[1]) {
$scope.suggest.result[value.key] = res.data;
}
});
} else {
$http.get('/rest/keywordtool/get', {
params: {
q: $scope.q + value.value,
cc: $scope.user.site.locale.cc,
lc: $scope.user.site.locale.lc,
}
}).then(function successCallback(res) {
if (res.data[1]) {
$scope.suggest.result[value.key] = res.data;
}
});
}
});
}
$scope.fnDownload = function(text) {
var text = '';
for (var i in $scope.suggest.result) {
var res = $scope.suggest.result[i];
for (var y in res) {
var woord = res[y];
if (woord.check) {
console.log(woord);
text = text + woord.r + "\r\n";
}
}
}
for (var i in $scope.kwplanner) {
if ($scope.kwplanner[i].check) {
text = text + $scope.kwplanner[i].keyword + " (" + $scope.kwplanner[i].vol + "," + $scope.kwplanner[i].seo + "," + $scope.kwplanner[i].cpc + ")\r\n";
}
}
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', 'zoekwoord suggesties voor ' + $scope.q + '.txt');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
}
]);
app.controller('onpagestatus', ['$scope', '$timeout', '$http', '$rootScope', '$location', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $location, $filter, h) {
$scope.page = {};
var _cb = {
getstarted: function() {
$scope.page.include = 'index.getstarted';
},
crawling: function() {
$scope.page.crawling = true;
$scope.page.include = 'index.list';
},
ready: function() {
$scope.page.include = 'index.list';
}
}
var timeout = 5000
var getStatus = function() {
if ($rootScope.onpagestatus && $rootScope.onpagestatus.data.status == 'done') {
_cb[$rootScope.onpagestatus.cb]($rootScope.onpagestatus.data)
} else {
$http.get('/rest/onpage/status', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
$rootScope.onpagestatus = res.data;
_cb[res.data.cb](res.data);
if ($rootScope.onpagestatus && $rootScope.onpagestatus.data.status == 'crawling') {
$scope.$broadcast('refreshonpagelist');
$timeout.cancel($rootScope.to);
$rootScope.to = $timeout(function() {
timeout = timeout * 1.2;
getStatus();
}, timeout);
}
});
}
}
getStatus();
$scope.fnCrawl = function() {
$http.get('/rest/onpage/crawl', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
$rootScope.onpagestatus.data.status = 'crawling';
$timeout(function() {
getStatus();
}, 2000);
$location.path('/onpage-seo/all');
});
}
$scope.fnEmpty = function() {
$http.get('/rest/onpage/empty', {
params: {
websiteid: h.siteId(),
}
});
}
}
]);
app.filter('zeroplus', function() {
return function(items, timingevent) {
return items.reduce(function(acc, cur) {
if (typeof cur.properties == 'object' && typeof cur.properties.timing == 'object' && cur.properties.timing[timingevent] > 0) {
acc.push(cur);
}
return acc;
}, []);
};
});
app.controller('onpagelist', ['$scope', '$timeout', '$http', '$rootScope', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $filter, h) {
$scope.page = { 'curllabel': '', 'newurl': $rootScope.user.site.domain + '/' };
$scope.page.table = {
'curpage': 0,
'itemsperpage': 34,
'offset': 0,
'rulefilter': 'all',
'filter': {},
'min': { 'opi': 100, 'score': 100 },
'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);
}
}
$scope.fnPctChange = function(o, n) {
if (n == 0) {
return '0% ';
}
var pct = ((n - o) / o) * 100;
var npct = $filter('number')(pct, 2);
if(!isFinite(npct)){
npct = 100;
}
if (pct < 0) {
return '' + npct + '% ';
}
return '+ ' + npct + '% ';r
}
$scope.fnNewLabel = function(label) {
if ($scope.page.curlabel != label) {
$scope.page.curlabel = label;
return true;
}
return false;
}
// filter pages by rule
$scope.fnFilterRule = function(ruleName) {
$scope.page.table.fnOrder('properties.opi', true)
var pages = angular.copy($rootScope._pages);
pages = $filter('filter')(pages, $scope.page.table.filter);
$scope.rule = $scope.onpage.rules[ruleName];
$scope.pages = $filter('filter')(pages, function(v) { return (v.rules[ruleName] === false) });
$scope.view = 'pages';
var stats = { 'done': 0, 'open': 0, 'total': 0, 'pages': 0 };
for (var i in pages) {
if (typeof pages[i]['rules'][ruleName] != 'undefined') {
stats['pages']++;
stats['total']++;
stats['done'] = (pages[i]['rules'][ruleName]) ? stats['done'] + 1 : stats['done'];
stats['open'] = (pages[i]['rules'][ruleName]) ? stats['open'] : stats['open'] + 1;
}
}
$scope.stats = stats;
}
var fnChanges = {
init: function() {
$http.get('/rest/onpage/changes', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
// filter in controller => data en url weergave
//$scope.changes = $filter('orderBy')(res.data.data,['-date','-url']);
$scope.changes = res.data.data;
$scope.page.table.fnOrder('date', true)
fnChart($rootScope._history, 'all');
fnChanges.stats();
fnChanges.group('date');
});
},
stats: function() {
var stats = [{ 'label': 'change', 'value': 0, 'color': '#808080' }, { 'label': 'done', 'value': 0, 'color': '#4caf50' }, { 'label': 'issue', 'value': 0, 'color': '#ff9800' }];
var totals = {};
for (var i in $scope.changes) {
if ($scope.changes[i].r === true) {
stats[1].value += 1;
} else if ($scope.changes[i].r === false) {
stats[2].value += 1;
} else {
stats[0].value += 1;
}
totals[$scope.changes[i].url] = totals[$scope.changes[i].url] + 1 || 1
}
// total changes etc per page
$scope.page.totals = [];
for (var i in totals) {
$scope.page.totals.push({ 'url': i, 'count': totals[i] })
}
$scope.page.totals = $filter('orderBy')($scope.page.totals, 'count', true).slice(0, 5);
$scope.page.stats = stats;
$scope.piechart = {
data: stats,
type: 'pie',
"precision": 0,
"decimalSeparator": ",",
"thousandsSeparator": ".",
legend: {
enabled: false,
position: 'right',
autoMargins: false,
marginRight: 100,
},
titleField: 'label',
valueField: 'value',
"colorField": "color",
labelRadius: 5,
radius: '42%',
innerRadius: '60%',
labelText: '[[label]]',
}
},
group: function(key) {
var groups = $scope.changes.reduce(function(acc, cur) {
acc[cur.date] = acc[cur.date] || { 'key': new Date(cur.date).toLocaleString('default', { year: 'numeric', 'month': 'long', day: 'numeric' }), 'letter': new Date(cur.date).toLocaleString('default', { 'month': 'short' }).charAt(0).toUpperCase(), 'items': [] };
acc[cur.date].items.push(cur);
return acc;
}, {});
$scope.groups = $filter('orderBy')(Object.values(groups), 'date', false);
}
}
var fnSpeed = function(pages) {
var t = {};
var s = {};
// min max
pages.forEach(function(page, k) {
if (typeof page.properties.timing != 'undefined' && page.properties.timing != null && page.properties.timing.ttfb) {
var _otiming = page.properties.timing;
delete _otiming.cls;
Object.keys(_otiming).forEach(function(timingevent, n) {
s[timingevent] = s[timingevent] || []
if (_otiming[timingevent] > 0) {
s[timingevent].push(_otiming[timingevent])
}
})
}
})
for (timingevent in s) {
var v = s[timingevent];
var min = 0;
var max = Math.max(...v);
var step = Math.round((max - min) / 18);
if (step == 0) {
step = 0.1;
}
var oinit = {}
for (var i = min; i < max; i = i + step) {
oinit[Math.round(i / step) * step] = 0;
}
t[timingevent] = v.reduce(function(acc, cur) {
acc[Math.round(cur / step) * step] = acc[Math.round(cur / step) * step] + 1;
return acc;
}, oinit)
}
// get events from keys
$scope.timingevents = Object.keys(t);
var colors = h.colors()
// create charts
$scope.charts = {};
Object.keys(t).forEach(function(timingevent, k) {
var data = Object.keys(t[timingevent]).map(function(a) {
return { 'timing': a, 'pages': t[timingevent][a] }
})
$scope.charts[timingevent] = {
data: data,
type: 'serial',
categoryField: 'timing',
rotate: false,
legend: {
enabled: false
},
categoryAxis: {
gridPosition: "start",
parseDates: false,
labelRotation: 0,
gridThickness: 0,
labelFunction: function(value, valueText, axis) { return value + 'ms' },
},
valueAxes: [{
position: "left",
axisAlpha: 0,
labelsEnabled: true,
dashLength: 4,
color: '#dedede',
}],
graphs: [{
fillColorsField: "color",
balloonText: "" + timingevent + " [[category]]ms: [[value]] pages ",
type: "column",
columnWidth: 0.90,
title: "Zoekwoord waarde",
valueField: 'pages',
fillAlphas: 1,
lineColor: colors[k],
autoColor: true,
}],
}
})
}
// AI-crawlers: status per bot voor de site en de pagina's met een blokkade
$scope.oAiBots = {
'loading': false,
'data': null,
init: function(refresh) {
var self = this;
self.loading = true;
$http.get('/rest/onpage/aibots', {
params: {
websiteid: h.siteId(),
refresh: refresh ? 1 : 0
}
}).then(function successCallback(res) {
self.data = res.data;
self.rows = self.fnRows(res.data);
self.loading = false;
}, function errorCallback() {
self.loading = false;
});
},
fnBlocked: function(type) {
return (this.data && this.data.bots || []).filter(function(b) {
return b.type == type && (b.robots_pages > 0 || b.server_blocked);
});
},
// één rij per pagina: AI-signalen en geblokkeerde AI-bots samen, zoals de paginatabel van de scan
'rows': [],
'table': {
'order': '-score', 'filter': { 'url': '' }, 'level': '',
// paginering zoals de paginatabel van de scan
'curpage': 1, 'itemsperpage': 34, 'offset': 0,
fnSetOffset: function() {
this.offset = this.itemsperpage * (this.curpage - 1);
},
fnReset: function() {
this.curpage = 1;
this.offset = 0;
}
},
fnRows: function(data) {
var map = {};
var rows = [];
var row = function(p) {
var id = p._id.$oid;
if (!map[id]) {
map[id] = { '_id': p._id, 'url': p.url, 'score': null, 'level': null, 'top': [], 'blocked': [], 'search': false };
rows.push(map[id]);
}
return map[id];
};
((data.aicontent && data.aicontent.pages) || []).forEach(function(p) {
var r = row(p);
r.score = p.score;
r.level = p.level;
r.top = p.top;
});
(data.pages || []).forEach(function(p) {
var r = row(p);
r.blocked = p.blocked;
r.search = p.search;
});
return rows;
},
fnOrder: function(key) {
this.table.order = (this.table.order == '-' + key) ? key : '-' + key;
this.table.fnReset();
},
fnLevel: function(r) {
var level = $scope.oAiBots.table.level;
return !level || r.level == level;
},
// status van een bot in één woord: robots.txt, server of toegestaan
fnBotStatus: function(b) {
if (b.robots_home) { return 'robots_site'; }
if (b.server_checked && this.data.conclusive && b.server_blocked) { return 'server'; }
if (b.robots_pages > 0) { return 'robots_pages'; }
return 'allowed';
}
};
// calculare rules and scores
var fnDuplicates = function(pages) {
var duplicates = {};
for (var i in pages) {
for (var y in pages[i]['compare']) {
duplicates[y] = duplicates[y] || {};
duplicates[y][pages[i]['compare'][y]] = duplicates[y][pages[i]['compare'][y]] || { 'name': pages[i]['compare'][y], 'count': 0, 'pages': [] };
duplicates[y][pages[i]['compare'][y]].count++;
duplicates[y][pages[i]['compare'][y]].pages.push({ 'serps': pages[i].serps, 'url': pages[i].url, 'properties': pages[i].properties, '_id': pages[i]._id });
}
}
// remove single counts in compare
for (var i in duplicates) {
for (var y in duplicates[i]) {
if (duplicates[i][y].count <= 1) {
delete duplicates[i][y];
}
}
}
var x = {}
for (var i in duplicates) {
x[i] = Object.values(duplicates[i])
}
$scope.duplicates = x;
}
// assistent
var fnAssistent = function(pages) {
$http.get('/rest/onpage/weights').then(function successCallback(weights) {
var issues = [];
for (var i in pages) {
for (var y in pages[i]['rules']) {
if (!pages[i]['rules'][y]) {
if (pages[i]['properties']['pagerank'] && pages[i]['properties']['opi'] && weights.data[y]) {
issues.push({ 'help': false, 'page': pages[i], 'weight': parseFloat(weights.data[y]['f'] * (200 + pages[i]['properties']['pagerank']) * (200 + pages[i]['properties']['opi'])), 'rule': y, 'f': weights.data[y]['f'] })
}
}
}
}
$scope.issues = issues;
});
}
// alerts
$scope.oAlerts = {
init: function() {
$scope.page.notifications = 'pending';
this.getSettings();
$timeout(function() { $scope.oAlerts.firebase.init(); }, 4000)
},
firebase: {
'config': {
apiKey: "AIzaSyAKgRy2MQF2OUPh-WpUYMWOU6M_XijIMxg",
authDomain: "marketingtracer-73fb0.firebaseapp.com",
databaseURL: "https://marketingtracer-73fb0.firebaseio.com",
projectId: "marketingtracer-73fb0",
storageBucket: "marketingtracer-73fb0.appspot.com",
messagingSenderId: "769511864713",
appId: "1:769511864713:web:340f1f811287abb766c958"
},
'init': function() {
firebase.initializeApp(this.config);
this.messaging = firebase.messaging();
this.messaging.usePublicVapidKey("BDRyazo-zYOixCGZOS-ZI87KPiGfDCCFr5J-GMvydQkBVXOU9RaV8dolUZt_9RT1ZXk2WQ54lwKulSXn-r_B3UY");
if (!("Notification" in window)) {
$scope.page.notifications = 'notenabled';
} else if (Notification.permission == "granted") {
this.getToken();
} else {
$scope.page.notifications = 'notgranted';
}
},
'getToken': function() {
this.messaging.getToken().then(function(token) {
$scope.token = token;
console.log($scope.alertnotifications);
if ($scope.alertnotifications.indexOf(token) > -1) {
$scope.page.notifications = 'granted';
} else {
$scope.page.notifications = 'available';
}
$scope.$apply();
console.log("token is : ", token)
})
},
unsubscribe: function() {
$scope.page.notifications = 'available';
$http.post('/rest/onpage/alertsettings.notifications.pull', {
websiteid: h.siteId(),
'token': $scope.token
});
},
subscribe: function() {
$scope.page.notifications = 'granted';
$http.post('/rest/onpage/alertsettings.notifications.save', {
websiteid: h.siteId(),
'token': $scope.token
});
},
'requestPermission': function() {
this.messaging
.requestPermission()
.then(function() {
console.log("Notification permission granted.");
$scope.page.notifications = 'granted';
$scope.$apply();
// get the token in the form of promise
return messaging.getToken()
})
.then(function(token) {
// console.log("token is : ", token)
$http.post('/rest/onpage/alertsettings.notifications.save', {
websiteid: h.siteId(),
'token': token
});
})
.catch(function(err) {
$scope.page.notifications = 'notenabled';
//console.log("Unable to get permission to notify.", err);
});
this.messaging.onMessage(function(payload) {
var notification = new Notification(payload.notification.title, payload.notification);
console.log("Message received. ", payload);
});
},
},
getSettings: function() {
// hardcoded ruleset
$scope.alertrules = [
{ 'rule': 'title_change' },
{ 'rule': 'pfirst_change' },
{ 'rule': 'metadescription_change' },
{ 'rule': 'h1_change' },
{ 'rule': 'title_removed' },
{ 'rule': 'h1_removed' },
{ 'rule': 'metadescription_removed' },
{ 'rule': 'not_indexable' },
{ 'rule': 'status_change' },
{ 'rule': 'total_score_change' },
{ 'rule': 'lighthouse_fail_change' },
];
// users, names and settings
$http.get('/rest/sites/get', {
params: {
'websiteid': h.siteId(),
}
}).then(function successCallback(res) {
if (res.data._id) {
$scope.user.site.users = res.data.users;
$scope.user.site.names = res.data.names;
$scope.alertnotifications = res.data.alertnotifications || [];
$scope.alertsettings = angular.extend({
'title_change': { 'sens': 0, 'users': [], 'active': false },
'pfirst_change': { 'sens': 0, 'users': [], 'active': false },
'metadescription_change': { 'sens': 0, 'users': [], 'active': false },
'h1_change': { 'sens': 0, 'users': [], 'active': false },
'title_removed': { 'sens': 0, 'users': [], 'active': false },
'h1_removed': { 'sens': 0, 'users': [], 'active': false },
'metadescription_removed': { 'sens': 0, 'users': [], 'active': false },
}, res.data.alertsettings);
}
});
},
saveSettings: function() {
$http.post('/rest/onpage/alertsettings.save', {
websiteid: h.siteId(),
'alertsettings': $scope.alertsettings
});
},
addUser: function(userid, rulename) {
var count = 0;
$scope.alertsettings[rulename].users = $scope.alertsettings[rulename].users || [];
for (var i = 0; i < $scope.alertsettings[rulename].users.length; i++) {
if ($scope.alertsettings[rulename].users[i].$oid == userid.$oid) {
$scope.alertsettings[rulename].users.splice(i, 1);
count = count + 1;
}
}
if (count == 0) {
$scope.alertsettings[rulename].users.push(userid);
}
this.saveSettings();
}
}
// calculare rules and scores
var fnRules = function(pages) {
var stats = { 'done': 0, 'open': 0, 'total': 0, 'pages': 0 };
var rules = {};
var compare = {};
for (var i in pages) {
stats['pages']++;
for (var y in pages[i]['rules']) {
if ($scope.onpage.rules[y]) {
stats['total']++;
stats['done'] = (pages[i]['rules'][y]) ? stats['done'] + 1 : stats['done'];
stats['open'] = (pages[i]['rules'][y]) ? stats['open'] : stats['open'] + 1;
rules[y] = rules[y] || { 'done': 0, 'open': 0, 'total': 0 }
rules[y]['done'] = (pages[i]['rules'][y]) ? rules[y]['done'] + 1 : rules[y]['done'];
rules[y]['open'] = (pages[i]['rules'][y]) ? rules[y]['open'] : rules[y]['open'] + 1;
rules[y]['total']++;
rules[y]['name'] = y;
}
}
}
$scope.rules = rules;
$scope.stats = stats;
}
var fnChart = function(history, field) {
$scope.chart = {
data: history,
type: 'serial',
theme: 'light',
marginLeft: 0,
marginRight: 0,
marginTop: 10,
fontFamily: 'Roboto',
categoryField: 'date',
rotate: false,
pathToImages: 'vendor/amchart/images/',
dataDateFormat: 'YYYY-MM-DD',
categoryAxis: {
gridPosition: "start",
parseDates: true,
gridThickness: 0
},
valueAxes: [{
position: "left",
axisAlpha: 0,
labelsEnabled: true,
dashLength: 4,
color: '#dedede',
}],
graphs: [{
balloonText: "[[key]] [[value]] ",
"bullet": "round",
"bulletBorderAlpha": 1,
"bulletColor": "#FFFFFF",
"useLineColorForBulletBorder": true,
"fillAlphas": 0,
"lineThickness": 2,
"lineAlpha": 1,
"bulletSize": 7,
"title": "Expenses",
"valueField": field,
lineColor: '#FFC107',
}],
}
}
var fnInit = function() {
if ($scope.viewrule) {
$scope.view = 'pages';
fnChart($rootScope._history, $scope.viewrule);
if ($scope.viewrule != 'all') {
$scope.fnFilterRule($scope.viewrule);
} else {
// fuck, ach ja ...
var pages = angular.copy($rootScope._pages);
var stats = { 'done': 0, 'open': 0, 'total': 0, 'pages': 0 };
$scope.rule = { 'title': 'All pages', 'textfalse': 'View all pages and scores' }
var compare = {};
for (var i in pages) {
stats['pages']++;
for (var y in pages[i]['rules']) {
stats['total']++;
stats['done'] = (pages[i]['rules'][y]) ? stats['done'] + 1 : stats['done'];
stats['open'] = (pages[i]['rules'][y]) ? stats['open'] : stats['open'] + 1;
}
}
$scope.stats = stats;
}
} else if ($scope.viewrulelabel) {
if ($scope.viewrulelabel == 'assistent') {
$scope.view = 'assistent';
fnAssistent($rootScope._pages);
} else if ($scope.viewrulelabel == 'duplicates') {
$scope.view = 'duplicates';
fnDuplicates($rootScope._pages);
} else if ($scope.viewrulelabel == 'alerts') {
$scope.view = 'alerts';
$scope.oAlerts.init();
} else if ($scope.viewrulelabel == 'changes') {
$scope.view = 'changes';
fnChanges.init();
} else if ($scope.viewrulelabel == 'speed') {
$scope.view = 'speed';
fnSpeed($rootScope._pages);
} else if ($scope.viewrulelabel == 'aibots') {
$scope.view = 'aibots';
$scope.oAiBots.init();
} else {
$scope.view = 'rules';
if ($scope.viewrulelabel != 'all') {
var ret = {};
for (var i in $scope.onpage.rules) {
if ($scope.onpage.rules[i].label == $scope.viewrulelabel) {
ret[i] = $scope.onpage.rules[i];
}
}
$scope.onpage = { 'rules': ret, 'rulelabels': $scope.onpage.rulelabels };
}
fnRules($rootScope._pages);
fnChart($rootScope._history, $scope.viewrulelabel);
}
}
}
$scope.$on('refreshonpagelist', function(e) {
fnIndexList();
});
$scope.fnStar = function(curpage){
curpage.properties.star = (curpage.properties.star == 1)?0:1;;
$http.post('/rest/onpage/page.save', {
websiteid: h.siteId(),
pageid: curpage._id.$oid,
fields: { 'properties.star': curpage.properties.star }
});
//$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(); })
}
// get list
var fnIndexList = function() {
$scope.page.table.fnOrder('properties.opi', true)
//allways get rule list
$http.get('/rest/onpage/rules', { 'params': { 'websiteid': h.siteId(), 'lang': $scope.user.site.locale.lang } }).then(function successCallback(res) {
angular.forEach(res.data.rules, function(v, k) {
v.enabled = !v.disabled;
})
$scope.onpage = res.data;
// get pages and historu of cache
if ($rootScope._pages && $rootScope.onpagestatus.data.status == 'done') {
$scope.pages = angular.copy($rootScope._pages);
$scope.history = angular.copy($rootScope._history);
fnInit();
} else {
$http.get('/rest/onpage/list', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
$rootScope._pages = res.data.data;
$rootScope._history = res.data.history;
$scope.pages = angular.copy($rootScope._pages);
$scope.history = angular.copy($rootScope._history);
fnInit();
});
}
});
}
if ($scope.custominit) {
$scope.viewrule = $scope.custominit.viewrule;
$scope.viewrulelabel = $scope.custominit.viewrulelabel;
};
fnIndexList();
$scope.fnScanByUrl = function() {
$scope.fnGoto('/rest/onpage/page.create?websiteid=' + h.siteId() + '&url=' + $scope.page.newurl);
}
}
]);
app.controller('onpagescan', ['$scope', '$http', '$rootScope', '$filter', '$location', 'h',
function($scope, $http, $rootScope, $filter, $location, h) {
//page_id
var _pages;
$scope.page = { 'pending': true, 'overlay': {}, 'charts': {}, 'q': '' };
// ?tab=aicontent opent meteen dat tabblad (link vanaf de AI-pagina)
$scope.initialTab = $location.search().tab || 'score';
// AI-signalen (beta): uit de verse scan (met de hele tekst) of anders uit de opgeslagen pagina
$scope.fnAi = function() {
return ($scope.scan && $scope.scan.aicontent) || ($scope.pageinfo && $scope.pageinfo.aicontent) || null;
}
$scope.fnAiReasons = function(reasons) {
return (reasons || []).map(function(k) {
return $filter('translate')('onpage.aicontent.sig_' + k + '_title');
}).join(', ');
}
// filter op één patroon in de tekst, en de uitgeklapte uitleg (object: het tabblad zit in een child scope)
$scope.ai = { 'filter': null, 'info': false };
// patronen die per zin gemarkeerd worden; de rest (ritme, alineavorm, leesniveau) geldt voor de hele pagina
$scope.fnAiIsSentenceSignal = function(k) {
return ['phrases', 'buzzwords', 'transitions', 'em_dash', 'not_but', 'triplets'].indexOf(k) > -1;
}
$scope.fnAiLevelClass = function(level) {
return {
'high': 'bg-red-100 text-red-800',
'medium': 'bg-amber-100 text-amber-800',
'low': 'bg-green-100 text-green-800'
}[level] || 'bg-gray-100 text-gray-600';
}
var _cb = {
'redir_back_to_index': function() {
$location.path('/onpage-seo/all/all');
},
}
$scope.fnPctChange = function(o, n) {
if (!o) {
return ' ';
}
if (n == 0) {
return '0% ';
}
var pct = ((n - o) / o) * 100;
var npct = $filter('number')(pct, 2);
if (pct < 0) {
return '' + npct + '% ';
}
return '+ ' + npct + '% ';
}
$scope.fnScoreToColour = function(o){
if (!o) {
return ' ';
}
if (o < 50) {
return ''+o+'% ';
}
if (o < 75) {
return ''+o+'% ';
}
return ''+o+'% ';
}
/* perform scan*/
var fn = {
info: function(cb) {
/* get ruleset */
$http.get('/rest/onpage/rules', { 'params': { 'websiteid': h.siteId(), 'lang': $scope.user.site.locale.lang } }).then(function successCallback(res) {
angular.forEach(res.data.rules, function(v, k) {
v.enabled = !v.disabled;
})
$scope.onpage = res.data;
$http.get('/rest/onpage/page.info', {
params: {
websiteid: h.siteId(),
pageid: $scope.pageid,
}
}).then(function successCallback(res) {
$scope.pageinfo = res.data.data;
cb();
});
});
},
scan: function(cb) {
$http.get('/rest/onpage/page.scan', {
params: {
websiteid: h.siteId(),
pageid: $scope.pageid,
}
}).then(function successCallback(res) {
$scope.page.pending = false;
$scope.scan = res.data.data;
$scope.scan.avg = { 'all': 0, 'technical': 0, 'content': 0, 'mobile': 0, 'indexability': 0, 'social': 0 }
var l = $scope.scan.history.length;
angular.forEach($scope.scan.history, function(value, key) {
$scope.scan.avg['all'] += value.all / l
$scope.scan.avg['technical'] += value.technical / l
$scope.scan.avg['content'] += value.content / l
$scope.scan.avg['mobile'] += value.mobile / l
$scope.scan.avg['indexability'] += value.indexability / l
$scope.scan.avg['social'] += value.social / l
});
$scope.fnDateChart('history', 'all', { data: res.data.data.history }, false)
cb();
});
},
stats: function(rules) {
var stats = {};
for (var i in $scope.onpage['rules']) {
var rule = $scope.onpage['rules'][i];
if (rules[i]) {
stats[rule.label] = stats[rule.label] + 1 || 1;
}
}
},
searchconsole(cb) {
var addslash = true;
$http.get('/rest/gscinsights/report', {
params: { websiteid: h.siteId(), 'opportunity':'true','dimension': 'query', 'start': '90DAYSAGO', 'end': 'TODAY', 'metrics': 'clicks,ctr,position,impressions', 'report': 'searchAnalytics', 'addslash': addslash, 'filterdimension': "page", 'filterexpression': $scope.pageinfo.serps.url, 'filteroperator': "equals", limit: 6000 }
}).then(function successCallback(res) {
if (res.data.data.length > 0) {
$scope.searchconsolequeries = res.data;
} else {
addslash = false;
$http.get('/rest/gscinsights/report', {
params: { websiteid: h.siteId(), 'opportunity':'true','dimension': 'query', 'start': '90DAYSAGO', 'end': 'TODAY', 'metrics': 'clicks,ctr,position,impressions', 'report': 'searchAnalytics', 'addslash': addslash, 'filterdimension': "page", 'filterexpression': $scope.pageinfo.serps.url, 'filteroperator': "equals", limit: 6000 }
}).then(function successCallback(res) {
$scope.searchconsolequeries = res.data;
})
}
$http.get('/rest/gscinsights/report', {
params: { websiteid: h.siteId(), 'dimension': 'date', 'start': '90DAYSAGO', 'end': 'TODAY', 'metrics': 'clicks,ctr,position,impressions', 'report': 'searchAnalytics', 'addslash': addslash, 'filterdimension': "page", 'filterexpression': $scope.pageinfo.serps.url, 'filteroperator': "equals" }
}).then(function successCallback(res) {
if (res.data.data.length > 0) {
$scope.searchconsoledates = res.data;
$scope.fnDateChart('searchconsole', 'clicks', $scope.searchconsoledates, true);
}
})
})
},
inlinks() {
// make summary for inlinks ... shoudl I do this in rest ??
var r = {}
var data = []
for (var i in $scope.pageinfo.inlinks.links) {
var t = $scope.pageinfo.inlinks.links[i];
r[t.anchor] = r[t.anchor] + 1 || 1;
}
for (var i in r) {
data.push({ 'anchor': i, 'n': r[i] });
}
fnPieChart('inlinks', 'anchor', 'n', data)
},
interceptions() {
// make summary for interceptions ... shoudl I do this in rest ??
var r = {}
var data = []
for (var i in $scope.scan.scan['response_interceptions']) {
var t = $scope.scan.scan['response_interceptions'][i];
t['content-length'] = parseInt((parseInt(t['content-length']) > 0) ? t['content-length'] : 0);
r[t['content-type']] = r[t['content-type']] + t['content-length'] || t['content-length'];
}
for (var i in r) {
data.push({ 'content-type': i, 'total': r[i] });
}
fnPieChart('interceptions', 'content-type', 'total', data)
},
analytics() {
$http.get('/rest/analytics4/report', {
params: { websiteid: h.siteId(), 'dimension': 'date', 'start': '30DAYSAGO', 'end': 'TODAY', 'metrics': 'sessions,totalUsers,screenPageViews,screenPageViewsPerSession,averageSessionDuration,newUsers,bounceRate,eventCount,eventCountPerUser', 'path': $scope.pageinfo.url, 'sort': 'date', 'sortorder': "ASCENDING" }
}).then(function successCallback(res) {
$scope.analyticsdates = res.data;
$scope.fnDateChart('analytics', 'sessions', $scope.analyticsdates, true);
})
$http.get('/rest/analytics4/report', {
params: { websiteid: h.siteId(), 'dimension': 'sessionDefaultChannelGrouping', 'start': '30DAYSAGO', 'end': 'TODAY', 'metrics': 'sessions,totalUsers,screenPageViews,screenPageViewsPerSession,averageSessionDuration,newUsers,bounceRate,eventCount,eventCountPerUser', 'path': $scope.pageinfo.url, 'sort': 'users', 'sortorder': "DESCENDING" }
}).then(function successCallback(res) {
$scope.analyticschannels = res.data;
})
},
}
fn.info(
function() {
fn.stats($scope.pageinfo.rules);
fn.analytics();
fn.searchconsole();
fn.inlinks();
}
);
fn.scan(
function() {
fn.interceptions();
}
);
var fnPieChart = function(name, field, value, data) {
$scope.page.charts[name] = {
data: data.slice(0, 9),
type: 'pie',
theme: 'light',
marginLeft: 0,
marginRight: 0,
marginTop: 10,
fontFamily: 'Roboto',
categoryField: field,
pathToImages: 'vendor/amchart/images/',
"precision": 0,
"decimalSeparator": ",",
"thousandsSeparator": ".",
legend: {
enabled: true,
position: 'right',
autoMargins: false,
marginRight: 100,
},
titleField: field,
valueField: value,
labelRadius: 5,
radius: '42%',
innerRadius: '60%',
labelText: '[[title]]',
}
}
$scope.fnDateChart = function(name, field, data, reverse) {
data.field = field;
data.reverse = reverse
var opts = {
'searchconsole': { 'clicks': true, 'ctr': true, 'position': false, 'impressions': true },
'analytics': { 'users': true, 'pageviews': true, 'pageviewsPerSession': false, 'avgSessionDuration': true, 'percentNewSessions': true, 'bounceRate': true, 'goalCompletionsAll': true, 'goalValueAll': true },
'history': { 'all': false }
};
var dateformats = { 'searchconsole': 'YYYY-MM-DD', 'analytics': 'YYYYMMDD', 'history': 'YYYY-MM-DD' };
var datefields = { 'searchconsole': 'date', 'analytics': 'date', 'history': 'date' };
var colors = { 'searchconsole': '#007bff', 'analytics': '#28a745', 'history': '#28a745' };
datareverse = opts[name][field];
var c = {
data: data.data,
type: 'serial',
theme: 'light',
marginLeft: 0,
marginRight: 0,
marginTop: 10,
fontFamily: 'Roboto',
categoryField: datefields[name],
rotate: false,
dataDateFormat: dateformats[name],
categoryAxis: {
gridPosition: "start",
parseDates: true,
gridThickness: 0
},
valueAxes: [{
position: "left",
axisAlpha: 0,
labelsEnabled: true,
dashLength: 4,
color: '#dedede',
}],
graphs: [{
balloonText: " [[value]] ",
"bullet": "round",
"bulletBorderAlpha": 1,
"bulletColor": "#FFFFFF",
"useLineColorForBulletBorder": true,
"fillAlphas": 0,
"lineThickness": 2,
"lineAlpha": 1,
"bulletSize": 7,
"title": "Expenses",
"valueField": field,
lineColor: colors[name],
}],
}
if (name == 'history') {
c.autoMargins = false;
c.marginLeft = 0;
c.marginRight = 0;
c.marginTop = 0;
c.marginBottom = 0;
c.valueAxes[0].gridAlpha = 0;
c.valueAxes[0].axisAlpha = 0;
c.valueAxes[0].color = '#ffffff';
c.valueAxes[0].dashLength = 0;
c.categoryAxis.gridAlpha = 0;
c.categoryAxis.axisAlpha = 0;
}
$scope.page.charts[name] = c;
}
$scope.fnHide = function(type) {
angular.element('#' + type).addClass('d-none');
}
$scope.fnExpose = function(bounds, type) {
angular.element('#' + type).removeClass('d-none');
if (bounds.h > 0) {
var overlay = {}
if (type == 'mobile') {
var width = document.getElementById(type).clientWidth;
var scale = 360 / width;
} else {
var width = document.getElementById(type).clientWidth;
var scale = 1366 / width;
}
var scaledbounds = { l: bounds.l / scale, t: bounds.t / scale, w: bounds.w / scale, h: bounds.h / scale };
overlay.c = { 'top': scaledbounds.t, 'left': scaledbounds.l, 'width': scaledbounds.w, 'height': scaledbounds.h }
overlay.tl = { 'width': scaledbounds.l + scaledbounds.w / 2, 'height': scaledbounds.t }
overlay.tr = { 'width': width - (scaledbounds.l + scaledbounds.w / 2), 'height': scaledbounds.t }
overlay.bl = { 'width': scaledbounds.l + scaledbounds.w / 2, 'top': scaledbounds.t + scaledbounds.h }
overlay.br = { 'width': width - (scaledbounds.l + scaledbounds.w / 2), 'top': scaledbounds.t + scaledbounds.h }
overlay.lct = { 'width': scaledbounds.l, 'top': scaledbounds.t, 'height': scaledbounds.h / 2 }
overlay.lcb = { 'width': scaledbounds.l, 'top': scaledbounds.t + scaledbounds.h / 2, 'height': scaledbounds.h / 2 }
overlay.rct = { 'width': width - (scaledbounds.l + scaledbounds.w), 'top': scaledbounds.t, 'height': scaledbounds.h / 2 }
overlay.rcb = { 'width': width - (scaledbounds.l + scaledbounds.w), 'top': scaledbounds.t + scaledbounds.h / 2, 'height': scaledbounds.h / 2 }
$scope.page.overlay[type] = overlay;
var scrolltop = ((bounds.t / scale) - 100 < 0) ? 0 : (bounds.t / scale - 100);
angular.element('#' + type).stop(true, true).animate({ 'scrollTop': scrolltop + "px" }, 400)
}
}
$scope.fnSaveRules = function() {
var disabled_rules = Object.keys($scope.onpage.rules).reduce(function(acc,cur){
if(!$scope.onpage.rules[cur].enabled){
acc[cur] = true;
}
return acc;
},{});
$http.post('/rest/onpage/rules.disabled.set', {
'websiteid': h.siteId(),
'disabled_rules': disabled_rules,
}).then(function successCallback(res) {
_cb[res.data.cb]();
});
}
$scope.fnSavePage = function() {
$http.post('/rest/onpage/page.save', {
websiteid: h.siteId(),
pageid: $scope.pageid,
fields: { 'q': $scope.pageinfo.q, 'qneg': $scope.pageinfo.qneg }
}).then(function successCallback(res) {
_cb[res.data.cb]();
});
}
$scope.fnDeletePage = function() {
$http.post('/rest/onpage/page.delete', {
websiteid: h.siteId(),
pageid: $scope.pageid,
}).then(function successCallback(res) {
_cb[res.data.cb]();
});
}
}
]);
app.controller('speedtest', ['$scope', '$http', '$rootScope', '$filter', '$window', 'h',
function($scope, $http, $rootScope, $filter, $window, h) {
var TARGET_TTFB = 64;
$scope.speedtestpage = { 'state': 'init', 'url': $scope.pageinfo.url };
$scope.t = { 'ttfb': 'TTFB', 'dns': 'DNS', 'dominteractive': 'DOM Interactive', 'domcontentloaded': 'DOM Content Loaded', 'onload': 'On Load', 'firstcontentfulfulpaint': 'FCP', 'largestcontentfulpaint': 'LCP', 'visualReady': 'visual Ready' };
// no need to bind everything
$scope.do = function(o, f) {
speedtest[f]();
}
$scope.getColor = function(v) {
return (v < 50) ? '#dc3545' : (v < 75) ? '#FF9800' : '#28a745';
}
//main
var speedtest = {
init: function() {
if (!$scope.speedtest) {
$scope.speedtestpage.state = 'pending';
$http.get('https://robinssl.seoserver.nl/speedtest/scan.php', {
params: {
'url': $scope.speedtestpage.url,
}
}).then(function successCallback(response) {
if (response.data.error) {
$scope.speedtestpage.state = 'error';
} else {
$scope.speedtestpage.state = 'done';
$scope.speedtest = response.data;
$scope.speedtest.timeline = {};
$scope.speedtest.date = new Date();
speedtest._totaltimeTTFB();
speedtest._firstPage();
speedtest._createResourceTimelineFromEventInterest('BlockingJavaScript')
speedtest._createResourceTimelineFromEventInterest('nonBlockingJavaScript')
speedtest._createResourceTimelineFromEventInterest('BlockingCss')
speedtest._createResourceTimelineFromResourceInterest('ImageNotLazy')
speedtest._createResourceTimelineFromResourceInterest('IncorrectImageScale')
speedtest._createResourceTimelineFromResourceInterest('BlockingFont')
speedtest._createResourceTimelineFromResourceInterest('NotCacheAble')
speedtest._createResourceTimelineFromResourceInterest('NotCompressed')
speedtest._createResourceTimelineFromResourceInterest('NotHttp2')
speedtest._createFilmstrip()
// estimated savings
$scope.speedtest.timeline['NotCompressed'].est_saving *= .15 /*15% faster*/
$scope.speedtest.timeline['NotHttp2'].est_saving *= .36 /*36% faster*/
var scores = {
'firstcontentfulfulpaint': score.computeLogNormalScore(speedtest._getTimingByKey('firstcontentfulfulpaint'), 600, 2200),
'largestcontentfulpaint': score.computeLogNormalScore(speedtest._getTimingByKey('largestcontentfulpaint'), 800, 2600),
'visualReady': score.computeLogNormalScore(speedtest._getTimingByKey('visualReady'), 3000, 4000),
'totaltimeTTFB': score.computeLogNormalScore($scope.speedtest.timeline['totaltimeTTFB'].est_saving, 400, 600),
'NotHttp2': score.computeLogNormalScore($scope.speedtest.timeline['NotHttp2'].est_saving, 400, 600),
'firstPage': score.computeLogNormalScore($scope.speedtest.timeline['firstPage'].est_saving, 80, 160),
'BlockingJavaScript': score.computeLogNormalScore($scope.speedtest.timeline['BlockingJavaScript'].est_saving, 50, 100),
'nonBlockingJavaScript': score.computeLogNormalScore($scope.speedtest.timeline['nonBlockingJavaScript'].est_saving, 1000, 2000),
'BlockingCss': score.computeLogNormalScore($scope.speedtest.timeline['BlockingCss'].est_saving, 60, 120),
'BlockingFont': score.computeLogNormalScore($scope.speedtest.timeline['BlockingFont'].est_saving, 50, 100),
'ImageNotLazy': score.computeLogNormalScore($scope.speedtest.timeline['ImageNotLazy'].est_saving, 150, 300),
'IncorrectImageScale': score.computeLogNormalScore($scope.speedtest.timeline['ImageNotLazy'].est_saving, 150, 300),
'NotCompressed': score.computeLogNormalScore($scope.speedtest.timeline['NotCompressed'].est_saving, 150, 300),
'NotCacheAble': score.computeLogNormalScore($scope.speedtest.timeline['NotCacheAble'].est_saving, 150, 300),
}
$scope.scores = {
'total': Math.round((scores.firstPage + scores.firstcontentfulfulpaint + scores.largestcontentfulpaint + scores.visualReady + scores.NotHttp2 + scores.BlockingJavaScript + scores.BlockingCss + scores.BlockingFont) / 8),
'server': Math.round((scores.firstPage) / 1),
'paint': Math.round((scores.firstcontentfulfulpaint + scores.largestcontentfulpaint + scores.BlockingJavaScript + scores.BlockingCss + scores.BlockingFont) / 5),
'webflow': Math.round((scores.ImageNotLazy + scores.totaltimeTTFB + scores.NotCacheAble + scores.NotCompressed + scores.NotHttp2 + scores.nonBlockingJavaScript) / 6),
'part': scores
}
$scope.speedtestpage.state = 'done';
}
});
}
},
_getTimingByKey: function(key) {
return ($scope.speedtest.timing.filter(function(a) { return (a['name'] === key) })[0] || { 'ts': 0 }).ts;
},
_createFilmstrip: function() {
var n = document.querySelector('#filmstrip').clientWidth / 55;
$scope.filmstrip = [];
// fake ts ergens in midden
for (var i = 0; i < n; i++) {
var y = ($scope.speedtest.ts.last / n) * i;
var filmstrip = $scope.speedtest.filmstrip.reduce(function image(acc, cur) {
return ((Math.abs(cur.ts - y) < Math.abs(acc.ts - y)) && cur.ts < y) ? cur : acc;
})
$scope.filmstrip.push(filmstrip.args.snapshot);
}
},
_firstPage: function() {
var firstPage = $scope.speedtest.resources[0]
firstPage.timing = firstPage.timing || { receiveHeadersEnd: TARGET_TTFB }
var saving = (firstPage.timing.receiveHeadersEnd > TARGET_TTFB) ? firstPage.timing.receiveHeadersEnd - TARGET_TTFB : 0
var timeline = {}
timeline[firstPage.url] = { 'resources': [firstPage], 'totaltime': firstPage.timing.receiveHeadersEnd }
$scope.speedtest.timeline['firstPage'] = { 'timeline': timeline, 'est_saving': saving, 'totaltime': firstPage.duration, 'ttfb': speedtest._getTimingByKey('ttfb') }
},
_totaltimeTTFB: function() {
// calc ttfb if larger then good ttfp, deep copy resouces and overwrite duration
var resourceclone = JSON.parse(JSON.stringify($scope.speedtest.resources));
var cutoff = speedtest._getTimingByKey('visualReady');
var ResourcesOfInterestUrls = resourceclone.reduce(function(acc, resource) {
resource.timing = resource.timing || { receiveHeadersEnd: TARGET_TTFB }
if (resource.timing.receiveHeadersEnd > TARGET_TTFB && resource.endTime < cutoff) {
resource.endTime = resource.startTime + resource.timing.receiveHeadersEnd - TARGET_TTFB
resource.duration = resource.timing.receiveHeadersEnd - TARGET_TTFB
acc.push(resource)
}
return acc;
}, []);
var timeline = ResourcesOfInterestUrls.reduce(function(a, b) {
a[b.url] = a[b.url] || { 'resources': [], 'totaltime': 0 }
return a;
}, {})
timeline = h.__mergeResources(timeline, ResourcesOfInterestUrls);
$scope.speedtest.timeline['totaltimeTTFB'] = h.__createTimeLine(timeline);
},
_createResourceTimelineFromResourceInterest: function(Interest) {
var ResourcesOfInterestUrls = $scope.speedtest.resources.filter(function(resource) {
return resource.interest.includes(Interest)
});
// create timeline from Resources
var timeline = ResourcesOfInterestUrls.reduce(function(a, b) {
a[b.url] = a[b.url] || { 'resources': [], 'totaltime': 0 }
return a;
}, {})
timeline = h.__mergeResources(timeline, ResourcesOfInterestUrls);
$scope.speedtest.timeline[Interest] = h.__createTimeLine(timeline);
},
_createResourceTimelineFromEventInterest: function(EventInterest) {
var allEvents = $scope.speedtest.eventsOfInterest.filter(function(event) {
return event.interest[0] == EventInterest
});
// creat timeline from events
var timeline = allEvents.reduce(function(a, b) {
a[b.url] = a[b.url] || { 'resources': [], 'totaltime': 0 }
a[b.url].resources.push(b);
return a;
}, {})
timeline = h.__mergeResources2(timeline, $scope.speedtest.resources);
$scope.speedtest.timeline[EventInterest] = h.__createTimeLine(timeline)
}
}
//helpers
var h = {
__mergeResources: function(timeline, allResources) {
// merge resources to timeline, url must match and type not html
angular.forEach(timeline, function(v, k) {
var myResources = Object.values(allResources).filter(function(e) {
return e.url == k;
//return e.url == k && e.mimeType != 'text/html';
})
timeline[k].resources = timeline[k].resources.concat(myResources);
timeline[k].totaltime = timeline[k].resources.reduce(function(acc, cur) { return acc + cur.duration }, 0);
});
return timeline;
},
__mergeResources2: function(timeline, allResources) {
// merge resources to timeline, url must match and type not html
angular.forEach(timeline, function(v, k) {
var myResources = Object.values(allResources).filter(function(e) {
//return e.url == k;
return e.url == k && e.mimeType != 'text/html';
})
timeline[k].resources = timeline[k].resources.concat(myResources);
timeline[k].totaltime = timeline[k].resources.reduce(function(acc, cur) { return acc + cur.duration }, 0);
});
return timeline;
},
__mergeTiming: function(aTiming) {
// sort timing (needer for merge)
aTiming = aTiming.sort(function(a, b) { return a[0] - b[0] })
// merge overlapping time
var mergedtime = aTiming.reduce(function(combined, next) {
if (!combined.length || combined[combined.length - 1][1] < next[0]) combined.push(next);
else {
var prev = combined.pop();
combined.push([prev[0], Math.max(prev[1], next[1])]);
}
return combined;
}, []);
// calculate total time
var totaltime = mergedtime.reduce(function(a, b) {
return (a + b[1] - b[0]);
}, 0);
return totaltime;
},
__createTimeLine(timeline) {
// fetch array of timings [[1,4],[2,3]] etc
var aTiming = [];
angular.forEach(timeline, function(v, url) {
var tmp = v.resources.reduce(function(a, b) {
a.push([b.startTime, b.endTime]);
return a;
}, []);
aTiming = aTiming.concat(tmp);
});
var est_saving = h.__mergeTiming(aTiming);
return { 'timeline': timeline, 'est_saving': est_saving, 'n_timeranges': aTiming.length }
},
}
//score
var score = {
computeLogNormalScore: function(measuredValue, diminishingReturnsValue, medianValue) {
var distribution = this.getLogNormalDistribution(
medianValue,
diminishingReturnsValue
);
let score = distribution.computeComplementaryPercentile(measuredValue);
score = Math.min(1, score);
score = Math.max(0, score);
return Math.round(score * 100);
},
erf: function(x) {
var sign = Math.sign(x);
x = Math.abs(x);
var a1 = 0.254829592;
var a2 = -0.284496736;
var a3 = 1.421413741;
var a4 = -1.453152027;
var a5 = 1.061405429;
var p = 0.3275911;
var t = 1 / (1 + p * x);
var y = t * (a1 + t * (a2 + t * (a3 + t * (a4 + t * a5))));
return sign * (1 - y * Math.exp(-x * x));
},
getLogNormalDistribution: function(median, falloff) {
var location = Math.log(median);
// The "falloff" value specified the location of the smaller of the positive
// roots of the third derivative of the log-normal CDF. Calculate the shape
// parameter in terms of that value and the median.
var logRatio = Math.log(falloff / median);
var shape = Math.sqrt(1 - 3 * logRatio - Math.sqrt((logRatio - 3) * (logRatio - 3) - 8)) / 2;
return {
computeComplementaryPercentile(x) {
var standardizedX = (Math.log(x) - location) / (Math.SQRT2 * shape);
return (1 - score.erf(standardizedX)) / 2;
},
};
}
}
//stupid resize
angular.element($window).bind('resize', function() {
speedtest._createFilmstrip();
$scope.$apply()
});
}
]);
app.controller('planning', ['$scope', '$timeout', '$http', '$rootScope', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $filter, h) {
$scope.curtemplate = {};
$scope.categorys = {};
var aTemplateIds = [];
// do
$scope.do = function(obj, method, param) {
if (obj == 'users') {
return users[method](param);
} else if (obj == 'taken') {
return taken[method](param);
} else if (obj == 'taak') {
return taak[method](param);
} else if (obj == 'fnTemplates') {
return fnTemplates[method](param);
}
}
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 '';
}
}
var taken = {
'data': { taken: [] },
init: function() {
$scope.datestring = ($rootScope.reportdates || { datestring: false }).datestring || new Date().toLocaleString('default', { year: 'numeric', 'month': 'long' })
//tming could be wrong ... callback?
users.userList();
taken.getTaken();
},
setDateString: function(s) {
$scope.datestring = s;
taken.getStats();
},
fnReset: function(mtype) {
console.log(mtype);
var aantal = (mtype.type == 'taak') ? { 'aantal': 1 } : {};
$scope.curtaak = angular.extend({ 'type': "taak", 'repeat': "", 'status': 'open', 'datum': new Date(), 'workflow': [{ 'uren': 0, 'user_id': $rootScope.user._id.$oid + "" }] }, mtype, aantal)
console.log($scope.curtaak);
/* $scope.curtaak = { 'aantal': 1, 'type': "taak", 'repeat': "", 'status': 'open', 'datum': new Date(), 'workflow': [{ 'uren': 0, 'user_id': $rootScope.user._id.$oid + "" }] };
if (mtype == 'workflow') {
$scope.curtaak = { 'type': "workflow", 'repeat': "", 'status': 'open', 'datum': new Date(), 'workflow': [{ 'uren': 0, 'user_id': $rootScope.user._id.$oid + "" }] };
} else {
$scope.curtaak = { 'aantal': 1, 'type': "taak", 'repeat': "", 'status': 'open', 'datum': new Date(), 'workflow': [{ 'uren': 0, 'user_id': $rootScope.user._id.$oid + "" }] };
}*/
},
getTaken: function() {
$http.get("/rest/planning/planning.list", {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
taken.data = res.data;
$scope.taken = taken.data.taken;
fnTemplates.init();
});
},
getStats: function() {
console.log('GET STATS');
var categories = {};
var overzicht = {};
var urenPerUser = {};
//loop planning
taken.data.taken.forEach(function(planning, k) {
//date conversion
planning.datum = new Date(planning.datum);
planning.datestring = planning.datum.toLocaleString('default', { year: 'numeric', 'month': 'long' })
//overichten aanmaken
overzicht[planning.datestring] = overzicht[planning.datestring] || { 'workflow': { 'open': 0, 'done': 0, 'active': 0 }, 'taak': { 'open': 0, 'done': 0 } }
// workflow
if (planning.type == 'workflow') {
planning.workflow.forEach(function(taak) {
// opverzicht[open|done] optellen
//er is een category
if (planning.template_id && aTemplateIds.indexOf(planning.template_id['$oid']) > -1) {
categories[planning.category] = { 'order': 1, 'name': planning.category, 'type': 'workflow' };
overzicht[planning.datestring][planning.category] = overzicht[planning.datestring][planning.category] || { 'open': 0, 'done': 0, 'active': 0 }
overzicht[planning.datestring][planning.category][planning.status] += (taak.uren * 1)
} else {
categories['workflow'] = { 'order': 2, 'name': 'workflow', 'type': 'workflow', 'translate': true };
overzicht[planning.datestring].workflow[planning.status] += (taak.uren * 1)
console.log(taak.uren);
console.log(planning.status);
}
if (planning.datestring == $scope.datestring) {
// user per user
urenPerUser[taak.user_id] = urenPerUser[taak.user_id] || 0
urenPerUser[taak.user_id] += (taak.status == 'done') ? taak.uren * 1 : 0;
}
});
// taak
} else {
// opverzicht[open|done] optellen
if (planning.template_id && aTemplateIds.indexOf(planning.template_id['$oid']) > -1) {
categories[planning.category] = { 'order': 1, 'name': planning.category, 'type': 'taak', 'translate': false };
overzicht[planning.datestring][planning.category] = overzicht[planning.datestring][planning.category] || { 'open': 0, 'done': 0 }
overzicht[planning.datestring][planning.category][planning.status] += (planning.aantal * 1)
} else {
categories['taak'] = { 'order': 2, 'name': 'taak', 'type': 'taak', 'translate': true };
overzicht[planning.datestring].taak[planning.status] += (planning.aantal * 1)
}
}
});
$scope.categories = Object.values(categories);
$scope.overzicht = overzicht;
charts.piechart(urenPerUser);
}
}
var fnTemplates = {
init: function() {
fnTemplates.list();
},
list: function() {
$http.get('/rest/planning/template.list', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
$scope.taaktemplates = res.data;
aTemplateIds = res.data.map(function(c) { return c.template_id['$oid'] });
taken.getStats();
});
},
pre_edit: function() {
$scope.fnModal('planning/modal.pre_edit');
},
create: function() {
$http.post('/rest/planning/template.save', { 'template': $scope.curtemplate }).then(function successCallback(res) {
$scope.fnCloseModal();
fnTemplates.list();
});
},
save: function(template) {
$http.post('/rest/planning/template.save', { websiteid: h.siteId(), 'template': template }).then(function successCallback(res) {
$scope.fnCloseModal();
fnTemplates.list();
});
},
delete: function(template) {
$http.post('/rest/planning/template.delete', template).then(function successCallback(res) {
$scope.fnCloseModal();
fnTemplates.list();
});
},
}
var taak = {
addWorkFlow: function() {
$scope.curtaak.workflow.push({ user_id: $rootScope.user._id.$oid + "" });
},
removeWorkFlow: function(index) {
$scope.curtaak.workflow.splice(index, 1);
},
useTemplate: function(curtaak) {
$scope.curtaak = curtaak;
},
addCurtaak: function(taak) {
$http.post('/rest/planning/planning.add', angular.extend($scope.curtaak, { websiteid: h.siteId(), })).then(function successCallback(res) {
taken.getTaken();
});
},
updateCurtaak: function(taak) {
$http.post('/rest/planning/planning.update', angular.extend($scope.curtaak, { websiteid: h.siteId(), })).then(function successCallback(res) {
taken.getTaken();
});
},
editTaakStatus: function(taak) {
taak.status = (taak.status == 'done') ? 'open' : 'done';
$http.post('/rest/planning/planning.update', angular.extend(taak, { websiteid: h.siteId(), })).then(function successCallback(res) {
taken.getTaken();
});
},
editTaak: function(taak) {
$scope.curtaak = taak;
$scope.fnModal('planning/modal.' + taak['type']);
},
setVoortgang: function([taak, index]) {
$http.post('/rest/planning/planning.progress', { 'taak': taak, 'index': index, websiteid: h.siteId() }).then(function successCallback(res) {
taken.getTaken();
});
},
removeCurtaak: function(taak) {
$http.post('/rest/planning/planning.delete', angular.extend($scope.curtaak, { websiteid: h.siteId(), })).then(function successCallback(res) {
taken.getTaken();
});
},
}
var charts = {
piechart: function(urenPerUser) {
var colors = h.colors();
// pie data zetten
var piedata = Object.keys(urenPerUser).reduce(function(acc, cur, idx) {
acc.push({ "color": colors[idx], label: users.matchUser(cur), data: urenPerUser[cur] })
return acc;
}, []);
$scope.piechart = {
data: piedata,
type: 'pie',
marginLeft: 0,
marginRight: 0,
marginTop: 0,
categoryField: 'label',
colorField: 'color',
"precision": 0,
"decimalSeparator": ",",
"thousandsSeparator": ".",
titleField: 'label',
valueField: 'data',
labelRadius: 5,
radius: '42%',
innerRadius: '60%',
labelText: '[[title]]',
}
}
}
taken.init();
}
]);
// White label (rol 1-2): /settings/reseller(/:tab). Tabs: overzicht, tools, huisstijl, team, communicatie, facturen,
// api. Opzet: https://claude.ai/artifact/H8qH1fEJ1ZXS2JxV7Cut4c (Arjen, 2026-10-27). Add-ons tegen kostprijs: dat staat
// er altijd, groot (onze manier om vriendelijk te zijn); geen opslag en geen bronnen noemen. Eindklanten zien nooit prijzen.
// Server: rest/data/reseller/ (overview, tools, upload, asset, reseller.get.fromuser, reseller.update).
app.controller('reseller', ['$scope', '$rootScope', '$http', '$routeParams', '$location', '$translate', '$timeout', 'mtFmt',
function ($scope, $rootScope, $http, $routeParams, $location, $translate, $timeout, mtFmt) {
var TABS = { '': 'overview', tools: 'tools', huisstijl: 'style', team: 'team', communicatie: 'mail', facturen: 'billing', api: 'api' };
var BASE = '/settings/reseller';
var t = function (k, v) { return $translate.instant(k, v); };
var f = mtFmt;
// zelfde lijsten als serp::FREQ/DEPTHS en ai::PLAN_FREQ/ANSWER_STEPS (metingen = prompt × AI)
var SERP_FREQS = ['monthly', 'biweekly', 'weekly', 'alternate', 'daily'];
var SERP_KEYWORDS = [100, 150, 200, 250, 300, 400, 500, 750, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 7500, 10000];
var SERP_DEPTHS = [20, 30, 50, 100];
var AI_FREQS = ['monthly', 'biweekly', 'weekly'];
var AI_ANSWERS = [30, 75, 150, 300, 600];
// Arjens eigen accounts zien de facturen ook als ze voor de reseller verborgen zijn (mandate.hidebilling)
var BILLING_ADMINS = ['5ca342895c5d0214af7d451d', '5c9ca93a5c5d0238a9171ac5'];
// de standaard grafiekkleuren (h.colors in js/app.js)
var CHART_DEFAULTS = ['#E91E63', '#3F51B5', '#00BCD4', '#8BC34A', '#FF9800', '#795548', '#607D8B', '#00CC00', '#0000CC', '#DDDDDD', '#999999', '#333333'];
var w = $scope.w = {
f: f,
tab: TABS[$routeParams.tab || ''] || 'overview',
r: null,
loading: true,
denied: false,
saving: false,
saved: false,
serpFreqs: SERP_FREQS, serpKeywords: SERP_KEYWORDS, serpDepths: SERP_DEPTHS, aiFreqs: AI_FREQS, aiAnswers: AI_ANSWERS,
adminRoles: [2, 3],
// vaste lijsten voor ng-repeat (geen array-literals in het sjabloon: eindeloze digest)
logoFields: [{ k: 'logo', w: 'w-32' }, { k: 'logosmall', w: 'w-12' }, { k: 'loginlogo', w: 'w-32' }, { k: 'loginbg', w: 'w-32' }],
colorFields: [{ k: 'colorprimary' }, { k: 'colorinfo' }],
reportFields: [{ k: 'coverimage', size: '992 × 1403 px' }, { k: 'headerimage', size: '992 × 120 px' }, { k: 'footerimage', size: '992 × 60 px' }],
addressFields: ['bedrijfsnaam', 'contactpersoon', 'address', 'postcode', 'stad', 'land', 'btwnr'],
previewBars: [40, 55, 48, 70, 62, 80, 74],
page: { newadmin: {}, testemail: ($rootScope.user && $rootScope.user.email) || '', showSecret: false, resetWarning: false, advanced: false },
init: function () {
var self = this;
self.tabs = Object.keys(TABS).map(function (slug) {
return { key: TABS[slug], label: t('whitelabel.tab.' + TABS[slug]), href: BASE + (slug ? '/' + slug : '') };
});
self.load();
$http.get('/rest/locale/language.list').then(function (res) { self.languages = res.data; });
$http.get('/rest/locale/country.list').then(function (res) { self.countries = res.data; });
},
load: function () {
var self = this;
$http.get('/rest/reseller/reseller.get.fromuser').then(function (res) {
if (!res.data._id) { self.denied = true; self.loading = false; return; }
var r = res.data;
r.config = r.config || {};
// grafiekkleuren: wat het dashboard nu echt gebruikt (h.colors: eigen kleuren over de standaard heen)
r.config.chartcolors = r.config.chartcolors || [];
angular.forEach(CHART_DEFAULTS, function (c, i) { if (!r.config.chartcolors[i]) { r.config.chartcolors[i] = c; } });
r.config.serp = r.config.serp || {};
r.config.serp.plan = r.config.serp.plan || { freq: 'monthly', keywords: 100, depth: 20 };
r.config.aiinsights = r.config.aiinsights || {};
r.config.aiinsights.plan = r.config.aiinsights.plan || { freq: 'monthly', answers: 30 };
// een plan van voor 2026-10-27 telde prompts op alle drie de AI's (zoals ai::planInput)
if (!r.config.aiinsights.plan.answers) { r.config.aiinsights.plan.answers = (+r.config.aiinsights.plan.prompts || 10) * 3; delete r.config.aiinsights.plan.prompts; }
r.email = r.email || {};
r.report = r.report || {};
r.locale = r.locale || {};
r.api = r.api || {};
r.api.moz = r.api.moz || {};
r.billingaddress = r.billingaddress || {};
self.r = r;
self.loading = false;
self.refresh();
self.planPrice('serp');
self.planPrice('ai');
}, function () { self.denied = true; self.loading = false; });
$http.get('/rest/reseller/overview').then(function (res) { self.overview = res.data; self.refresh(); });
$http.get('/rest/reseller/health').then(function (res) { self.health = res.data; self.refresh(); });
$http.get('/rest/reseller/tools').then(function (res) { self.toolsData = res.data; self.refresh(); });
},
// alles wat ng-repeat toont, één keer per nieuwe data
refresh: function () {
var self = this, v = {};
// Overzicht: de staat van alle sites (cron/whitelabel.health, 's nachts); eerst toegevoegde waarde, de
// kosten staan bij Facturen (Arjen, 2026-10-27)
if (self.health) {
v.health = self.viewHealth(self.health);
}
var o = self.overview;
if (o) {
var money = function (x) { return self.money(x, o.currency); };
v.lines = [{ what: t('whitelabel.overview.line_dashboards'), how: t('whitelabel.overview.line_dashboards_how', { n: f.num(o.units, 1), price: money(o.unit_price) }), amount: money(o.dashboards) }];
if (o.serp.gross) {
v.lines.push({ what: t('whitelabel.overview.line_serp'), how: t('whitelabel.overview.line_sites', { n: o.serp.sites }), amount: f.euro(o.serp.gross) });
}
if (o.ai.gross) {
v.lines.push({ what: t('whitelabel.overview.line_ai'), how: t('whitelabel.overview.line_sites', { n: o.ai.sites }), amount: f.euro(o.ai.gross) });
}
v.totalText = money(o.total);
var inc = o.included;
v.included = [
{ t: t('whitelabel.overview.inc_tools'), d: t('whitelabel.overview.inc_tools_d') },
{ t: t('menu.serp'), d: t('whitelabel.overview.inc_serp', { n: inc.serp.keywords, depth: inc.serp.depth }) },
{ t: t('aiinsights.title'), d: t('whitelabel.overview.inc_ai', { n: inc.ai.answers }) },
{ t: t('whitelabel.overview.inc_brand'), d: t('whitelabel.overview.inc_brand_d') }
];
var freq = function (x) { return t('serp.freq.' + x); };
v.addons = [
{ name: t('menu.serp'), included: t('whitelabel.overview.inc_serp', { n: inc.serp.keywords, depth: inc.serp.depth }),
examples: o.examples.serp.map(function (p) { return { label: t('whitelabel.addons.ex_serp', { n: f.num(p.keywords), freq: freq(p.freq).toLowerCase(), depth: p.depth }), price: f.euro(p.price) }; }) },
{ name: t('aiinsights.title'), included: t('whitelabel.overview.inc_ai', { n: inc.ai.answers }),
examples: o.examples.ai.map(function (p) { return { label: t('whitelabel.addons.ex_ai', { n: p.answers, freq: freq(p.freq).toLowerCase() }), price: f.euro(p.price) }; }) }
];
}
if (self.toolsData && self.r) {
// de matrix op het reseller-object, zodat opslaan precies opslaat wat je ziet
var cfg = self.r.config;
cfg.tools = cfg.tools || {};
angular.forEach(self.toolsData.roles, function (role) {
cfg.tools[role] = cfg.tools[role] && !angular.isArray(cfg.tools[role]) ? cfg.tools[role] : {};
angular.forEach(self.toolsData.matrix[role], function (on, tool) {
if (typeof cfg.tools[role][tool] === 'undefined') { cfg.tools[role][tool] = on; }
});
});
v.roles = self.toolsData.roles.map(function (role) { return { role: role, label: t('whitelabel.role.' + role) }; });
v.tools = self.toolsData.tools.map(function (x) { return { tool: x.tool, label: t(x.translate) }; });
}
if (self.r) {
v.billingRows = Object.keys(self.r.billing || {}).filter(function (m) { return /^\d{6}$/.test(m); }).sort().reverse().map(function (m) {
var b = self.r.billing[m], serp = +((b.serp || {}).gross || 0), ai = +((b.ai || {}).gross || 0), total = +b.price || 0;
return { key: m, month: f.monthLong(m.substr(0, 4) + '-' + m.substr(4, 2)), units: f.num(+b.units || 0, 1),
dashboards: self.money(total - serp - ai), serp: serp ? f.euro(serp) : '–', ai: ai ? f.euro(ai) : '–', total: self.money(total) };
});
v.showBilling = !(self.r.mandate && self.r.mandate.hidebilling) || BILLING_ADMINS.indexOf((($rootScope.user || {})._id || {}).$oid || ($rootScope.user || {})._id) > -1;
v.team = (self.r.users || []).filter(function (u) { return self.r.names && self.r.names[u.user_id.$oid]; }).map(function (u) {
u.role = +u.role;
return { u: u, name: self.r.names[u.user_id.$oid].name, email: self.r.names[u.user_id.$oid].email, me: u.user_id.$oid === ((($rootScope.user || {})._id || {}).$oid || ($rootScope.user || {})._id) };
});
v.chartIdx = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
// de eigen reseller (MarketingTracer zelf) heeft geen abonnement nodig
v.mandateOk = (self.r.mandate && self.r.mandate.status === 'done') || !!(self.r.config && self.r.config.own);
}
self.v = v;
},
hsort: { key: 'domain', desc: false },
viewHealth: function (h) {
if (!h.at) { return { pending: true }; }
var s = h.summary;
var pct = function (a, b) { return b ? 100 * (a - b) / b : null; };
var kpis = [
{ label: t('whitelabel.health.kpi_sites'), value: f.num(s.sites), note: t('whitelabel.health.scanned', { n: s.scanned }) },
{ label: t('whitelabel.health.kpi_score'), value: s.score === null ? '–' : f.num(s.score, 0), delta: s.score !== null && s.score_before !== null ? s.score - s.score_before : null, unit: 'pt', note: t('whitelabel.health.vs_4w') },
{ label: t('whitelabel.health.kpi_fixed'), value: f.num(s.fixed), note: t('whitelabel.health.last_4w') },
{ label: t('whitelabel.health.kpi_clicks'), value: s.gsc ? f.num(s.clicks) : '–', delta: s.gsc ? pct(s.clicks, s.clicks_before) : null, note: s.gsc ? t('whitelabel.health.gsc_sites', { n: s.gsc }) : t('whitelabel.health.no_gsc') }
];
if (s.monitored) {
kpis.push({ label: t('whitelabel.health.kpi_uptime'), value: f.num(s.monitored - s.down) + ' / ' + f.num(s.monitored), note: s.down ? t('whitelabel.health.down', { n: s.down }) : t('whitelabel.health.all_up'), warn: s.down > 0 });
}
var item = function (x) {
var bits = x.why.map(function (w) {
if (w === 'score') { return t('whitelabel.health.why.score', { n: (x.score_change > 0 ? '+' : '') + x.score_change }); }
if (w === 'clicks') { return t('whitelabel.health.why.clicks', { pct: (x.clicks_change > 0 ? '+' : '') + f.num(x.clicks_change, 0) + '%' }); }
if (w === 'errors') { return t('whitelabel.health.why.errors', { n: -x.fixed }); }
if (w === 'fixed') { return t('whitelabel.health.why.fixed', { n: x.fixed }); }
return t('whitelabel.health.why.' + w);
});
return { id: x.id, domain: x.domain, text: bits.join(' · '), score: x.score === null ? '–' : x.score };
};
return {
at: h.at, kpis: kpis, cols: kpis.length === 5 ? 'lg:grid-cols-5' : 'lg:grid-cols-4',
attention: s.attention.map(item), gainers: s.gainers.map(item),
rows: h.rows.map(function (r) {
return angular.extend({}, r, {
scoreText: r.score === null ? '–' : r.score,
clicksText: r.clicks === null ? '–' : f.num(r.clicks),
top10Text: r.keywords ? r.top10 + ' / ' + r.keywords : '–',
// sorteren: zonder waarde onderaan
sScore: r.score === null ? -1 : r.score, sClicks: r.clicks === null ? -1 : r.clicks, sTop10: r.top10 === null ? -1 : r.top10,
sChange: r.clicks_change === null ? -1000 : r.clicks_change, sScoreChange: r.score_change === null ? -1000 : r.score_change
});
})
};
},
// naar een site: wisselen en naar het dashboard
openSite: function (id) {
$http.post('/rest/sites/change', { websiteid: id }).then(function () { window.location.href = '/'; });
},
money: function (x, cur) { return (cur || (this.overview && this.overview.currency)) === 'USD' ? '$ ' + f.num(x, 2) : f.euro(x); },
// kleine bedragen per eenheid: zoveel decimalen als nodig
eur: function (x, digits) { return '€ ' + f.num(x, digits); },
go: function (tab) { $location.path(BASE + (tab ? '/' + tab : '')); },
// ---------------------------------------------------------------- opslaan
save: function () {
var self = this;
self.saving = true;
self.saved = false;
$http.post('/rest/reseller/reseller.update', self.r).then(function () {
self.saving = false;
self.saved = true;
$timeout(function () { self.saved = false; }, 3000);
$http.get('/rest/reseller/overview').then(function (res) { self.overview = res.data; self.refresh(); });
}, function () { self.saving = false; self.error = t('whitelabel.save_failed'); });
},
// ---------------------------------------------------------------- standaardpakketten (Rank tracker, AI Insights)
planPrice: function (which) {
var self = this, cfg = self.r.config;
var url = which === 'serp' ? '/rest/serp/plan' : '/rest/aiinsights/plan';
var plan = which === 'serp' ? cfg.serp.plan : cfg.aiinsights.plan;
$http.get(url, { params: plan }).then(function (res) {
self[which + 'Price'] = res.data;
});
},
// ---------------------------------------------------------------- afbeeldingen uploaden
uploading: {},
upload: function (input, field, set) {
var self = this, file = input.files && input.files[0];
if (!file) { return; }
var fd = new FormData();
fd.append('file', file);
fd.append('field', field);
$scope.$apply(function () { self.uploading[field] = true; self.uploadError = null; });
$http.post('/rest/reseller/upload', fd, { headers: { 'Content-Type': undefined }, transformRequest: angular.identity }).then(function (res) {
self.uploading[field] = false;
set(res.data.url);
input.value = '';
}, function (res) {
self.uploading[field] = false;
self.uploadError = t('whitelabel.style.upload_error.' + ((res.data && res.data.error) || 'upload'));
input.value = '';
});
},
// ---------------------------------------------------------------- team
addMember: function () {
var self = this;
$http.post('/rest/reseller/reseller.admin.add', self.page.newadmin).then(function () { self.page.newadmin = {}; self.load(); });
},
removeMember: function (u) {
var self = this;
if (!window.confirm(t('whitelabel.team.remove_confirm'))) { return; }
$http.post('/rest/reseller/reseller.admin.delete', u).then(function () { self.load(); });
},
// ---------------------------------------------------------------- e-mail, facturen
testMail: function () {
var self = this;
self.page.testSent = false;
$http.post('/rest/reseller/reseller.email.test', { email: self.page.testemail }).then(function () { self.page.testSent = true; });
},
invoice: function (key) { $scope.fnGoto('/rest/pay/invoice.link?type=reseller&key=' + key); },
startBilling: function (method, currency) { $scope.fnGoto('/rest/pay/reseller.start?method=' + method + '¤cy=' + currency); },
resetBilling: function () {
var self = this;
self.page.resetWarning = false;
$http.post('/rest/pay/reseller.reset', self.r).then(function () { self.load(); });
},
backup: function () { $scope.fnGoto('/rest/linkbuilding/backup.reseller'); }
};
$translate.onReady().then(function () { w.init(); });
}
]);
app.controller('insights', ['$scope', '$timeout', '$http', '$rootScope', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $filter, h) {
var colorindex = 0;
$scope.page = {
tab: 'managers'
}
$scope.config = {
analytics: {
name: 'analytics visitors',
dataset: 'analytics',
metrickey: 'analytics',
metrics: ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalValueAll', 'goalConversionRateAll', 'goalCompletionsAll']
},
adwords: {
name: 'adwords',
dataset: 'adwords',
metrickey: 'googlexads',
metrics: ['Clicks', 'Impressions', 'Cost', 'AverageCpc', 'Ctr', 'Conversions', 'CostPerConversion', 'ConversionRate', 'ConversionValue', 'roas']
},
searchconsole: {
name: 'searchconsole',
dataset: 'searchconsole',
metrickey: 'searchconsole',
metrics: ['clicks', 'position', 'impressions', 'ctr']
}
};
$scope.report = $scope.config[$scope.urlreport];
$scope.gloMetrics = h.metrics($scope.report.metrickey);
/* TEMP => RULES SHOULD BE FETCHED!!*/
var _rules = [{
'metric': 'avgSessionDuration',
'comp': 'lt',
'val': 70
}, {
'metric': 'bounceRate',
'comp': 'gt',
'val': 60
}, {
'metric': 'sessions',
'comp': 'drop',
'val': 30
}];
var rules = _rules.reduce((acc, item) => {
acc[item.metric] = item;
return acc;
}, {});
$scope.ruleopts = [{ v: 'gt', n: 'greater then (abs value)' },{ v: 'lt', n: 'less then (abs value)' },{ v: 'drop', n: 'drop (percentage)' },{ v: 'bump', n: 'increase (percentage)' }]
$scope.rules = _rules;
$scope.fnMatchesRule = function(metric, value, valueprev) {
var rule = $scope.rules.find(function(o) { return o.metric === metric });
if (rule) {
console.log(rule);
// less then abs value
if (rule.comp == 'lt' && value < rule.val) {
return 'bg-warning'
}
if (rule.comp == 'gt' && value > rule.val) {
return 'bg-warning'
}
if (rule.comp == 'drop' && (((valueprev - value) / value) * 100) > rule.val) {
return 'bg-warning'
}
return 'bg-white rulepass';
} else {
return 'bg-white norule';
}
}
// filter sites per user
var fnFilterUser = function(id) {
//var sites = angular.copy($scope.sites);
sites = $scope.sites.filter(function(v) {
for (var i in v.users) {
if (v.users[i].user_id.$oid == id.$oid) {
return true
}
}
return false;
});
return sites;
}
$scope.fnPctChange = function(o, n, reversesign) {
if (n == 0) {
return '0% ';
}
var pct = ((n - o) / o) * 100;
var npct = $filter('number')(pct, 2);
if (reversesign) {
if (pct < 0) {
return '' + npct + '% ';
}
return '+ ' + npct + '% ';
}
if (pct < 0) {
return '' + npct + '% ';
}
return '+ ' + npct + '% ';
}
$http.get("/rest/insights/list", {
}).then(function successCallback(res) {
$scope.sites = res.data;
$scope.avg = fnCalcAvg($scope.sites);
// get chart data
$http.get('/rest/reseller/insights.get').then(function successCallback(res) {
$scope.insights = res.data[$scope.report.dataset];
fnLineChart('c1', $scope.insights, 'date', $scope.report.metrics[0]);
fnLineChart('c2', $scope.insights, 'date', $scope.report.metrics[1]);
});
// list users per reseller
$http.get('/rest/reseller/users.list').then(function successCallback(res) {
$scope.users = res.data;
for (var i in $scope.users) {
$scope.users[i]['avg'] = fnCalcAvg(fnFilterUser($scope.users[i]._id));
}
});
});
$scope.fnSwitchMetric = function(metric) {
fnLineChart('c2', $scope.insights, 'date', metric);
}
$scope.colors = ['#E91E63', '#3F51B5', '#00BCD4', '#8BC34A', '#FF9800', '#795548', '#607D8B', '#00CC00', '#0000CC', '#DDDDDD', '#999999', '#333333', '#990000']
$scope.pagecharts = {};
var fnLineChart = function(chart, dataset, dimension, metric) {
$scope.pagecharts[chart] = {
metric: metric,
data: dataset,
type: 'serial',
theme: 'light',
marginLeft: 0,
marginRight: 0,
marginTop: 10,
fontFamily: 'Roboto',
categoryField: dimension,
rotate: false,
pathToImages: 'vendor/amchart/images/',
dataDateFormat: 'YYYYMMDD',
categoryAxis: {
gridPosition: "start",
parseDates: true,
gridThickness: 0
},
valueAxes: [{
position: "left",
axisAlpha: 0,
labelsEnabled: true,
dashLength: 4,
color: '#dedede',
}],
graphs: [{
balloonText: $filter('translate')('metrics.' + metric) + " [[value]] ",
"bullet": "round",
"bulletBorderAlpha": 1,
"bulletColor": "#FFFFFF",
"useLineColorForBulletBorder": true,
"fillAlphas": 0.2,
"lineThickness": 2,
"lineAlpha": 1,
"bulletSize": 7,
"title": "Expenses",
"valueField": metric,
lineColor: $scope.colors[colorindex],
}],
}
colorindex += 1;
}
var fnCalcAvg = function(sites) {
var dataset = $scope.report.dataset;
// CALCULATE AVERAGE
return sites.reduce((acc, item) => {
if (typeof item.insights[dataset] != 'undefined') {
for (var y in [0, 1]) {
if (typeof item.insights[dataset][y] != 'undefined') {
for (var i in item.insights[dataset][y]) {
if (typeof item.insights[dataset][y][i] != 'undefined') {
acc[i] = acc[i] || [{ 'n': 0, 's': 0 }, { 'n': 0, 's': 0 }];
acc[i][y].s = acc[i][y].s + item.insights[dataset][y][i];
acc[i][y].n = acc[i][y].n + 1;
}
}
}
}
}
return acc;
}, {});
}
}
]);
app.controller('mcp', ['$scope', '$rootScope', '$timeout', '$location', 'h', function ($scope, $rootScope, $timeout, $location, h) {
// Visibility comes from user.site.show.mcp (reseller tools config, with a
// non-guest default when the key is not set). See permission::show().
if (!$rootScope.user || !$rootScope.user.site || !$rootScope.user.site.show || !$rootScope.user.site.show.mcp) {
$location.path('/');
return;
}
// The endpoint is always this host. The reseller is resolved from
// HTTP_HOST server-side, so a white-label customer connects to their own
// domain and needs no extra configuration.
$scope.mcp = {
host: window.location.host,
endpoint: window.location.protocol + '//' + window.location.host + '/rest/mcp/server',
skillRepo: 'https://github.com/corewebvitals/seo-superpowers',
// Claude Code plugin marketplace (see repo .claude-plugin/marketplace.json)
marketplaceAdd: '/plugin marketplace add corewebvitals/seo-superpowers',
marketplaceInstall: '/plugin install seo-superpowers@seo-superpowers',
marketplaceReload: '/reload-plugins',
revealed: false,
copied: ''
};
$scope.fnKey = function () {
return ($rootScope.user && $rootScope.user.key) ? $rootScope.user.key : '';
};
// Masked by default: the key is a permanent credential and this page gets
// screen-shared and screenshotted.
$scope.fnKeyMasked = function () {
var k = $scope.fnKey();
if (!k) { return ''; }
return k.substring(0, 8) + '••••••••••••••••••••••••••••••••' + k.substring(k.length - 4);
};
$scope.fnConfig = function () {
return JSON.stringify({
mcpServers: {
marketingtracer: {
type: 'http',
url: $scope.mcp.endpoint,
headers: { Authorization: 'Bearer ' + ($scope.mcp.revealed ? $scope.fnKey() : 'YOUR_API_KEY') }
}
}
}, null, 2);
};
$scope.fnCopy = function (what, text) {
var done = function () {
$scope.mcp.copied = what;
$timeout(function () { $scope.mcp.copied = ''; }, 2000);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
$scope.$apply(done);
});
return;
}
// Older browsers: fall back to a throwaway textarea.
var ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
done();
};
$scope.tools = [
{ name: 'list_sites', what: 'Find your websites and the site_id every other tool needs, plus which integrations are connected.' },
{ name: 'page_brief', what: 'Everything about one page at once: the keywords it ranks for, whether they are rising or falling, its traffic, and its on-page issues.' },
{ name: 'find_opportunities', what: 'The pages worth fixing first, ranked by optimisation potential and weighed against real search demand.' },
{ name: 'diagnose_traffic_drop', what: 'Compares a period against the one before it, says which pages lost the traffic, and whether anything was edited on them.' },
{ name: 'find_content_decay', what: 'Pages that have been sliding for months rather than dropping in one go. The decline a month-on-month view never shows.' },
{ name: 'find_content_gaps', what: 'Queries you already appear for and do not win: real demand, ranking below page one.' },
{ name: 'find_cannibalization', what: 'Queries where two or more of your pages show up for the same search term.' },
{ name: 'cluster_performance', what: 'How each section of the site performs, by URL folder: how many pages it holds, how many earn anything, and whether it is growing.' },
{ name: 'get_search_console', what: 'Search Console clicks, impressions, CTR and position, for the whole site or one page.' },
{ name: 'get_analytics', what: 'Google Analytics 4 sessions, users, conversions and revenue, for the whole site or one page.' },
{ name: 'get_scan', what: 'On-page SEO scan results: which rules fail across the site, or one page\'s issues with how to fix them.' },
{ name: 'get_ai_check', what: 'AI readiness: which AI crawlers can read the site, and per page how much the text reads like stock AI writing (beta).' }
];
// ---- demo terminal ------------------------------------------------------
//
// Example questions are the demo: clicking one runs it. Figures are invented
// and the footer says so. A convincing fake of the user's own data would
// contradict the promise that these tools never invent numbers.
var scenarios = [
{
// Shape matches a real diagnose_traffic_drop answer: site-wide may
// not be “down”; the pain is one money page + a soft section.
prompt: 'Why is our organic traffic down?',
chip: 'Why is traffic down?',
tools: [
{ name: 'diagnose_traffic_drop', detail: '28 days vs the 28 before' },
{ name: 'find_content_decay', detail: '6 months on the main loser' },
{ name: 'page_brief', detail: '/services/warehouse-coating' },
{ name: 'get_analytics', detail: 'sessions same windows' }
],
answer: [
{ t: ' site-wide (28d vs prior) recent prior change', dim: true },
{ t: ' GSC clicks 1,110 1,105 +0.5%', dim: true },
{ t: ' GSC impressions 295,000 540,000 −45%', dim: true },
{ t: ' GA sessions 4,900 4,300 +14%', dim: true },
{ t: '' },
{ t: 'Clicks are flat and sessions are up. The chart looks bad because', em: true },
{ t: 'impressions dropped. That is not the same as fewer visits.', em: true },
{ t: '' },
{ t: ' largest click losses', dim: true },
{ t: ' /services/warehouse-coating 402 → 349 pos 6.1 → 9.1', dim: true },
{ t: ' /guides/surface-prep 20 → 10', dim: true },
{ t: ' /blog/* (several) −5 to −9 each', dim: true },
{ t: '' },
{ t: 'Most of the real loss is one page. Over six months' },
{ t: '/services/warehouse-coating went 880 → 600 clicks and drifted' },
{ t: 'from position ~4 to ~6. It is missing a meta description; head' },
{ t: 'terms still get good CTR where you rank high.' },
{ t: '' },
{ t: 'What to do: update that service page (content + meta), check who' },
{ t: 'outranked you, and only then look at the small blog dips. A' },
{ t: 'site-wide rewrite is not justified by this data.' }
]
},
{
prompt: 'We rank on page 1 for a lot of keywords but barely get any clicks. Why?',
chip: 'Ranking but no clicks?',
tools: [
{ name: 'get_search_console', detail: 'queries by impressions, 90 days' },
{ name: 'page_brief', detail: 'the four commercial URLs behind them' },
{ name: 'get_scan', detail: 'titles on those pages' }
],
answer: [
{ t: ' query pos impr CTR typical', dim: true },
{ t: ' epoxy floor coating 3.1 22,100 2.4% ~11%', dim: true },
{ t: ' warehouse floor paint 2.8 14,800 2.9% ~13%', dim: true },
{ t: ' industrial floor cost 4.0 9,200 1.8% ~8%', dim: true },
{ t: '' },
{ t: 'You rank where you want. People are not clicking. At those' },
{ t: 'positions you would usually see roughly 8-12% CTR.' },
{ t: '' },
{ t: 'All four titles start with the company name, then the keyword.' },
{ t: 'They were rewritten in the same February release. Pages you did' },
{ t: 'not touch still sit near a normal click rate.' },
{ t: '' },
{ t: 'What to do: put the keyword first in the title, brand at the end,' },
{ t: 'and test that as one template change. Also glance at the SERP for' },
{ t: 'AI overviews on the more informational queries, but fix titles first.' }
]
},
{
prompt: 'What keywords do we show up for but never really rank for?',
chip: 'Keywords we almost rank for?',
tools: [
{ name: 'find_content_gaps', detail: 'impressions, average position worse than 8' },
{ name: 'get_search_console', detail: 'which URL ranks for the top gaps' }
],
answer: [
{ t: ' query pos impr clicks', dim: true },
{ t: ' commercial epoxy flooring 11.2 18,400 38', dim: true },
{ t: ' anti slip warehouse floor 12.4 11,200 22', dim: true },
{ t: ' resin floor cost calculator 14.1 8,600 14', dim: true },
{ t: ' epoxy vs polyurethane floor 10.8 7,100 31', dim: true },
{ t: '' },
{ t: 'You already get seen for these. You are just not on page one.' },
{ t: '' },
{ t: '“commercial epoxy flooring” is the priority: most impressions,' },
{ t: 'and /services/warehouse-coating is already the URL that ranks for' },
{ t: 'it. Strengthen that page (content, internal links from guides),' },
{ t: 'do not start a second page for the same intent.' },
{ t: '' },
{ t: '“resin floor cost calculator” has no good match on the site.' },
{ t: 'That one may need a new page. Check the other two queries against' },
{ t: 'existing product URLs before writing anything new.' }
]
},
{
prompt: 'Which pages used to do well and are slowly dying?',
chip: 'Pages that are slowly dying?',
tools: [
{ name: 'find_content_decay', detail: '6 months, period by period' },
{ name: 'page_brief', detail: 'the three biggest losses' }
],
answer: [
{ t: 'Three pages. No single crash. A few percent down every month.' },
{ t: '' },
{ t: ' /guides/surface-prep', dim: true },
{ t: ' 880 → 810 → 740 → 690 → 640 → 600 clicks (−31%)', dim: true },
{ t: ' Position ~4.1 → ~4.6. Impressions are what is falling.', dim: true },
{ t: '' },
{ t: ' /blog/floor-prep-checklist same shape, −28%', dim: true },
{ t: ' /blog/moisture-testing same shape, −24%', dim: true },
{ t: '' },
{ t: 'Position is almost flat, so this is not a sudden ranking collapse.' },
{ t: 'The queries that used to hit these pages are drying up, and two' },
{ t: 'newer guides now rank for terms surface-prep used to own.' },
{ t: '' },
{ t: 'What to do: refresh surface-prep first (highest absolute loss).' },
{ t: 'Merge the two overlapping prep posts into one strong guide and' },
{ t: '301 the weaker URL. Do that before publishing more in the same topic.' }
]
},
{
prompt: 'We redesigned the site last month. Did we lose anything important?',
chip: 'Did the redesign hurt us?',
tools: [
{ name: 'diagnose_traffic_drop', detail: '4 weeks after launch vs 4 weeks before' },
{ name: 'get_scan', detail: 'indexability and sitemap coverage' },
{ name: 'get_search_console', detail: 'top URLs that disappeared' }
],
answer: [
{ t: ' net clicks about −6%, but two opposite moves:', dim: true },
{ t: ' /guides/* +380 clicks new URLs indexed fine', dim: true },
{ t: ' /old-blog/* −860 clicks 41 URLs missing from the sitemap', dim: true },
{ t: '' },
{ t: 'Those 41 still return 200 and still have internal links. They were' },
{ t: 'dropped from the sitemap, not redirected. Six of them were in the' },
{ t: 'top 5 before the cutover and make up most of the 860 lost clicks.' },
{ t: '' },
{ t: 'What to do: restore or 301 those six first. Put them back in the' },
{ t: 'sitemap or redirect each to the new URL that replaced it. The other' },
{ t: '35 were under 10 clicks a month between them. Clean them up later.' },
{ t: '' },
{ t: 'The new guides section is fine. Do not roll back the redesign for this.' }
]
},
{
prompt: 'Is our blog actually bringing traffic, or is it just sitting there?',
chip: 'Is the blog doing anything?',
tools: [
{ name: 'cluster_performance', detail: 'pages by section, 90 days vs prior 90' },
{ name: 'get_search_console', detail: 'top URLs under /blog' }
],
answer: [
{ t: ' section pages with clicks clicks vs prev 90d', dim: true },
{ t: ' /guides 14 13 3,180 +18%', dim: true },
{ t: ' /products 412 46 1,940 −4%', dim: true },
{ t: ' /blog 308 19 210 −31%', dim: true },
{ t: '' },
{ t: 'The blog is large and almost empty. 308 pages, 19 with any clicks,' },
{ t: 'and two of those 19 carry most of the 210. The section is down 31%.' },
{ t: '' },
{ t: 'Fourteen guides out-earn the whole blog and are growing.' },
{ t: '' },
{ t: 'What to do: double down on the two blog posts that work (update,' },
{ t: 'internal links, better titles). Stop publishing new posts until those' },
{ t: 'are solid. Noindex or merge the long tail that earns nothing. Put the' },
{ t: 'next content effort into /guides, where the same work pays off more.' }
]
},
{
// Sprint prioritisation: data + issue → fix. Invented generic paths
// (same style as the other demos, not a real site).
prompt: 'What SEO work should we do this sprint?',
chip: 'What SEO work this sprint?',
tools: [
{ name: 'find_opportunities', detail: 'OPI × search demand, 90 days' },
{ name: 'find_content_decay', detail: '6 months, period by period' },
{ name: 'page_brief', detail: 'top candidates from the list' },
{ name: 'get_analytics', detail: 'top sessions by path' }
],
answer: [
{ t: ' page clicks impr pos sessions', dim: true },
{ t: ' /services/warehouse-coating 880 18.4k 8.2 640', dim: true },
{ t: ' /guides/surface-prep 604 9.1k 4.6 510', dim: true },
{ t: ' /blog/epoxy-vs-polyurethane 12 11.2k 14.1 40', dim: true },
{ t: ' /products/anti-slip-paint 41 7.2k 9.0 95', dim: true },
{ t: '' },
{ t: '1. /services/warehouse-coating (best ROI)', em: true },
{ t: ' Page two for a commercial query with real demand. Add a proper' },
{ t: ' meta description, expand the comparison table, and link to it' },
{ t: ' from the relevant guides so it can break page one.' },
{ t: '' },
{ t: '2. /guides/surface-prep (slow decline)', em: true },
{ t: ' Down every month for half a year while position holds. Refresh' },
{ t: ' the guide and merge the near-duplicate post so one page owns the topic.' },
{ t: '' },
{ t: '3. /blog/epoxy-vs-polyurethane (indexation)', em: true },
{ t: ' 11k impressions but marked not indexable. Check noindex, robots' },
{ t: ' and canonical; fix that before rewriting the article.' },
{ t: '' },
{ t: '4. /products/anti-slip-paint (titles)', em: true },
{ t: ' CTR ~2% at position 9 is low. Lead the title with the product' },
{ t: ' keyword, not the brand, and write a clearer meta.' },
{ t: '' },
{ t: 'Leave /careers and other high-OPI pages with no search demand alone' },
{ t: 'until the four above are done.' }
]
},
{
// find_cannibalization: query×page pairs, multiple URLs per query.
prompt: 'Are any of our pages competing for the same keywords?',
chip: 'Pages competing for keywords?',
tools: [
{ name: 'find_cannibalization', detail: 'query + page pairs, 90 days' }
],
answer: [
{ t: ' query pages impr clicks', dim: true },
{ t: ' commercial epoxy flooring 3 18.4k 41', dim: true },
{ t: ' warehouse floor coating 2 11.2k 28', dim: true },
{ t: ' epoxy vs polyurethane 2 7.1k 19', dim: true },
{ t: '' },
{ t: '1. commercial epoxy flooring', em: true },
{ t: ' /services/warehouse-coating pos 8.2 · most impressions' },
{ t: ' /blog/epoxy-vs-polyurethane pos 14 · overlapping intent' },
{ t: ' /products/epoxy-pro pos 18 · thin product page' },
{ t: ' Pick one commercial URL (usually the service page), retarget the' },
{ t: ' blog as a supporting guide, and link both to that winner.' },
{ t: '' },
{ t: '2. warehouse floor coating', em: true },
{ t: ' Service page and a guide both show up. Keep the service page as' },
{ t: ' the main result; make the guide clearly "how to" and link up.' },
{ t: '' },
{ t: 'Do not 301 everything automatically. Check intent first: product,' },
{ t: 'service and guide can share a theme without being true duplicates.' }
]
}
];
// Short labels for the buttons; the terminal types the full question.
$scope.examples = scenarios.map(function (s) { return s.chip; });
var timers = [];
var at = function (ms, fn) { timers.push($timeout(fn, ms)); };
// The panel is a fixed height and scrolls, so it has to follow the output.
var follow = function () {
timers.push($timeout(function () {
var el = document.getElementById('mcpTerminal');
if (el) { el.scrollTop = el.scrollHeight; }
}));
};
$scope.demo = { prompt: '', tools: [], answer: [], running: false, index: 0 };
$scope.fnDemoRun = function (which) {
angular.forEach(timers, function (t) { $timeout.cancel(t); });
timers = [];
var s = scenarios[which % scenarios.length];
$scope.demo = { prompt: '', tools: [], answer: [], running: true, index: which % scenarios.length };
var el = document.getElementById('mcpTerminal');
if (el) { el.scrollTop = 0; }
var clock = 0;
// Type the question a character at a time.
for (var i = 1; i <= s.prompt.length; i++) {
(function (n) {
at(clock += 18, function () { $scope.demo.prompt = s.prompt.substring(0, n); });
})(i);
}
// Each tool appears, spins, then settles.
angular.forEach(s.tools, function (tool) {
at(clock += 380, function () {
$scope.demo.tools.push({ name: tool.name, detail: tool.detail, done: false });
follow();
});
at(clock += 700, function () {
$scope.demo.tools[$scope.demo.tools.length - 1].done = true;
});
});
// Then the answer, line by line.
clock += 350;
angular.forEach(s.answer, function (line) {
at(clock += (line.t === '' ? 45 : 150), function () {
$scope.demo.answer.push(line);
follow();
});
});
at(clock += 400, function () { $scope.demo.running = false; });
};
$scope.$on('$destroy', function () {
angular.forEach(timers, function (t) { $timeout.cancel(t); });
timers = [];
});
$scope.fnDemoRun(0);
}]);
app.controller('uptime', ['$scope', '$rootScope', '$http', '$location', '$translate', 'h', function ($scope, $rootScope, $http, $location, $translate, h) {
// Zichtbaar via user.site.show.uptime (reseller tools config, standaard aan). Zie permission::show().
if (!$rootScope.user || !$rootScope.user.site || !$rootScope.user.site.show || !$rootScope.user.site.show.uptime) {
$location.path('/');
return;
}
$scope.oUptime = {
loading: false,
saving: false,
data: null,
chart: null,
init: function () {
var self = this;
self.loading = true;
$http.get('/rest/uptime/get', { params: { websiteid: h.siteId() } }).then(function (res) {
self.data = res.data;
self.chart = self.fnChart(res.data.series || []);
self.loading = false;
}, function () {
self.loading = false;
});
},
fnSet: function (enabled) {
var self = this;
self.saving = true;
$http.post('/rest/uptime/set', { websiteid: h.siteId(), enabled: enabled }).then(function () {
self.saving = false;
self.init();
}, function () {
self.saving = false;
});
},
// reactietijd per check; storingen zonder antwoord tellen niet mee
fnChart: function (series) {
var data = [];
angular.forEach(series, function (s) {
if (s.ttfb !== null && s.outcome !== 'down') {
data.push({ date: new Date(s.at), ttfb: s.ttfb });
}
});
if (!data.length) {
return null;
}
return {
data: data,
type: 'serial',
categoryField: 'date',
categoryAxis: { parseDates: true, minPeriod: 'mm', gridThickness: 0 },
valueAxes: [{ position: 'left', axisAlpha: 0, dashLength: 4, color: '#9ca3af', unit: ' ms', minimum: 0 }],
graphs: [{
valueField: 'ttfb',
type: 'line',
lineColor: '#3b82f6',
fillAlphas: 0.1,
lineThickness: 2,
balloonText: 'TTFB [[value]] ms '
}]
};
},
fnDate: function (ms, withTime) {
if (!ms) {
return '';
}
var d = new Date(ms);
var opts = { weekday: 'short', day: 'numeric', month: 'short' };
if (d.getFullYear() !== new Date().getFullYear()) {
opts.year = 'numeric';
}
if (withTime) {
opts.hour = '2-digit';
opts.minute = '2-digit';
}
return d.toLocaleString($translate.use(), opts);
},
fnDuration: function (inc) {
var end = inc.end || Date.now();
var min = Math.max(30, Math.round((end - inc.start) / 60000));
if (min < 60) {
return min + ' min';
}
// eenheden uit de vertaling (nl "u", en "h"), het getal in de taal van de app
var hrs = Math.round(min / 6) / 10;
if (hrs < 48) {
return hrs.toLocaleString($translate.use()) + ' ' + $translate.instant('uptime.unit_hour');
}
return Math.round(hrs / 24) + ' ' + $translate.instant('uptime.unit_day');
},
fnAgo: function (ms) {
if (!ms) {
return '';
}
return Math.max(1, Math.round((Date.now() - ms) / 60000));
},
fnCertDays: function () {
if (!this.data || !this.data.cert_expires) {
return null;
}
return Math.floor((this.data.cert_expires - Date.now()) / 86400000);
},
fnBarClass: function (bar) {
return {
up: 'bg-green-500',
warn: 'bg-amber-400',
blocked: 'bg-amber-400',
down: 'bg-red-500'
}[bar.outcome] || 'bg-gray-200';
},
fnBarTitle: function (bar) {
var t = this.fnDate(bar.at, true);
if (!bar.outcome) {
return t;
}
var s = $translate.instant('uptime.state.' + bar.outcome);
if (bar.outcome !== 'up' && (bar.status || bar.error)) {
s += ': ' + (bar.status || bar.error);
}
return t + ' · ' + s;
},
fnReason: function (inc) {
if (inc.mitigated) {
return inc.status + ', ' + $translate.instant('uptime.firewall');
}
if (inc.error) {
return inc.error_text || inc.error;
}
return inc.status ? String(inc.status) : '';
}
};
$scope.oUptime.init();
}]);
// Korte verwijzing naar de firewallinstellingen, op elke pagina waar de MarketingTracer-bot geblokkeerd
// kan worden (uptime, SEO-scan): .
// De tekst noemt de tool ("onze uptime-monitor"), niet "MarketingTracer": in white label is dat niet de naam.
// Altijd zichtbaar; de link alleen voor wie de instellingen mag zien (rol 5 of lager).
app.directive('botFirewallRef', ['$rootScope', function ($rootScope) {
return {
restrict: 'E',
scope: { blocked: '=?', tool: '@' },
template: '',
link: function (scope) {
scope.canEdit = function () {
return $rootScope.user && $rootScope.user.site && $rootScope.user.site.role < 6;
};
}
};
}]);
// Site-instellingen (/settings/site#firewall): de geheime waarde van de site en de firewallregels voor
// de MarketingTracer-bot (SEO-scan en uptime). Zie rest/include/bot.php.
app.controller('botfirewall', ['$scope', '$http', '$location', '$anchorScroll', '$timeout', 'h', function ($scope, $http, $location, $anchorScroll, $timeout, h) {
$scope.oFirewall = {
data: null,
revealed: false,
copied: '',
// Gemaskeerd tot je op "tonen" klikt: deze pagina wordt gedeeld en gescreenshot.
fnMasked: function (token) {
return token ? token.substring(0, 4) + '••••••••••••••••••••••' + token.substring(token.length - 4) : '';
},
fnMaskRule: function (rule) {
var t = this.data && this.data.token;
return t ? rule.split(t).join(this.fnMasked(t)) : rule;
},
fnCopy: function (what, text) {
var self = this;
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function () {
$scope.$apply(function () {
self.copied = what;
});
});
}
}
};
$http.get('/rest/bot/firewall', { params: { websiteid: h.siteId() } }).then(function (res) {
$scope.oFirewall.data = res.data;
// link vanaf /uptime en de scan: /settings/site#firewall
if ($location.hash() === 'firewall') {
$timeout(function () {
$anchorScroll('firewall');
});
}
});
}]);
// Wat bewaard blijft tussen de drie pagina's (/serp, /serp/keywords, /serp/competitors): de laatst opgehaalde
// gegevens (direct tonen, op de achtergrond vernieuwen), ingeklapte groepen, en een filter dat een klik op het
// overzicht meegeeft aan de zoekwoordenpagina.
var serpState = { site: null, data: null, collapsed: {}, pending: null };
app.controller('serp', ['$scope', '$rootScope', '$location', '$routeParams', '$http', '$translate', '$timeout', 'h', function ($scope, $rootScope, $location, $routeParams, $http, $translate, $timeout, h) {
// Posities in Google (SERP-tracker). Ontwerp: docs/superpowers/specs/2026-10-27-serp-tracker-design.md,
// server: rest/data/serp/ en rest/include/serp.php. Nog niet in het menu: zichtbaar via user.site.show.serp.
// De overzichtscijfers (zichtbaarheid, verdeling, stijgers) rekent de browser uit de posities per maand.
// in een rapport (newlayout/serp/report.html): de maand van het rapport, alleen lezen
var report = $scope.custominit && $scope.custominit.serp ? $scope.custominit : null;
if (!report && (!$rootScope.user || !$rootScope.user.site || !$rootScope.user.site.show || !$rootScope.user.site.show.serp)) {
$location.path('/');
return;
}
// CTR per positie (organisch, ruwe curve) voor de zichtbaarheid
var CTR = [0, 0.28, 0.15, 0.10, 0.07, 0.05, 0.04, 0.03, 0.025, 0.02, 0.018, 0.012, 0.011, 0.010, 0.009, 0.008, 0.007, 0.006, 0.005, 0.005, 0.004];
var COLORS = { own: '#2563eb', own2: '#93c5fd', other: '#94a3b8', c: ['#f59e0b', '#10b981', '#ef4444', '#8b5cf6', '#64748b', '#ec4899'] };
var ROW = 24;
var FREQS = ['monthly', 'biweekly', 'weekly', 'alternate', 'daily'];
var KEYWORD_STEPS = [100, 150, 200, 250, 300, 400, 500, 750, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 7500, 10000];
var DEPTHS = [20, 30, 50, 100];
// zelfde lijst als serp::LOCATIONS (rest/include/serp.php)
var COUNTRIES = ['nl', 'be', 'de', 'uk', 'us', 'fr', 'at', 'ch', 'lu', 'it', 'es', 'pt', 'se', 'dk', 'fi', 'ie', 'pl', 'cz', 'sk', 'hu', 'ro', 'bg', 'hr', 'si', 'gr', 'tr', 'lv', 'lt', 'ee', 'ca', 'au', 'ae', 'jp', 'id'];
function last(a) { return a[a.length - 1]; }
// series: [{values:[positie|null], color, width, own}] -> svg-paden; positie 1 bovenaan (of omhoog als invert false)
function lineChart(series, opts) {
var w = opts.w, hgt = opts.h, n = opts.n, pad = opts.pad === undefined ? 4 : opts.pad, padX = opts.padX === undefined ? pad : opts.padX;
var max = opts.max, min = opts.min, invert = opts.invert !== false;
function x(i) { return n > 1 ? padX + i * (w - 2 * padX) / (n - 1) : w / 2; }
function y(v) { var t = (v - min) / (max - min); return pad + (invert ? t : 1 - t) * (hgt - 2 * pad); }
function smooth(pts) {
var d = 'M' + pts[0].x.toFixed(1) + ' ' + pts[0].y.toFixed(1);
for (var i = 0; i < pts.length - 1; i++) {
var p0 = pts[i - 1] || pts[i], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] || p2;
d += ' C' + [p1.x + (p2.x - p0.x) / 6, p1.y + (p2.y - p0.y) / 6, p2.x - (p3.x - p1.x) / 6, p2.y - (p3.y - p1.y) / 6, p2.x, p2.y].map(function (v) { return v.toFixed(1); }).join(' ');
}
return d;
}
return series.map(function (s) {
var segs = [], cur = [], dots = [];
(s.values || []).forEach(function (v, i) {
if (v === null || v === undefined) { if (cur.length) { segs.push(cur); } cur = []; return; }
var pt = { x: x(i), y: y(v), v: v };
cur.push(pt); dots.push(pt);
});
if (cur.length) { segs.push(cur); }
return { d: segs.map(smooth).join(' '), color: s.color, width: s.width || 2, own: s.own, dots: dots, last: last(dots), label: s.label, raw: s.raw || s.values };
});
}
$scope.oSerp = {
loading: true,
data: null,
rows: [],
months: [],
competitors: [],
countries: COUNTRIES,
colors: COLORS,
freqs: FREQS,
depths: DEPTHS,
keywordSteps: KEYWORD_STEPS,
ROW: ROW,
rowsAxis: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
// de pagina komt uit het adres, zodat verversen je op dezelfde pagina laat
tab: report ? report.urlview : (['keywords', 'competitors'].indexOf($routeParams.tab) > -1 ? $routeParams.tab : 'overview'),
report: !!report,
filter: { q: '', group: '', star: false, device: '', cannibal: false },
sort: 'pos',
add: { show: false, text: '', cc: 'nl', device: 'desktop', group: '' },
bulk: { group: '' },
message: '',
newCompetitor: '',
settings: null,
v: {},
init: function () {
var self = this;
if (report) {
// de maand van het rapport (reportdates.end in seconden), als YYYYMM
var end = new Date((($rootScope.reportdates || {}).end || Date.now() / 1000) * 1000);
var month = '' + end.getFullYear() + ('0' + (end.getMonth() + 1)).slice(-2);
$http.get('/rest/serp/get', { params: { websiteid: h.siteId(), month: month } }).then(function (res) {
self.load(res.data);
}, function () {
self.loading = false;
self.failed = true;
});
return;
}
if (serpState.site === h.siteId() && serpState.data && !self.data) {
self.load(serpState.data);
}
$http.get('/rest/serp/get', { params: { websiteid: h.siteId() } }).then(function (res) {
serpState.site = h.siteId();
serpState.data = res.data;
self.load(res.data);
}, function () {
self.loading = false;
});
},
// naar een andere pagina; filter of toevoegen gaat mee naar de zoekwoordenpagina
fnGo: function (tab, pending) {
serpState.pending = pending || null;
$location.path(tab === 'overview' ? '/serp' : '/serp/' + tab);
},
load: function (d) {
var self = this;
// nog leeg en Search Console gekoppeld: eenmalig vullen met de 10 grootste zoekwoorden, daarna opnieuw laden
if (d.can_autofill && !self.autofilling && !report) {
self.autofilling = true;
$http.post('/rest/serp/autofill', { websiteid: h.siteId() }).then(function (res) {
if (res.data.added) { self.init(); } else { self.autofilling = false; }
}, function () { self.autofilling = false; });
}
var open = {};
angular.forEach(self.rows, function (r) { if (r.open) { open[r.id] = r.detail; } });
self.data = d;
self.months = (d.months || []).map(function (ym) { return new Date(+ym.substr(0, 4), +ym.substr(4, 2), 0); });
self.competitors = d.competitors || [];
self.rows = (d.keywords || []).map(function (k) {
var urls = Object.keys(k.urls || {}).map(function (u) { return { url: u, series: k.urls[u] }; });
urls.sort(function (a, b) { var x = last(a.series), y = last(b.series); return (x === null ? 99 : x) - (y === null ? 99 : y); });
return angular.extend(k, { urls: urls, comp: k.comp || {}, open: !!open.hasOwnProperty(k.id), detail: open[k.id] || null, selected: false });
});
self.loading = false;
self.refresh();
},
// alles wat ng-repeat toont en nieuw berekend wordt, één keer per wijziging (anders een eindeloze digest)
refresh: function () {
var self = this;
angular.forEach(self.rows, function (r) { r._chart = null; r._serp = null; r._spark = null; r._urlRows = null; });
self.v = {
summary: self.fnSummary(),
groups: self.fnGroupStats(),
comp: self.fnCompStats(),
distribution: self.fnDistribution(),
visChart: self.fnVisChart(),
vs: self.fnVs(),
up: self.fnMovers(true),
down: self.fnMovers(false),
anyMeasured: self.rows.some(function (r) { return self.fnLastIdx(r) > -1; }),
hasPrev: self.rows.some(function (r) { return self.fnPrev(r) !== undefined; }),
// rapport, zoekwoorden: alles op positie
reportRows: report ? self.fnFiltered() : null
};
self.v.groupMap = {};
angular.forEach(self.fnGroupStats(true), function (g) { self.v.groupMap[g.name] = g; });
self.rowsVersion = (self.rowsVersion || 0) + 1;
},
// --- posities per zoekwoord: alleen gemeten maanden tellen (null = niet in de top 20)
fnLastIdx: function (r, before) {
var m = r.measured || [];
for (var i = (before === undefined ? m.length : before) - 1; i >= 0; i--) { if (m[i]) { return i; } }
return -1;
},
fnPos: function (r) { var i = this.fnLastIdx(r); return i < 0 ? undefined : r.series[i]; },
fnPrev: function (r) { var i = this.fnLastIdx(r); var j = i < 0 ? -1 : this.fnLastIdx(r, i); return j < 0 ? undefined : r.series[j]; },
fnWaiting: function (r) { return this.fnLastIdx(r) < 0; },
fnDiffOf: function (a, b) {
if (a === undefined || b === undefined) { return 0; }
return (a === null ? 21 : a) - (b === null ? 21 : b);
},
fnDiff: function (r) { return this.fnDiffOf(this.fnPrev(r), this.fnPos(r)); },
// beste positie over de gemeten maanden; undefined als er nog niets gemeten is
fnBest: function (r) {
var b = this.fnLastIdx(r) < 0 ? undefined : null;
r.series.forEach(function (p, i) { if (r.measured[i] && p !== null && (!b || p < b)) { b = p; } });
return b;
},
fnUrlRows: function (r) {
if (r._urlRows) { return r._urlRows; }
var i = this.fnLastIdx(r);
r._urlRows = r.urls.map(function (u) { return { url: u.url, pos: i < 0 ? null : u.series[i] }; })
.filter(function (u) { return u.pos !== null; })
.sort(function (a, b) { return a.pos - b.pos; });
return r._urlRows;
},
fnCannibal: function (r) { return this.fnUrlRows(r).length > 1; },
fnTopUrl: function (r) { var u = this.fnUrlRows(r); return u.length ? u[0].url : null; },
fnSpark: function (r) {
if (!r._spark) {
var depth = this.data.depth || 20;
var vals = r.series.map(function (p, i) { return r.measured[i] ? p : null; });
r._spark = lineChart([{ values: vals, color: COLORS.own, width: 1.5 }], { w: 80, h: 22, n: vals.length, min: 1, max: depth + 1 })[0];
}
return r._spark;
},
// --- uitklap: de top 20 van de laatste meting en de grafiek op dezelfde schaal (rij p = hoogte p)
fnToggle: function (r) {
r.open = !r.open;
if (r.open && !r.detail) {
$http.get('/rest/serp/keyword', { params: { websiteid: h.siteId(), id: r.id } }).then(function (res) {
r.detail = res.data;
r._serp = null; r._chart = null;
});
}
},
fnSerp: function (r) {
if (r._serp) { return r._serp; }
if (!r.detail) { return []; }
var self = this;
var ownColor = {};
angular.forEach(r.urls, function (u, i) { ownColor[u.url] = i ? COLORS.own2 : COLORS.own; });
r._serp = (r.detail.top || []).slice(0, 20).map(function (s) {
return angular.extend({}, s, { competitor: self.competitors.indexOf(s.domain), color: s.own ? (ownColor[s.url] || COLORS.own) : null });
});
return r._serp;
},
fnDomainSeries: function (r, d) {
if (r.detail && r.detail.domains && r.detail.domains[d]) { return r.detail.domains[d]; }
return r.comp[d] || [];
},
fnKeywordChart: function (r) {
var self = this;
r.pin = r.pin || {};
var show = Object.keys(r.pin).filter(function (d) { return r.pin[d]; });
if (r.hl && show.indexOf(r.hl) === -1 && r.hl !== self.data.own) { show.push(r.hl); }
var key = show.join('|');
if (r._chart && r._chartKey === key) { return r._chart; }
var measured = r.measured;
// de grafiek loopt van 1 tot 20 (zoals de top 20 ernaast); dieper gemeten posities vallen eronder weg
function onlyMeasured(vals) { return (vals || []).map(function (p, i) { return measured[i] && p !== null && p <= 20 ? p : null; }); }
var series = show.map(function (d) {
var ci = self.competitors.indexOf(d);
var raw = self.fnDomainSeries(r, d);
return { values: onlyMeasured(raw), raw: raw, label: d, color: ci > -1 ? COLORS.c[ci] : COLORS.other, width: 2 };
});
angular.forEach(r.urls, function (u, i) { series.push({ values: onlyMeasured(u.series), raw: u.series, label: u.url, color: i ? COLORS.own2 : COLORS.own, width: 3, own: true }); });
r._chart = lineChart(series, { w: 600, h: 20 * ROW, n: self.months.length, min: 0.5, max: 20.5, pad: 0, padX: 6 });
r._chartKey = key;
return r._chart;
},
fnHover: function (r, d) { r.hl = d; },
fnPin: function (r, d) { if (d === this.data.own) { return; } r.pin = r.pin || {}; r.pin[d] = !r.pin[d]; },
// --- lijsten
fnFiltered: function () {
var f = this.filter, q = (f.q || '').toLowerCase(), self = this, key = this.sort;
return this.rows.filter(function (r) {
return (!q || r.q.indexOf(q) > -1) && (!f.group || r.group === f.group) && (!f.star || r.star) && (!f.device || r.device === f.device) && (!f.cannibal || self.fnCannibal(r));
}).sort(function (a, b) {
if (key === 'q') { return a.q.localeCompare(b.q); }
if (key === 'diff') { return self.fnDiff(b) - self.fnDiff(a); }
if (key === 'vol') { return (b.vol || 0) - (a.vol || 0); }
var x = self.fnPos(a), y = self.fnPos(b);
x = x === undefined ? 100 : (x === null ? 99 : x); y = y === undefined ? 100 : (y === null ? 99 : y);
return x - y;
});
},
// de gefilterde zoekwoorden per groep (alfabetisch, zonder groep als laatste); bewaard tot filter, sortering
// of de zoekwoorden veranderen, anders telt ng-repeat elke digest nieuwe groepen
collapsed: serpState.collapsed,
fnGrouped: function () {
var key = JSON.stringify(this.filter) + '|' + this.sort + '|' + this.rowsVersion;
if (this._grouped && this._groupedKey === key) { return this._grouped; }
var by = {}, order = [];
angular.forEach(this.fnFiltered(), function (r) {
var g = r.group || '';
if (!by[g]) { by[g] = { name: g, rows: [], selected: false }; order.push(g); }
by[g].rows.push(r);
});
order.sort(function (a, b) { return a === '' ? 1 : (b === '' ? -1 : a.localeCompare(b)); });
this._grouped = order.map(function (g) { return by[g]; });
this._groupedKey = key;
return this._grouped;
},
fnToggleGroup: function (name) { this.collapsed[name] = !this.collapsed[name]; },
fnSelectGroup: function (g, on) { angular.forEach(g.rows, function (r) { r.selected = on; }); },
fnGroups: function () {
var g = {};
angular.forEach(this.rows, function (r) { if (r.group) { g[r.group] = true; } });
return Object.keys(g).sort();
},
fnMovers: function (up) {
var self = this;
return this.rows.filter(function (r) { return self.fnPrev(r) !== undefined && (up ? self.fnDiff(r) > 0 : self.fnDiff(r) < 0); })
.sort(function (a, b) { return up ? self.fnDiff(b) - self.fnDiff(a) : self.fnDiff(a) - self.fnDiff(b); })
.slice(0, 6);
},
// --- zichtbaarheid: geschatte klikken / klikken als je overal #1 stond, gewogen naar volume (zonder volume: 1)
fnVis: function (rows, domain, mi) {
var self = this;
rows = rows || self.rows;
mi = mi === undefined ? self.months.length - 1 : mi;
var got = 0, max = 0;
angular.forEach(rows, function (r) {
if (!r.measured[mi]) { return; }
var vol = r.vol || 1;
var p = (!domain || domain === self.data.own) ? r.series[mi] : (r.comp[domain] || [])[mi];
max += vol * CTR[1];
if (p !== null && p !== undefined) { got += vol * (CTR[p] || 0); }
});
return max ? 100 * got / max : null;
},
fnVisChart: function () {
var self = this;
var domains = [self.data.own].concat(self.competitors);
var series = domains.map(function (d, i) {
return { values: self.months.map(function (m, mi) { return self.fnVis(null, d, mi); }), label: d, color: i ? COLORS.c[i - 1] : COLORS.own, width: i ? 2 : 3, own: i === 0 };
});
var max = 0;
series.forEach(function (s) { s.values.forEach(function (v) { if (v > max) { max = v; } }); });
self.visMax = Math.ceil(max / 10) * 10 || 10;
return lineChart(series, { w: 640, h: 200, n: self.months.length, min: 0, max: self.visMax, invert: false });
},
fnDistribution: function () {
var self = this;
return self.months.map(function (m, mi) {
var d = { month: m, top3: 0, top10: 0, top20: 0, out: 0, total: 0 };
angular.forEach(self.rows, function (r) {
if (!r.measured[mi]) { return; }
var p = r.series[mi];
d.total++;
if (p === null || p > 20) { d.out++; } else if (p <= 3) { d.top3++; } else if (p <= 10) { d.top10++; } else { d.top20++; }
});
return d;
});
},
// withEmpty: ook "zonder groep" (voor de koppen in de zoekwoordentabel)
fnGroupStats: function (withEmpty) {
var self = this;
var names = self.fnGroups();
if (withEmpty && self.rows.some(function (r) { return !r.group; })) { names.push(''); }
return names.map(function (g) {
var rows = self.rows.filter(function (r) { return r.group === g; });
var n = 0, sum = 0, top10 = 0;
angular.forEach(rows, function (r) { var p = self.fnPos(r); if (p) { n++; sum += p; if (p <= 10) { top10++; } } });
var vis = self.fnVis(rows), prev = self.fnVis(rows, null, self.months.length - 2);
return { name: g, count: rows.length, vis: vis, diff: vis !== null && prev !== null ? vis - prev : null, avg: n ? sum / n : null, top10: top10 };
});
},
fnSummary: function () {
var self = this, s = { top10: 0, top10prev: 0, n: 0, sum: 0, cannibal: 0 };
angular.forEach(self.rows, function (r) {
var p = self.fnPos(r), pp = self.fnPrev(r);
if (p) { s.n++; s.sum += p; if (p <= 10) { s.top10++; } }
if (pp && pp <= 10) { s.top10prev++; }
if (self.fnCannibal(r)) { s.cannibal++; }
});
s.avg = s.n ? s.sum / s.n : null;
s.vis = self.fnVis(null);
var prev = self.fnVis(null, null, self.months.length - 2);
s.visDiff = s.vis !== null && prev !== null ? s.vis - prev : null;
return s;
},
// --- concurrenten
fnCompPos: function (r, d) {
if (d === this.data.own) { return this.fnPos(r); }
var i = this.fnLastIdx(r);
return i < 0 ? undefined : (r.comp[d] || [])[i];
},
fnCompDiff: function (r, d) {
if (d === this.data.own) { return this.fnDiff(r); }
var i = this.fnLastIdx(r), j = i < 0 ? -1 : this.fnLastIdx(r, i);
return j < 0 ? 0 : this.fnDiffOf((r.comp[d] || [])[j], (r.comp[d] || [])[i]);
},
fnCompStats: function () {
var self = this;
return [self.data.own].concat(self.competitors).map(function (d, i) {
var top10 = 0, n = 0, sum = 0;
angular.forEach(self.rows, function (r) {
var p = self.fnCompPos(r, d);
if (p) { n++; sum += p; if (p <= 10) { top10++; } }
});
var vis = self.fnVis(null, d), prev = self.fnVis(null, d, self.months.length - 2);
return { domain: d, own: i === 0, color: i ? COLORS.c[i - 1] : COLORS.own, vis: vis, diff: vis !== null && prev !== null ? vis - prev : null, top10: top10, avg: n ? sum / n : null, found: n };
});
},
// per concurrent: op hoeveel zoekwoorden staat hij boven je
fnVs: function () {
var self = this, out = {};
angular.forEach(self.competitors, function (c) {
var o = { you: 0, them: 0 };
angular.forEach(self.rows, function (r) {
var a = self.fnPos(r), b = self.fnCompPos(r, c);
if (a === undefined) { return; }
a = a === null ? 99 : a; b = (b === null || b === undefined) ? 99 : b;
if (a < b) { o.you++; } else if (b < a) { o.them++; }
});
out[c] = o;
});
return out;
},
fnSaveCompetitors: function (list) {
var self = this;
$http.post('/rest/serp/competitors', { websiteid: h.siteId(), competitors: list }).then(function () { self.newCompetitor = ''; self.init(); });
},
fnAddCompetitor: function (d) { if (d) { this.fnSaveCompetitors(this.competitors.concat([d])); } },
fnRemoveCompetitor: function (d) { this.fnSaveCompetitors(this.competitors.filter(function (c) { return c !== d; })); },
// --- bewerken (land of apparaat wijzigen en verwijderen tellen mee in het tegoed)
fnSelected: function () { return this.rows.filter(function (r) { return r.selected; }); },
fnSelectAll: function (on) { angular.forEach(this.fnFiltered(), function (r) { r.selected = on; }); },
fnMessage: function (res) {
var self = this, parts = [];
var d = res.data || {};
if (d.error === 'trial') { parts.push($translate.instant('serp.msg.trial')); }
if (d.added !== undefined) { parts.push($translate.instant('serp.msg.added', { n: d.added })); }
if (d.exists) { parts.push($translate.instant('serp.msg.exists', { n: d.exists })); }
if (d.over) { parts.push($translate.instant('serp.msg.over', { n: d.over })); }
self.message = parts.join(' ');
},
fnAdd: function () {
var self = this;
$http.post('/rest/serp/add', { websiteid: h.siteId(), keywords: self.add.text, cc: self.add.cc, device: self.add.device, group: self.add.group }).then(function (res) {
self.fnMessage(res);
if (res.data.added) { self.add.text = ''; self.add.show = false; }
self.init();
});
},
fnEdit: function (ids, changes, reload) {
var self = this;
return $http.post('/rest/serp/edit', angular.extend({ websiteid: h.siteId(), ids: ids }, changes)).then(function (res) {
self.fnMessage(res);
if (reload) { self.init(); }
});
},
fnToggleStar: function (r) { r.star = !r.star; this.rowsVersion++; this.fnEdit([r.id], { star: r.star }, false); },
// in bulk alleen de groep (bestaand of nieuw); land en apparaat liggen vast zodra een zoekwoord bestaat
fnBulkApply: function () {
if (!this.bulk.group) { return; }
this.fnEdit(this.fnSelected().map(function (r) { return r.id; }), { group: this.bulk.group }, true);
this.bulk = { group: '' };
this.allSelected = false;
},
fnBulkRemove: function () {
var self = this;
$http.post('/rest/serp/delete', { websiteid: h.siteId(), ids: self.fnSelected().map(function (r) { return r.id; }) }).then(function () {
self.allSelected = false;
self.init();
});
},
fnExport: function () { window.location.href = '/rest/serp/export?websiteid=' + h.siteId(); },
// --- meetinstellingen (alleen wie plannen mag instellen); de prijs komt van de server
fnOpenSettings: function () {
var self = this;
$http.get('/rest/serp/plan', { params: { websiteid: h.siteId() } }).then(function (res) {
var p = res.data.plan;
self.settings = {
own: res.data.own,
buy: !!res.data.buy,
now: res.data.now,
freq: Math.max(0, FREQS.indexOf(p.freq)),
kw: Math.max(0, KEYWORD_STEPS.indexOf(p.keywords) > -1 ? KEYWORD_STEPS.indexOf(p.keywords) : KEYWORD_STEPS.filter(function (k) { return k <= p.keywords; }).length - 1),
depth: Math.max(0, DEPTHS.indexOf(p.depth)),
price: res.data.price
};
});
},
fnSettingsPlan: function () {
var s = this.settings;
return { freq: FREQS[+s.freq], keywords: KEYWORD_STEPS[+s.kw], depth: DEPTHS[+s.depth] };
},
fnSettingsChanged: function () {
var self = this;
if (self._priceTimer) { $timeout.cancel(self._priceTimer); }
self._priceTimer = $timeout(function () {
$http.get('/rest/serp/plan', { params: angular.extend({ websiteid: h.siteId() }, self.fnSettingsPlan()) }).then(function (res) {
if (self.settings) { self.settings.price = res.data.price; self.settings.now = res.data.now; }
});
}, 250);
},
// directe klant: kopen. Naar Mollie als er nu iets te betalen is, anders staat het plan meteen aan.
fnBuy: function () {
var self = this;
self.settings.busy = true;
$http.post('/rest/serp/buy', { websiteid: h.siteId(), plan: self.fnSettingsPlan() }).then(function (res) {
if (res.data.checkout) { window.location.href = res.data.checkout; return; }
self.settings = null;
self.init();
}, function () { self.settings.busy = false; });
},
fnSaveSettings: function (followDefault) {
var self = this;
$http.post('/rest/serp/plan', { websiteid: h.siteId(), plan: followDefault ? null : self.fnSettingsPlan() }).then(function () {
self.settings = null;
self.init();
});
},
// --- opmaak
fnMonth: function (d) { return d.toLocaleDateString($translate.use() || 'nl', { month: 'short' }).replace('.', ''); },
fnDate: function (ms) { return ms ? new Date(ms).toLocaleDateString($translate.use() || 'nl', { day: 'numeric', month: 'short' }) : ''; },
fnNum: function (v, digits) { return v === null || v === undefined ? '–' : (+v).toLocaleString($translate.use() || 'nl', { minimumFractionDigits: digits || 0, maximumFractionDigits: digits || 0 }); },
fnEuro: function (v) { return '€' + (+v).toLocaleString($translate.use() || 'nl', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); },
// --- tooltips: één zwevend kader bij de muis, in de stijl van de andere grafieken (donker)
tip: null,
_bands: {},
// per maand een strook over de grafiek waarop de muis de tooltip opent (zelfde x als de punten)
fnBands: function (n, w, padX) {
var key = n + '|' + w + '|' + padX;
if (!this._bands[key]) {
var step = n > 1 ? (w - 2 * padX) / (n - 1) : w;
var out = [];
for (var i = 0; i < n; i++) {
var cx = n > 1 ? padX + i * step : w / 2;
var left = Math.max(0, cx - step / 2), right = Math.min(w, cx + step / 2);
out.push({ i: i, left: 100 * left / w, width: 100 * (right - left) / w, center: 100 * cx / w });
}
this._bands[key] = out;
}
return this._bands[key];
},
fnMonthLong: function (i) {
var d = this.months[i];
if (!d) { return ''; }
var opts = { month: 'long' };
if (d.getFullYear() !== new Date().getFullYear()) { opts.year = 'numeric'; }
return d.toLocaleDateString($translate.use() || 'nl', opts);
},
fnTipAt: function ($event, kind, i, title, rows) {
var x = $event.clientX, y = $event.clientY;
this.tip = { kind: kind, i: i, title: title, rows: rows, left: x + 240 > window.innerWidth ? x - 236 : x + 14, top: y + 14 };
},
fnTipOff: function () { this.tip = null; },
fnVisTip: function ($event, i) {
var self = this;
var rows = self.v.visChart.map(function (p) {
var v = p.raw[i];
return { color: p.color, label: p.label, value: v === null || v === undefined ? '–' : self.fnNum(v, 1) + '%', strong: p.own };
});
self.fnTipAt($event, 'vis', i, self.fnMonthLong(i), rows);
},
fnDistTip: function ($event, d, i) {
var t = function (k) { return $translate.instant(k); };
this.fnTipAt($event, 'dist', i, this.fnMonthLong(i), d.total ? [
{ color: '#1d4ed8', label: 'top 3', value: d.top3 },
{ color: '#60a5fa', label: '4-10', value: d.top10 },
{ color: '#bfdbfe', label: '11-20', value: d.top20 },
{ color: '#e5e7eb', label: '> 20', value: d.out }
] : [{ label: t('serp.not_measured'), value: '' }]);
},
fnSparkTip: function ($event, r) {
var self = this, rows = [];
r.series.forEach(function (p, i) {
if (r.measured[i]) { rows.push({ label: self.fnMonth(self.months[i]), value: self.fnPosLabel(p) }); }
});
if (!rows.length) { rows.push({ label: $translate.instant('serp.waiting'), value: '' }); }
self.fnTipAt($event, 'spark', null, r.q, rows);
},
fnDetailTip: function ($event, r, i) {
var self = this;
var rows = self.fnKeywordChart(r).slice().sort(function (a, b) { return (b.own ? 1 : 0) - (a.own ? 1 : 0); }).map(function (p) {
return { color: p.color, label: p.label, value: r.measured[i] ? self.fnPosLabel((p.raw || [])[i]) : '–', strong: p.own };
});
self.fnTipAt($event, 'detail', i, self.fnMonthLong(i) + (r.measured[i] ? '' : ' · ' + $translate.instant('serp.not_measured')), rows);
},
// landnaam in de taal van de gebruiker (uk = Verenigd Koninkrijk)
fnCountry: function (c) {
try { return new Intl.DisplayNames([$translate.use() || 'nl'], { type: 'region' }).of(c === 'uk' ? 'GB' : c.toUpperCase()); } catch (e) { return c.toUpperCase(); }
},
fnDaysUntil: function (ms) { return Math.max(0, Math.ceil((ms - Date.now()) / 86400000)); },
fnPosLabel: function (p) { return p === undefined ? '–' : (p === null ? '> ' + ((this.data && this.data.depth) || 20) : p); }
};
// een tooltip hoort bij wat je ziet: weg bij een andere tab (mouseleave komt niet als het element verdwijnt)
$scope.$watch('oSerp.tab', function () { $scope.oSerp.tip = null; });
// een klik op het overzicht (groep, interne concurrentie, toevoegen) zet het filter op de zoekwoordenpagina
if (serpState.pending && $scope.oSerp.tab === 'keywords') {
var p = serpState.pending;
serpState.pending = null;
if (p.group !== undefined) { $scope.oSerp.filter.group = p.group; }
if (p.cannibal) { $scope.oSerp.filter.cannibal = true; }
if (p.add) { $scope.oSerp.add.show = true; }
}
// terug van Mollie: de webhook verwerkt de betaling, meestal binnen een paar seconden. Tot 30 seconden de status van
// de bestelling opvragen en zeggen wat er echt gebeurde.
if (!report && $location.search().betaald) {
var orderId = $location.search().betaald, tries = 0;
$location.search('betaald', null);
$scope.oSerp.paidMessage = 'serp.paid_wait';
var check = function () {
$http.get('/rest/serp/order', { params: { websiteid: h.siteId(), id: orderId } }).then(function (res) {
var st = res.data.status;
if (st === 'paid') {
$scope.oSerp.paidMessage = 'serp.paid_ok';
$scope.oSerp.init();
} else if (st && st !== 'open') {
$scope.oSerp.paidMessage = 'serp.paid_failed';
} else if (++tries < 15) {
return $timeout(check, 2000);
}
$timeout(function () { $scope.oSerp.paidMessage = null; }, 8000);
});
};
check();
}
$scope.oSerp.init();
}]);
// Bouwblokken voor de nieuwe pagina's (Posities in Google, Search Console Insights, en wat daarna komt).
// Wat ze doen, hoe ze eruitzien en wanneer je welke gebruikt: docs/ui.md. Nieuwe pagina's gebruiken deze blokken
// in plaats van eigen kaarten, grafieken en tooltips.
//
// Regels die overal gelden:
// - Grafieken krijgen een nieuw data-object als de gegevens veranderen; de blokken kijken naar de verwijzing,
// niet naar de inhoud (geen dure deep watch, geen eindeloze digest).
// - Teksten komen vertaald binnen (title="{{'sci.x' | translate}}"); de blokken vertalen alleen hun eigen labels (ui.*).
// --- glimmer over placeholders (mt-shim): een lichte baan die van links naar rechts loopt, zodat je ziet dat er iets
// laadt. Eén keer in de pagina gezet.
(function () {
if (document.getElementById('mt-ui-css')) { return; }
var st = document.createElement('style');
st.id = 'mt-ui-css';
st.textContent = '@keyframes mt-shim{0%{background-position:-400px 0}100%{background-position:400px 0}}' +
'.mt-shim{background:linear-gradient(90deg,#f3f4f6 0%,#e5e7eb 40%,#f3f4f6 80%);background-size:800px 100%;animation:mt-shim 1.4s linear infinite}' +
// kaarten en tegels zijn blokken: anders werken tussenruimtes (space-y, mt-*) niet als ze onder elkaar staan
'mt-card,mt-kpi,mt-state{display:block}';
document.head.appendChild(st);
})();
// --- notatie: één plek voor getallen, procenten, euro's, maanden en verschillen
app.factory('mtFmt', ['$translate', function ($translate) {
function lang() { return $translate.use() || 'nl'; }
function num(v, digits) {
if (v === null || v === undefined || v === '' || isNaN(v)) { return '–'; }
return (+v).toLocaleString(lang(), { minimumFractionDigits: digits || 0, maximumFractionDigits: digits || 0 });
}
// 'YYYY-MM' of 'YYYY-MM-DD' of een Date
function date(d) {
if (d instanceof Date) { return d; }
if (typeof d === 'number') { return new Date(d); }
var p = String(d).split('-');
return new Date(+p[0], (+p[1] || 1) - 1, +p[2] || 1);
}
return {
num: num,
// grote getallen kort: 12.400 -> 12,4K vanaf 10.000
short: function (v) {
if (v === null || v === undefined || isNaN(v)) { return '–'; }
var a = Math.abs(v);
if (a >= 1e6) { return num(v / 1e6, 1) + 'M'; }
if (a >= 1e4) { return num(v / 1e3, 1) + 'K'; }
return num(v);
},
pct: function (v, digits) { return v === null || v === undefined || isNaN(v) ? '–' : num(v, digits === undefined ? 1 : digits) + '%'; },
// euro's: hele euro's vanaf 100, anders twee decimalen
euro: function (v) {
if (v === null || v === undefined || isNaN(v)) { return '–'; }
return '€ ' + num(v, Math.abs(v) >= 100 ? 0 : 2);
},
pos: function (v) { return v === null || v === undefined || isNaN(v) || v <= 0 ? '–' : num(v, 1); },
month: function (d) { return date(d).toLocaleDateString(lang(), { month: 'short' }).replace('.', ''); },
// "oktober" dit jaar, "oktober 2025" een ander jaar
monthLong: function (d) {
d = date(d);
var o = { month: 'long' };
if (d.getFullYear() !== new Date().getFullYear()) { o.year = 'numeric'; }
return d.toLocaleDateString(lang(), o);
},
day: function (d) { return date(d).toLocaleDateString(lang(), { day: 'numeric', month: 'short' }); },
// met de dag van de week, voor tooltips per dag: "wo 27 okt"
dayWeek: function (d) { return date(d).toLocaleDateString(lang(), { weekday: 'short', day: 'numeric', month: 'short' }).replace(/\./g, ''); },
// procentuele verandering, null als er niets te vergelijken is
change: function (now, before) {
if (before === null || before === undefined || now === null || now === undefined || !before) { return null; }
return 100 * (now - before) / before;
}
};
}]);
// --- één zwevende tooltip voor alle grafieken (donker, bij de muis); los van de digest
app.factory('mtTip', function () {
var el = null;
function esc(s) { return String(s === null || s === undefined ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
return {
// rows: [{label, value, color?, strong?}]
show: function (e, title, rows) {
if (!el) {
el = document.createElement('div');
el.className = 'fixed z-50 pointer-events-none rounded-md bg-gray-900/90 text-white text-xs shadow-lg px-3 py-2 min-w-[140px] max-w-[280px]';
document.body.appendChild(el);
}
var html = title ? '' + esc(title) + '
' : '';
(rows || []).forEach(function (r) {
html += '' +
(r.color ? ' ' : '') +
'' + esc(r.label) + ' ' + esc(r.value) + '
';
});
el.innerHTML = html;
el.style.display = 'block';
var x = e.clientX, y = e.clientY;
el.style.left = (x + 290 > window.innerWidth ? x - 286 : x + 14) + 'px';
el.style.top = (y + 14) + 'px';
},
hide: function () { if (el) { el.style.display = 'none'; } }
};
});
// --- geometrie voor de grafieken (los te testen, geen DOM)
app.factory('mtChart', function () {
// een rond maximum voor de as: 0, de helft en dit getal
function niceMax(v) {
if (!v || v <= 0) { return 1; }
var p = Math.pow(10, Math.floor(Math.log10(v)));
var steps = [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10];
for (var i = 0; i < steps.length; i++) { if (steps[i] * p >= v) { return steps[i] * p; } }
return 10 * p;
}
// per punt een strook over de grafiek waarop de muis de tooltip opent (zelfde x als de punten), in %
function bands(n, w, padX) {
var step = n > 1 ? (w - 2 * padX) / (n - 1) : w, out = [];
for (var i = 0; i < n; i++) {
var cx = n > 1 ? padX + i * step : w / 2;
var left = Math.max(0, cx - step / 2), right = Math.min(w, cx + step / 2);
out.push({ i: i, left: 100 * left / w, width: 100 * (right - left) / w, center: 100 * cx / w });
}
return out;
}
// monotone curve (Arjen 2026-10-28: de lijn schoot boven de top en onder de 0): glad, maar nooit voorbij het
// hoogste of laagste punt. pts: [{x, y}] met oplopende x
function smooth(pts) {
var n = pts.length, t = [], s = [], h = [], i;
var d = 'M' + pts[0].x.toFixed(1) + ' ' + pts[0].y.toFixed(1);
if (n < 2) { return d; }
for (i = 0; i < n - 1; i++) { h[i] = pts[i + 1].x - pts[i].x; s[i] = (pts[i + 1].y - pts[i].y) / (h[i] || 1); }
for (i = 1; i < n - 1; i++) {
var p = (s[i - 1] * h[i] + s[i] * h[i - 1]) / ((h[i - 1] + h[i]) || 1);
t[i] = (Math.sign(s[i - 1]) + Math.sign(s[i])) * Math.min(Math.abs(s[i - 1]), Math.abs(s[i]), 0.5 * Math.abs(p)) || 0;
}
t[0] = n > 2 ? (3 * s[0] - t[1]) / 2 : s[0];
t[n - 1] = n > 2 ? (3 * s[n - 2] - t[n - 2]) / 2 : s[0];
if (Math.sign(t[0]) !== Math.sign(s[0])) { t[0] = 0; }
if (Math.sign(t[n - 1]) !== Math.sign(s[n - 2])) { t[n - 1] = 0; }
for (i = 0; i < n - 1; i++) {
var dx = h[i] / 3;
d += ' C' + [pts[i].x + dx, pts[i].y + dx * t[i], pts[i + 1].x - dx, pts[i + 1].y - dx * t[i + 1], pts[i + 1].x, pts[i + 1].y].map(function (v) { return v.toFixed(1); }).join(' ');
}
return d;
}
// series [{values:[getal|null], connect?, nodots?}] -> svg-paden met losse stukken waar data ontbreekt, een dunne
// stippellijn over die gaten (bridge) en de punten. connect: over lege waarden heen tekenen (losse metingen);
// nodots: alleen een punt waar een stuk uit één punt bestaat
function lines(series, o) {
var w = o.w, h = o.h, n = o.n, pad = o.pad === undefined ? 6 : o.pad, padX = o.padX === undefined ? pad : o.padX;
function x(i) { return n > 1 ? padX + i * (w - 2 * padX) / (n - 1) : w / 2; }
function y(v) { var t = (v - o.min) / ((o.max - o.min) || 1); return pad + (o.invert ? t : 1 - t) * (h - 2 * pad); }
return series.map(function (s) {
var segs = [], cur = [], dots = [];
(s.values || []).forEach(function (v, i) {
if (v === null || v === undefined || isNaN(v)) { if (cur.length && !s.connect) { segs.push(cur); cur = []; } return; }
var pt = { x: x(i), y: y(v), px: 100 * x(i) / w, py: 100 * y(v) / h };
cur.push(pt); dots.push(pt);
});
if (cur.length) { segs.push(cur); }
var bridge = [];
for (var j = 1; j < segs.length; j++) {
var a = segs[j - 1][segs[j - 1].length - 1], b = segs[j][0];
bridge.push('M' + a.x.toFixed(1) + ' ' + a.y.toFixed(1) + ' L' + b.x.toFixed(1) + ' ' + b.y.toFixed(1));
}
if (s.nodots) { dots = segs.filter(function (g) { return g.length === 1; }).map(function (g) { return g[0]; }); }
return { d: segs.map(smooth).join(' '), bridge: bridge.join(' '), color: s.color, width: s.width || 2, dash: s.dash ? '4 4' : '', dots: dots, label: s.label };
});
}
// as voor hele getallen (klikken, vertoningen): minstens tot 2 en altijd een heel midden
function niceInt(v) {
if (!v || v <= 2) { return 2; }
var p = Math.pow(10, Math.floor(Math.log10(v)));
var steps = [2, 4, 6, 8, 10];
for (var i = 0; i < steps.length; i++) { if (steps[i] * p >= v) { return steps[i] * p; } }
return 10 * p;
}
return { niceMax: niceMax, niceInt: niceInt, bands: bands, lines: lines, smooth: smooth };
});
// --- kaart: wit, rand, titel met eventueel een link rechts, een uitlegregel, en de inhoud.
// loading: de inhoud is er nog niet; dan grijze balken op de plek (minh = hoogte in px), zodat niets verspringt.
app.directive('mtCard', function () {
return {
restrict: 'E', transclude: true,
scope: { title: '@', sub: '@', more: '@', moreHref: '@', loading: '', minh: '@' },
template:
'' +
'
' +
'
{{sub}}
' +
'
' +
'
',
// title is ook een HTML-attribuut: dan toont de browser de kaartnaam als tooltip waar je ook in de kaart staat
link: function (scope, el, attrs) { attrs.$observe('title', function () { el[0].removeAttribute('title'); }); }
};
});
// --- verschil: ▲ groen of ▼ rood; invert voor posities (lager is beter); unit '%' (standaard, ook leeg), 'pt' of 'none'
// up / down: een woord achter het getal ("▼ 30% sneller", "▲ 1% trager"); zero: tekst bij geen verschil ("gelijk")
app.directive('mtDelta', ['mtFmt', function (mtFmt) {
return {
restrict: 'E',
scope: { value: '<', invert: '', unit: '@', digits: '@', up: '@', down: '@', zero: '@' },
template:
'' +
' {{v === 0 && zero ? zero : txt}} {{v > 0 ? up : down}} ',
link: function (scope) {
scope.$watch('value', function (val) {
scope.v = val === null || val === undefined || isNaN(val) ? null : +val;
if (scope.v === null) { return; }
var unit = !scope.unit || scope.unit === '%' ? '%' : (scope.unit === 'none' ? '' : ' ' + scope.unit);
var d = scope.digits === undefined || scope.digits === '' ? (unit === '%' ? 0 : 1) : +scope.digits;
if (Math.abs(scope.v) < Math.pow(10, -d) / 2) { scope.v = 0; }
scope.good = scope.invert ? scope.v < 0 : scope.v > 0;
scope.txt = mtFmt.num(Math.abs(scope.v), d) + unit;
});
}
};
}]);
// --- cijfertegel: label, groot getal, verschil, een regel uitleg; met href is de hele tegel een link
app.directive('mtKpi', function () {
return {
restrict: 'E',
// pick: klikbaar (bv. de grafiek eronder wisselen), pickTitle: wat de klik doet, active: de gekozen tegel
// tone: een stip voor het getal in de kleur van een oordeel (good | ni | poor | none), bv. Core Web Vitals
// mark: een rond icoon voor het getal (good = vinkje, poor = kruis, none = streepje), bv. geslaagd of niet;
// suffix: kleiner en grijs achter het getal ("#1 van 4"); zero: tekst bij een verschil van 0 ("gelijk")
scope: { label: '@', value: '@', delta: '', invert: '', unit: '@', note: '@', href: '@', warn: '', loading: '', pick: '&?', pickable: '', pickTitle: '@', active: '', tone: '@', mark: '@', suffix: '@', zero: '@' },
template:
' ' +
'' +
'
' +
'
',
link: function (scope) { scope.hasDelta = function () { return scope.delta !== null && scope.delta !== undefined; }; }
};
});
app.run(['$templateCache', function ($templateCache) {
$templateCache.put('mt-kpi-body',
'{{label}}
' +
'
' +
'' +
' ' +
'' +
' ' +
'{{value}} ' +
'{{suffix}} ' +
'
' +
'{{note}}
');
}]);
// --- tabs bovenaan een pagina; elke tab is een adres, zodat verversen je op dezelfde pagina laat
// tabs: [{key, label, href, count?}]
app.directive('mtTabs', function () {
return {
restrict: 'E',
scope: { tabs: '<', active: '@' },
template:
// vaste hoogte: de tabs komen soms pas na de vertalingen, de pagina mag daardoor niet verspringen.
// Eén regel, op mobiel horizontaal scrollen; de grijze lijn is een schaduw zodat de blauwe streep erover valt
''
};
});
// --- toestand in plaats van inhoud: laden, leeg, bezig (met voortgang), fout, niet gekoppeld
// kind: loading | empty | working | error | connect; progress 0-100 (bij working); action + on-action voor de knop.
// layout="row": laag en over de volle breedte (icoon, tekst, knop naast elkaar), voor een lege toestand boven de
// rest van een pagina die wel inhoud heeft; zonder layout een kaart in het midden, voor een pagina die verder leeg is.
app.directive('mtState', function () {
var icon = '' +
'
';
return {
restrict: 'E', transclude: true,
scope: { kind: '@', title: '@', text: '@', step: '@', progress: '', action: '@', onAction: '&', href: '@', layout: '@' },
template:
'
' +
'' +
'' + icon +
'
{{title}} ' +
'
{{text}}
' +
'
' +
'
' +
'
{{action}} ' +
'
{{action}} ' +
'
',
link: function (scope, el, attrs) {
// een link naar /rest/ (bv. Search Console koppelen) moet de pagina echt verlaten; anders vangt de router van
// Angular (html5Mode) de klik af en stuurt hij een onbekende route door naar /
attrs.$observe('href', function (h) { scope.rest = /^\/rest\//.test(h || ''); });
// title is hier de kop, geen browser-tooltip (die bleef naast de kaart hangen)
attrs.$observe('title', function () { el[0].removeAttribute('title'); });
}
};
});
// --- staafgrafiek per periode, gestapeld; met een lijn ter vergelijking (bv. vorig jaar)
// data: {labels:[kort], titles:[tooltipkop], series:[{label, color, values}], line?:{label, color, values},
// faded?:[bool] (lichter: bv. ouder dan 16 maanden), extra?:[[{label, value}]] (extra tooltipregels per staaf)}
// fmt: hoe een waarde in de tooltip en op de as staat (standaard een getal)
app.directive('mtBars', ['mtChart', 'mtTip', 'mtFmt', function (mtChart, mtTip, mtFmt) {
return {
restrict: 'E',
scope: { data: '<', height: '@', fmt: '' },
template:
'{{axis[0]}} {{axis[1]}} 0
' +
'
' +
'' +
' {{s.label}} ' +
' {{data.line.label}}
',
link: function (scope) {
scope.h = +(scope.height || 200);
var f = function (v) { return scope.fmt ? scope.fmt(v) : mtFmt.short(v); };
scope.$watch('data', function (d) {
scope.cols = []; scope.linePath = null; scope.axis = ['', ''];
if (!d || !d.labels) { return; }
var n = d.labels.length, max = 0;
for (var i = 0; i < n; i++) {
var sum = 0;
d.series.forEach(function (s) { sum += +(s.values[i] || 0); });
max = Math.max(max, sum, d.line ? +(d.line.values[i] || 0) : 0);
}
max = mtChart.niceMax(max);
scope.axis = [f(max), f(max / 2)];
for (var j = 0; j < n; j++) {
// de onderste reeks onderaan: in een flex-kolom komt het laatste element onderaan
var parts = d.series.slice().reverse().map(function (s) { return { h: 100 * (+(s.values[j] || 0)) / max, color: s.color }; });
scope.cols.push({ parts: parts, faded: d.faded && d.faded[j] });
}
if (d.line) {
// de lijn door het midden van elke staaf
var pts = [];
d.line.values.forEach(function (v, k) {
if (v === null || v === undefined) { return; }
pts.push((k ? 'L' : 'M') + (1000 * (k + 0.5) / n).toFixed(1) + ' ' + (100 - 100 * v / max).toFixed(1));
});
scope.linePath = pts.join(' ').replace(/^L/, 'M');
}
});
scope.tip = function (e, i) {
var d = scope.data, rows = [];
d.series.forEach(function (s) { rows.push({ color: s.color, label: s.label, value: f(s.values[i]) }); });
if (d.line && d.line.values[i] !== null && d.line.values[i] !== undefined) { rows.push({ color: d.line.color || '#9ca3af', label: d.line.label, value: f(d.line.values[i]) }); }
((d.extra && d.extra[i]) || []).forEach(function (r) { rows.push(r); });
mtTip.show(e, (d.titles || d.labels)[i], rows);
};
scope.off = mtTip.hide;
scope.$on('$destroy', mtTip.hide);
}
};
}]);
// --- lijngrafiek per periode, met een punt per meting (bij één meting is dat alles wat er is)
// data: {labels, titles?, series:[{label, color, values, width?, dash?, strong?}], guides?:[{label, color, value}]};
// invert: hoog = klein getal (posities). guides: vaste horizontale stippellijnen (bv. de grens goed/slecht), in de
// legenda maar niet in de tooltip; de as reikt tot de hoogste grens
// min/max: vaste as (anders 0 tot een rond maximum); ints: hele getallen (as minstens tot 2)
app.directive('mtLine', ['mtChart', 'mtTip', 'mtFmt', function (mtChart, mtTip, mtFmt) {
var W = 640;
return {
restrict: 'E',
scope: { data: '<', height: '@', invert: '', min: '', max: '', fmt: '', ints: '' },
template:
'{{axis[0]}} {{axis[1]}} {{axis[2]}}
' +
'
' +
'
' +
' ' +
' ' +
' ' +
' ' +
'
' +
'
' +
'
' +
'
{{l}}
' +
'' +
'{{s.label}} ' +
' {{g.label}}
',
link: function (scope) {
scope.h = +(scope.height || 200);
scope.hover = null;
var f = function (v) { return scope.fmt ? scope.fmt(v) : mtFmt.short(v); };
scope.$watch('data', function (d) {
scope.paths = []; scope.bands = []; scope.guides = []; scope.axis = ['', '', ''];
if (!d || !d.labels) { return; }
var all = [];
d.series.forEach(function (s) { s.values.forEach(function (v) { if (v !== null && v !== undefined && !isNaN(v)) { all.push(+v); } }); });
(d.guides || []).forEach(function (g) { all.push(+g.value); });
var min = scope.min !== undefined ? scope.min : (scope.invert ? 1 : 0);
var top = Math.max.apply(null, all.concat([1]));
var max = scope.max !== undefined ? scope.max : (scope.ints ? mtChart.niceInt(top) : mtChart.niceMax(top));
scope.axis = scope.invert ? [f(min), f((min + max) / 2), f(max)] : [f(max), f((min + max) / 2), f(min)];
scope.paths = mtChart.lines(d.series, { w: W, h: 200, n: d.labels.length, min: min, max: max, invert: !!scope.invert, padX: 6 });
scope.bands = mtChart.bands(d.labels.length, W, 6);
// zelfde y als mtChart.lines (pad 6 op 200 hoog)
scope.guides = (d.guides || []).map(function (g) {
var t = (g.value - min) / ((max - min) || 1);
return { y: (6 + (scope.invert ? t : 1 - t) * 188).toFixed(1), color: g.color };
});
});
scope.tip = function (e, i) {
scope.hover = i;
var d = scope.data;
mtTip.show(e, (d.titles || d.labels)[i], d.series.map(function (s) { return { color: s.color, label: s.label, value: f(s.values[i]), strong: s.strong }; }));
};
scope.off = function () { scope.hover = null; mtTip.hide(); };
scope.$on('$destroy', mtTip.hide);
}
};
}]);
// --- minilijn in een tabelregel (Arjen 2026-10-28: één soort, geen grijze en blauwe staafjes): één kleur, het laatste
// punt dikker, een stippellijn over maanden zonder waarde, per maand een tooltip. invert: hoog = klein getal (positie)
// values: [getal|null]; labels: tooltipkop per waarde; label: naam van de waarde; empty: tekst bij geen waarde
app.directive('mtSparkLine', ['mtChart', 'mtTip', 'mtFmt', function (mtChart, mtTip, mtFmt) {
var W = 110, H = 24;
return {
restrict: 'E',
scope: { values: '<', labels: '', title: '@', label: '@', empty: '@', invert: '', fmt: '' },
template:
'' +
' ' +
' ' +
' ' +
' ' +
' ',
link: function (scope) {
scope.$watch('values', function (v) {
v = v || [];
var nums = v.filter(function (x) { return x !== null && x !== undefined; });
scope.d = ''; scope.bridge = ''; scope.last = null; scope.singles = []; scope.hits = [];
var n = v.length;
if (!n) { return; }
scope.hits = v.map(function (x, i) { return { x: i * W / n, w: W / n }; });
if (!nums.length) { return; }
var lo = Math.min.apply(null, nums), hi = Math.max.apply(null, nums);
if (scope.invert) {
// een kleine schommeling geen grote golf: minstens 4 plaatsen hoog
if (hi - lo < 4) { var c = (hi + lo) / 2; lo = Math.max(1, c - 2); hi = lo + 4; }
} else { lo = 0; hi = Math.max(hi, 2); }
var paths = mtChart.lines([{ values: v, nodots: true }], { w: W, h: H, n: n, min: lo, max: hi, invert: !!scope.invert, pad: 3 })[0];
scope.d = paths.d; scope.bridge = paths.bridge; scope.singles = paths.dots;
for (var i = n - 1; i >= 0; i--) {
if (v[i] !== null && v[i] !== undefined) {
var px = n > 1 ? 3 + i * (W - 6) / (n - 1) : W / 2, t = (v[i] - lo) / ((hi - lo) || 1);
scope.last = { x: px, y: 3 + (scope.invert ? t : 1 - t) * (H - 6) };
break;
}
}
});
scope.tip = function (e, i) {
var v = scope.values[i];
mtTip.show(e, scope.title + (scope.labels ? ' · ' + scope.labels[i] : ''), [{ label: scope.label || '', value: v === null || v === undefined ? (scope.empty || '–') : (scope.fmt ? scope.fmt(v) : mtFmt.num(v)) }]);
};
scope.off = mtTip.hide;
scope.$on('$destroy', mtTip.hide);
}
};
}]);
// --- tooltip bij een cel of label: mt-hover-tip="{t: 'kop', r: [{label, value}]}" (zelfde tooltip als de grafieken)
app.directive('mtHoverTip', ['mtTip', '$parse', function (mtTip, $parse) {
return {
restrict: 'A',
link: function (scope, el, attrs) {
var get = $parse(attrs.mtHoverTip);
el.on('mousemove', function (e) { var d = get(scope); if (d) { mtTip.show(e, d.t, d.r || []); } });
el.on('mouseleave', mtTip.hide);
scope.$on('$destroy', mtTip.hide);
}
};
}]);
// --- minigrafiek in een tabelregel: staafjes, met een tooltip per staafje
// values: [getal|null]; labels: tooltipkop per staafje; faded: aantal staafjes vooraan dat lichter is (bv. vorig jaar)
app.directive('mtSpark', ['mtTip', 'mtFmt', function (mtTip, mtFmt) {
return {
restrict: 'E',
scope: { values: '<', labels: '', title: '@', faded: '', fmt: '' },
template:
'',
link: function (scope) {
scope.$watch('values', function (v) {
v = v || [];
var max = Math.max.apply(null, v.map(function (x) { return +x || 0; }).concat([1]));
scope.bars = v.map(function (x, i) {
var none = x === null || x === undefined;
return { h: none ? 8 : Math.max(6, 100 * x / max), none: none, faded: i < (scope.faded || 0) };
});
});
scope.tip = function (e, i) {
var v = scope.values[i];
mtTip.show(e, scope.title, [{ label: scope.labels ? scope.labels[i] : '', value: v === null || v === undefined ? '–' : (scope.fmt ? scope.fmt(v) : mtFmt.num(v)) }]);
};
scope.off = mtTip.hide;
scope.$on('$destroy', mtTip.hide);
}
};
}]);
// --- aandeel als horizontale balk (0-100)
app.directive('mtShare', function () {
return {
restrict: 'E',
scope: { value: '<', color: '@' },
template: ''
};
});
// --- gestapelde horizontale balk: delen van een geheel (goed/matig/slecht) of opgetelde stappen (de LCP-onderdelen)
// parts: [{label, value, color, text?, ink?}]; max: de schaal (standaard de som, dus altijd vol; geef het maximum van
// alle regels mee om regels onderling te vergelijken); size: xs (dun, onder een getal) | sm | md (standaard, met witte
// naden) | lg (22 px hoog, losse afgeronde blokjes met 2 px ertussen en text in het blokje als het breed genoeg is, in
// de kleur ink, standaard wit; voor een balk per domein zoals de LCP-tijdverdeling); title + fmt: kop en notatie in de
// tooltip (een regel per deel); empty: tekst in een gestippeld vak als er geen delen zijn (alleen lg)
app.directive('mtStack', ['mtTip', 'mtFmt', function (mtTip, mtFmt) {
var H = { xs: 'h-1', sm: 'h-1.5', md: 'h-3.5' };
return {
restrict: 'E',
scope: { parts: '<', max: '', size: '@', title: '@', fmt: '', empty: '@' },
template:
'' +
'' +
'{{empty}}
',
link: function (scope, el) {
scope.h = H[scope.size] || H.md;
scope.seams = !scope.size || scope.size === 'md';
function build() {
var parts = (scope.parts || []).filter(function (p) { return p && p.value > 0; });
var sum = parts.reduce(function (a, p) { return a + (+p.value); }, 0);
var max = scope.max > 0 ? Math.max(scope.max, sum) : sum;
// lg: tekst alleen in een blokje van minstens 46 px breed (anders past hij niet; op een telefoon minder tekst)
var px = el[0].offsetWidth || 600;
scope.total = max ? 100 * sum / max : 0;
scope.segs = parts.map(function (p) {
var w = sum ? 100 * p.value / sum : 0;
return { w: w, color: p.color, ink: p.ink, text: p.text && px * w * scope.total / 1e4 >= 46 ? p.text : '' };
});
}
scope.$watchGroup(['parts', 'max'], build);
if (scope.size === 'lg') {
var onResize = function () { scope.$applyAsync(build); };
window.addEventListener('resize', onResize);
scope.$on('$destroy', function () { window.removeEventListener('resize', onResize); });
// de eerste keer is de breedte er pas na het tekenen
scope.$applyAsync(build);
}
scope.tip = function (e) {
if (!scope.parts || !scope.parts.length) { return; }
mtTip.show(e, scope.title, scope.parts.map(function (p) {
return { color: p.color, label: p.label, value: p.value === null || p.value === undefined ? '–' : (scope.fmt ? scope.fmt(p.value) : mtFmt.num(p.value)) };
}));
};
scope.off = mtTip.hide;
scope.$on('$destroy', mtTip.hide);
}
};
}]);
// --- bolletje met een oordeel, bv. een positie in Google die wel of niet slaagt; tooltip met mt-hover-tip erbij
// tone: good | ni | poor | none (geen data) | pending (nog niet bekend); ring: blauw omcirkeld (jij); text: in het bolletje;
// size="sm": klein, zonder tekst (legenda)
app.directive('mtDot', function () {
var C = { good: 'bg-green-500 text-white', ni: 'bg-amber-400 text-white', poor: 'bg-red-500 text-white', none: 'bg-gray-200 text-gray-500', pending: 'bg-white border border-dashed border-gray-300 text-gray-400' };
return {
restrict: 'E',
scope: { tone: '@', ring: '', text: '@', size: '@' },
template: '{{text}} ',
link: function (scope) { scope.c = C; }
};
});
// --- Core Web Vitals: de grenzen van Google, het oordeel, de notatie en de kleuren; voor elke pagina met CrUX- of
// RUM-cijfers (zelfde grenzen als crux::LIMITS in rest/include/crux.php)
app.factory('mtCwv', ['mtFmt', '$translate', function (mtFmt, $translate) {
var LIMITS = { lcp: [2500, 4000], inp: [200, 500], cls: [0.1, 0.25], fcp: [1800, 3000], ttfb: [800, 1800] };
var COLORS = { good: '#22c55e', ni: '#f59e0b', poor: '#ef4444' };
var cwv = {
LIMITS: LIMITS,
COLORS: COLORS,
// good | ni | poor | null
rating: function (m, v) {
if (v === null || v === undefined || isNaN(v) || !LIMITS[m]) { return null; }
return v <= LIMITS[m][0] ? 'good' : (v <= LIMITS[m][1] ? 'ni' : 'poor');
},
// LCP en FCP in seconden (0,59 s), CLS op twee decimalen, de rest in ms (176 ms)
fmt: function (m, v) {
if (v === null || v === undefined || isNaN(v)) { return '–'; }
if (m === 'cls') { return mtFmt.num(v, 2); }
if (m === 'lcp' || m === 'fcp') { return mtFmt.num(v / 1000, 2) + ' s'; }
return mtFmt.num(v) + ' ms';
},
// een tijd onder een seconde in ms (358 ms), daarboven in seconden (1,15 s): de LCP-stappen, TTFB, FCP, rekentijd
ms: function (v) {
if (v === null || v === undefined || isNaN(v)) { return '–'; }
return v < 1000 ? mtFmt.num(v) + ' ms' : mtFmt.num(v / 1000, 2) + ' s';
},
// de schaal van een : goed tot, matig tot, einde van de balk
SCALE: { lcp: [2500, 4000, 5000], inp: [200, 500, 700], cls: [0.1, 0.25, 0.4] },
// lichte vlakken voor de zones in een grafiek of balk
ZONES: { good: '#f0fdf4', ni: '#fffbeb', poor: '#fef2f2' },
TRACK: { good: '#bbf7d0', ni: '#fde68a', poor: '#fecaca' },
// een notatiefunctie voor één metric (voor fmt="..." van mt-line, mt-spark-line, mt-stack)
fmtOf: function (m) { return function (v) { return cwv.fmt(m, v); }; },
// verdeling {good, ni, poor} (aandelen 0-1) naar delen voor
distParts: function (d) {
if (!d) { return null; }
return ['good', 'ni', 'poor'].map(function (k) { return { label: $translate.instant('ui.cwv.' + k), value: d[k] || 0, color: COLORS[k] }; });
},
pct: function (v) { return mtFmt.pct(100 * v, 0); }
};
return cwv;
}]);
// --- label voor een soort bevinding; elke soort heeft overal dezelfde kleur
// kind: chance | decay | cannibal | lowctr | new | lost | drop
app.directive('mtBadge', function () {
var C = {
chance: 'bg-green-100 text-green-800', decay: 'bg-red-100 text-red-800', cannibal: 'bg-amber-100 text-amber-800',
lowctr: 'bg-violet-100 text-violet-800', 'new': 'bg-blue-100 text-blue-800', lost: 'bg-gray-100 text-gray-700', drop: 'bg-red-100 text-red-800',
// AI Insights
not_mentioned: 'bg-red-100 text-red-800', no_conversion: 'bg-violet-100 text-violet-800', idea: 'bg-blue-100 text-blue-800',
// Core Web Vitals: geslaagd, niet geslaagd, te weinig Chrome-verkeer
cwv_pass: 'bg-green-100 text-green-700', cwv_fail: 'bg-red-100 text-red-700', cwv_nodata: 'bg-gray-100 text-gray-500'
};
var ICON = { cwv_pass: 'fa-check', cwv_fail: 'fa-times', cwv_nodata: 'fa-minus' };
return {
restrict: 'E',
scope: { kind: '@' },
// Core Web Vitals als pil met een icoon (vinkje, kruis, streepje): een status nooit alleen met kleur
template: '{{\'ui.badge.\' + kind | translate}} ' +
' {{\'ui.badge.\' + kind | translate}} ',
link: function (scope) { scope.c = C; scope.icon = ICON; }
};
});
// --- AI-antwoord: status van je site in één antwoord (AI Insights). status: linked (bron op plek pos) | mentioned |
// none | nosearch (de AI zocht niet op het web) | leeg (niet gemeten)
app.directive('mtAiStatus', function () {
var C = { linked: 'bg-green-100 text-green-800', mentioned: 'bg-blue-100 text-blue-800', none: 'bg-gray-100 text-gray-500', nosearch: 'bg-amber-50 text-amber-800' };
return {
restrict: 'E',
scope: { status: '@', pos: '@' },
template: '' +
'{{\'ui.ai_status.\' + status | translate:{pos: pos} }} – ',
link: function (scope) { scope.c = C; }
};
});
// --- ster: volgen of niet (zoekwoorden)
app.directive('mtStar', function () {
return {
restrict: 'E',
scope: { on: '<', onToggle: '&' },
template: ' '
};
});
// --- kolomkop die sorteert:
// sort: {key, desc}; een klik op dezelfde kolom draait de richting om
app.directive('mtTh', function () {
return {
restrict: 'A',
scope: { key: '@mtTh', sort: '=', label: '@', align: '@' },
template: '{{label}} ',
link: function (scope, el) {
el.addClass('py-2 pr-4 font-medium' + (scope.align === 'right' ? ' text-right' : ''));
scope.go = function () {
if (scope.sort.key === scope.key) { scope.sort.desc = !scope.sort.desc; } else { scope.sort.key = scope.key; scope.sort.desc = true; }
};
}
};
});
// --- Core Web Vital in een tabelregel (scorebord): het getal, een korte noot ernaast, en daaronder een dunne balk met
// de zones goed/matig/slecht van Google (mtCwv.SCALE) met een stip op de waarde; own: de stip blauw (jij), anders grijs
// metric: lcp | inp | cls; value: getal of null (dan '–' en geen stip); text: eigen notatie (standaard mtCwv.fmt)
app.directive('mtCwvTrack', ['mtCwv', function (mtCwv) {
return {
restrict: 'E',
scope: { metric: '@', value: '<', text: '@', note: '@', own: '' },
template:
'' +
'
{{text || shown}} ' +
'{{note}}
' +
'
',
link: function (scope) {
scope.c = mtCwv.TRACK;
scope.$watchGroup(['metric', 'value'], function () {
var s = mtCwv.SCALE[scope.metric] || [1, 2, 3], v = scope.value;
scope.z = [s[0] / s[2], (s[1] - s[0]) / s[2], (s[2] - s[1]) / s[2]];
scope.x = v === null || v === undefined || isNaN(v) ? null : 100 * Math.min(Math.max(v, 0), s[2]) / s[2];
scope.shown = mtCwv.fmt(scope.metric, v);
});
}
};
}]);
// --- kleine grafieken naast elkaar (small multiples, Arjen 2026-10-28: de trend was "rommelig"): per paneel één metric
// over dezelfde weken, de zones van Google als lichte vlakken, jij als dikke blauwe lijn met een stip aan het eind.
// mode "group": de concurrenten als één grijze band (snelste tot traagste per week) met de middelste als stippellijn;
// mode "each": een dunne lijn per concurrent in een vaste kleur per domein (nooit per rang), met legenda erboven en het
// domein aan het eind van elke lijn. Eén muis voor alle panelen: een verticale lijn en een tooltip per paneel in
// dezelfde week. Een week zonder waarde is een gat, nooit 0.
// data: {n, titles: [kop per week], months: [{i, label}], labels: {own, med, nodata}, others: [{label, color}] (legenda),
// panels: [{title, sub, fmt, tick, max, ticks: [..], good, poor, limitLabel, own: [..], band: {lo, hi}, med: [..],
// others: [{label, color, values}], takeaway}]}
app.directive('mtMultiples', function () {
var W = 400, H = 190;
function X(i, n) { return n > 1 ? i * W / (n - 1) : W / 2; }
// een pad met losse stukken waar data ontbreekt; een stuk van één week een rond puntje
function path(vals, n, Y) {
var d = '', pen = false;
(vals || []).forEach(function (v, i) {
if (v === null || v === undefined || isNaN(v)) { pen = false; return; }
var next = vals[i + 1];
var alone = !pen && (next === null || next === undefined || isNaN(next));
d += (pen ? ' L' : ' M') + X(i, n).toFixed(1) + ' ' + Y(v).toFixed(1) + (alone ? ' h0.01' : '');
pen = true;
});
return d.trim();
}
function bandPath(lo, hi, n, Y) {
var out = [], run = [];
function flush() {
if (run.length > 1) {
out.push('M' + run.map(function (i) { return X(i, n).toFixed(1) + ' ' + Y(hi[i]).toFixed(1); }).join(' L') +
' L' + run.slice().reverse().map(function (i) { return X(i, n).toFixed(1) + ' ' + Y(lo[i]).toFixed(1); }).join(' L') + ' Z');
}
run = [];
}
for (var i = 0; i < n; i++) { if (lo[i] !== null && lo[i] !== undefined && hi[i] !== null && hi[i] !== undefined) { run.push(i); } else { flush(); } }
flush();
return out.join(' ');
}
function lastOf(vals) { for (var i = (vals || []).length - 1; i >= 0; i--) { if (vals[i] !== null && vals[i] !== undefined) { return i; } } return -1; }
return {
restrict: 'E',
scope: { data: '<', mode: '@' },
template:
'' +
'{{data.labels.own}} ' +
' {{o.label}}
' +
'' +
'
' +
'
' +
'
' +
'
' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
'
{{t.label}}
' +
'
{{p.limitLabel}}
' +
'
' +
'
' +
'
{{l.label}}
' +
'
' +
'
{{data.titles[hover]}}
' +
'
{{r.label}} {{r.value}}
' +
'
' +
'
{{p.takeaway}}
',
link: function (scope) {
scope.hover = null;
function build() {
var d = scope.data, each = scope.mode === 'each';
scope.panels = [];
scope.gutter = each ? 84 : 0;
if (!d || !d.panels) { return; }
var n = d.n;
(d.months || []).forEach(function (m) { m.x = n > 1 ? 100 * m.i / (n - 1) : 50; });
scope.panels = d.panels.map(function (p) {
var max = p.max || 1;
var Y = function (v) { return H - Math.min(Math.max(v, 0), max) / max * H; };
var gy = Y(p.good), ny = p.poor < max ? Y(p.poor) : 0;
var others = (p.others || []).map(function (o) {
var li = lastOf(o.values);
return { label: o.label, color: o.color, d: path(o.values, n, Y), end: li >= 0 ? { x: 100 * X(li, n) / W, y: 100 * Y(o.values[li]) / H } : null };
});
var li = lastOf(p.own);
// het domein aan het eind van elke lijn, niet over elkaar (minstens 7,5% van de hoogte uit elkaar)
var labels = others.filter(function (o) { return o.end; }).map(function (o) { return { label: o.label, ink: '#374151', y: o.end.y }; })
.sort(function (a, b) { return a.y - b.y; });
for (var k = 1; k < labels.length; k++) { labels[k].y = Math.max(labels[k].y, labels[k - 1].y + 7.5); }
var over = labels.length ? labels[labels.length - 1].y - 97 : 0;
labels.forEach(function (l) { l.y = Math.max(3, l.y - Math.max(over, 0)); });
return {
title: p.title, sub: p.sub, takeaway: p.takeaway, fmt: p.fmt, src: p,
zGoodY: gy, zGoodH: H - gy, zNiY: ny, zNiH: Math.max(gy - ny, 0), zPoorH: p.poor < max ? ny : 0,
limitTop: p.good < max ? 100 * gy / H : null, limitLabel: p.limitLabel,
ticks: (p.ticks || []).map(function (t) { return { y: 100 * Y(t) / H, label: p.tick(t) }; }),
band: p.band ? bandPath(p.band.lo, p.band.hi, n, Y) : '', med: path(p.med, n, Y), own: path(p.own, n, Y), others: others, labels: labels,
end: li >= 0 ? { x: 100 * X(li, n) / W, y: 100 * Y(p.own[li]) / H } : null, tip: []
};
});
if (scope.hover !== null) { tips(scope.hover); }
}
function tips(i) {
var d = scope.data, each = scope.mode === 'each', L = d.labels || {};
scope.panels.forEach(function (p) {
var s = p.src, f = s.fmt, rows = [];
var own = s.own[i];
rows.push({ color: '#60a5fa', label: L.own, value: own === null || own === undefined ? '– (' + L.nodata + ')' : f(own) });
if (each) {
(s.others || []).forEach(function (o) { rows.push({ color: o.color, label: o.label, value: o.values[i] === null || o.values[i] === undefined ? '–' : f(o.values[i]) }); });
} else if (s.med && s.med.length) {
rows.push({ dash: true, label: L.med, value: s.med[i] === null || s.med[i] === undefined ? '–' : f(s.med[i]) });
}
p.tip = rows;
});
}
scope.$watchGroup(['data', 'mode'], build);
scope.move = function (e) {
var n = scope.data.n, r = e.currentTarget.getBoundingClientRect();
var i = n > 1 ? Math.round(Math.min(Math.max((e.clientX - r.left) / r.width, 0), 1) * (n - 1)) : 0;
if (i === scope.hover) { return; }
scope.hover = i;
scope.hx = X(i, n);
scope.hpct = 100 * scope.hx / W;
scope.shift = scope.hpct < 22 ? 'translateX(-8px)' : (scope.hpct > 78 ? 'translateX(calc(-100% + 8px))' : 'translateX(-50%)');
tips(i);
};
scope.leave = function () { scope.hover = null; };
}
};
});
// --- twee grafieken onder elkaar over dezelfde weken, met één muis (nooit twee assen in één grafiek): boven een lijn
// (bv. de LCP, met de zones van Google als lichte vlakken), onder staven (bv. bezoekers). Een verticale lijn door beide
// en één tooltip boven. Een week zonder waarde: een gat in de lijn, geen staaf.
// data: {n, titles: [kop per week], months: [{i, label}], top: {title, sub, label, values, fmt, tick, max, good, poor},
// bottom: {title, sub, badge, label, values, fmt, tick, max}}
app.directive('mtPair', function () {
var W = 1000, H = 170;
return {
restrict: 'E',
transclude: true,
scope: { data: '<' },
template:
'' +
'
{{data.top.title}}
{{data.top.sub}}
' +
'
' +
'
' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
'
{{t.axis[0]}}
' +
'
{{t.axis[1]}}
' +
'
{{t.axis[2]}}
' +
'
' +
'
{{data.titles[hover]}}
{{tipText}}
' +
'
' +
'' +
'
{{data.bottom.title}}{{data.bottom.badge}}
{{data.bottom.sub}}
' +
'
' +
'
' +
'
' +
'
{{b.axis[0]}}
' +
'
{{b.axis[1]}}
' +
'
' +
'
',
link: function (scope) {
scope.hover = null;
function x(i, n) { return (i + 0.5) / n; }
scope.$watch('data', function (d) {
scope.t = {}; scope.b = {}; scope.bars = []; scope.months = [];
// andere gegevens (een andere metric): de oude tooltip klopt niet meer
scope.hover = null;
if (!d || !d.top) { return; }
var n = d.n, tp = d.top, max = tp.max || 1;
var Y = function (v) { return H - Math.min(Math.max(v, 0), max) / max * H; };
var dd = '', pen = false;
(tp.values || []).forEach(function (v, i) {
if (v === null || v === undefined) { pen = false; return; }
var next = tp.values[i + 1], alone = !pen && (next === null || next === undefined);
dd += (pen ? ' L' : ' M') + (W * x(i, n)).toFixed(1) + ' ' + Y(v).toFixed(1) + (alone ? ' h0.01' : '');
pen = true;
});
var gy = Y(tp.good), ny = tp.poor < max ? Y(tp.poor) : 0;
scope.t = { d: dd.trim(), zGoodY: gy, zGoodH: H - gy, zNiY: ny, zNiH: Math.max(gy - ny, 0), zPoorH: tp.poor < max ? ny : 0, axis: [tp.tick(max), tp.tick(max / 2), tp.tick(0)] };
scope.months = (d.months || []).map(function (m) { return { label: m.label, x: 100 * x(m.i, n) }; });
var bt = d.bottom;
if (bt && bt.values) {
var bm = bt.max || 1;
scope.b = { axis: [bt.tick(bm), bt.tick(0)] };
scope.bars = bt.values.map(function (v) { return { h: v === null || v === undefined ? null : Math.max(1, 100 * Math.min(v, bm) / bm) }; });
}
});
scope.move = function (e) {
var d = scope.data, n = d.n, r = e.currentTarget.getBoundingClientRect();
var i = Math.min(n - 1, Math.max(0, Math.floor((e.clientX - r.left) / r.width * n)));
scope.hover = i;
scope.hpct = 100 * x(i, n);
scope.hx = W * x(i, n);
scope.shift = scope.hpct < 15 ? 'translateX(-8px)' : (scope.hpct > 85 ? 'translateX(calc(-100% + 8px))' : 'translateX(-50%)');
var tv = d.top.values[i], bv = d.bottom && d.bottom.values ? d.bottom.values[i] : undefined;
scope.tipText = d.top.label + ' ' + (tv === null || tv === undefined ? '–' : d.top.fmt(tv)) +
(bv !== undefined ? ' · ' + d.bottom.label + ' ' + (bv === null ? '–' : d.bottom.fmt(bv)) : '');
};
scope.leave = function () { scope.hover = null; };
}
};
});
// --- bevinding in een kaartje (Diagnose): soort met een gekleurde stip, een vette zin en een regel uitleg
// kind: het label (bv. "Advertenties", "Concurrent"); color: de stip; head: de bevinding; text: wat het betekent
app.directive('mtFinding', function () {
return {
restrict: 'E',
scope: { kind: '@', color: '@', head: '@', text: '@' },
template:
'' +
'
' +
'{{kind}}
' +
'
{{head}}
' +
'
{{text}}
'
};
});
// Wat bewaard blijft tussen de pagina's van Search Console Insights: de laatst opgehaalde antwoorden per adres
// (direct tonen, op de achtergrond vernieuwen) en de filters van Verkennen.
var gsciState = { site: null, cache: {}, explore: null, compare: 'prev' };
// Search Console in een rapport (newlayout/gscinsights/report.html): de periode van het rapport ($rootScope.reportdates)
// tegenover de periode ervoor, en de grootste 25 zoekwoorden, pagina's, landen of apparaten (custominit.urlview)
app.controller('gscinsightsReport', ['$scope', '$rootScope', '$http', '$translate', 'h', 'mtFmt', function ($scope, $rootScope, $http, $translate, h, mtFmt) {
var DIMS = { keywords: 'query', pages: 'page', country: 'country', device: 'device' };
var view = ($scope.custominit && $scope.custominit.urlview) || 'keywords';
var dim = DIMS[view] || 'query';
var t = function (k, v) { return $translate.instant(k, v); };
var r = $scope.r = { f: mtFmt, rows: null, chart: null, title: '', kpis: ['clicks', 'impressions', 'ctr', 'position'].map(function (k) { return { label: '', loading: true }; }) };
var dates = $rootScope.reportdates || {};
var base = { websiteid: h.siteId(), start: dates.start || '30DAYSAGO', end: dates.end || 'TODAY' };
var countryName = function (cc) { try { return cc ? new Intl.DisplayNames([$translate.use() || 'nl'], { type: 'region' }).of(cc) : ''; } catch (e) { return cc; } };
$translate.onReady().then(function () {
r.title = t('gscinsights.explore.dim.' + dim);
$http.get('/rest/gscinsights/report', { params: angular.extend({ dimension: dim, compare: 'PERIOD', limit: 25 }, base) }).then(function (res) {
var d = res.data, now = (d.totals || [])[0] || {}, before = (d.totals || [])[1];
var ch = function (a, b) { return b ? 100 * (a - b) / b : null; };
r.kpis = [
{ label: t('gscinsights.kpi.clicks'), value: mtFmt.num(now.clicks), delta: before ? ch(now.clicks, before.clicks) : null },
{ label: t('gscinsights.kpi.impressions'), value: mtFmt.num(now.impressions), delta: before ? ch(now.impressions, before.impressions) : null },
{ label: t('gscinsights.kpi.ctr'), value: mtFmt.pct(now.ctr), delta: before ? now.ctr - before.ctr : null, unit: 'pt' },
{ label: t('gscinsights.kpi.position'), value: mtFmt.pos(now.position), delta: before && now.position && before.position ? now.position - before.position : null, invert: true, unit: 'none' }
];
r.rows = (d.data || []).map(function (x) {
x.label = dim === 'page' ? (x.page.replace(/^https?:\/\/[^\/]+/, '') || '/') : (dim === 'device' ? t('gscinsights.explore.device_name.' + x.device) : (dim === 'country' ? countryName(x.cc) || (x.country || '').toUpperCase() : x.query));
return x;
});
});
$http.get('/rest/gscinsights/report', { params: angular.extend({ dimension: 'date' }, base) }).then(function (res) {
var rows = res.data.data || [];
r.chart = { labels: rows.map(function (x, i) { return i % 7 === 0 ? mtFmt.day(x.date) : ''; }), titles: rows.map(function (x) { return mtFmt.day(x.date); }),
series: [{ label: t('gscinsights.kpi.clicks'), color: '#2563eb', values: rows.map(function (x) { return x.clicks; }) }] };
});
});
}]);
// site-instellingen: Search Console alleen voor één land (standaard wereldwijd)
app.controller('gscinsightsSiteSettings', ['$scope', '$http', 'h', function ($scope, $http, h) {
var s = $scope.gsciSettings = { country: '', loaded: false, saved: false };
var load = function () {
$http.get('/rest/gscinsights/settings', { params: { websiteid: h.siteId() } }).then(function (res) {
s.country = res.data.country || '';
s.canEdit = res.data.can_edit;
s.property = res.data.property;
s.connected = res.data.connected;
s.siteId = h.siteId();
s.loaded = true;
});
};
load();
// ontkoppeld in Koppelingen (js/controller/settings.site.js): opnieuw ophalen en Search Insights niet uit de cache
$scope.$on('siteConnectionsChanged', function () {
gsciState.cache = {};
load();
});
s.save = function () {
$http.post('/rest/gscinsights/settings', { websiteid: h.siteId(), country: s.country }).then(function () {
s.saved = true;
gsciState.cache = {};
});
};
}]);
// Search Insights in tegels, zinnen en grafieken; gedeeld met het Dashboard (js/controller/home.js), zodat beide hetzelfde
// laten zien: de cijfertegels, de grafiek per maand, waarom het veranderde en de kansen.
app.factory('gsciView', ['$translate', 'mtFmt', function ($translate, mtFmt) {
var t = function (k, v) { return $translate.instant(k, v); };
var f = mtFmt;
var BASE = '/search-insights';
var view = {
kpis: function (now, before, ga4) {
var ch = function (a, b) { return b ? 100 * (a - b) / b : null; };
var out = [
{ key: 'clicks', label: t('gscinsights.kpi.clicks'), value: f.num(now.clicks), delta: before ? ch(now.clicks, before.clicks) : null },
{ key: 'impressions', label: t('gscinsights.kpi.impressions'), value: f.num(now.impressions), delta: before ? ch(now.impressions, before.impressions) : null },
{ key: 'ctr', label: t('gscinsights.kpi.ctr'), value: f.pct(now.ctr), delta: before ? now.ctr - before.ctr : null, unit: 'pt', digits: 1 },
{ key: 'position', label: t('gscinsights.kpi.position'), value: f.pos(now.position), delta: before && now.position && before.position ? now.position - before.position : null, invert: true, unit: 'none', digits: 1, note: t('gscinsights.kpi.position_note') }
];
if (ga4 === null || ga4 === undefined) {
out.push({ label: t('gscinsights.kpi.revenue'), value: '–', note: t('gscinsights.kpi.no_ga4') });
} else if (ga4.has_revenue) {
out.push({ key: 'revenue', label: t('gscinsights.kpi.revenue'), value: f.euro(ga4.now.revenue), delta: ch(ga4.now.revenue, ga4.before && ga4.before.revenue), note: t('gscinsights.kpi.organic') });
} else {
out.push({ key: 'conversions', label: t('gscinsights.kpi.conversions'), value: f.num(ga4.now.conversions), delta: ch(ga4.now.conversions, ga4.before && ga4.before.conversions), note: t('gscinsights.kpi.organic') });
}
return out;
},
causeText: function (r) {
if (r.cause === 'position') { return t('gscinsights.cause.position', { from: f.pos(r.before.position), to: f.pos(r.now.position) }); }
if (r.cause === 'demand') { return t('gscinsights.cause.demand', { pct: f.num(100 * (r.now.impressions - r.before.impressions) / r.before.impressions) + '%' }); }
return t('gscinsights.cause.' + r.cause);
},
actionText: function (a) {
if (a.kind === 'chance') { return t('gscinsights.action.chance', { q: a.query, pos: f.pos(a.position), impr: f.num(a.impressions), gain: f.num(a.gain) }); }
if (a.kind === 'lowctr') { return t('gscinsights.action.lowctr', { q: a.query, pos: f.pos(a.position), ctr: f.pct(a.ctr), exp: f.pct(a.expected_ctr) }); }
if (a.kind === 'decay') {
var cause = view.causeText({ cause: a.cause, before: { position: a.position_before, impressions: a.impressions_before }, now: { position: a.position, impressions: a.impressions } });
return t('gscinsights.action.decay', { q: a.query, pct: f.num(-a.change) + '%', cause: cause });
}
if (a.kind === 'cannibal') {
return t('gscinsights.action.cannibal', { q: a.query, n: a.pages.length, shares: a.pages.map(function (p) { return p.share + '%'; }).join(' / ') });
}
return a.query;
},
exploreHref: function (q) { return BASE + '/verkennen?q=' + encodeURIComponent(q) + '&q_op=equals'; },
// waarom het steeg of daalde (part why): een zin en de zoekwoorden met het grootste verschil
why: function (w) {
return {
down: w.diff < 0,
text: t(w.diff < 0 ? 'gscinsights.overview.why_down' : 'gscinsights.overview.why_up', { n: f.num(Math.abs(w.diff)) }),
rows: w.rows.map(function (r) { return { q: r.query, diff: f.num(r.diff), up: r.diff > 0, why: view.causeText(r), href: view.exploreHref(r.query) }; })
};
},
overviewChart: function (d, metric) {
var months = d.months || [], brand = d.brand.length > 0;
var col = function (k) { return months.map(function (m) { return m[k] === undefined ? null : m[k]; }); };
var labels = months.map(function (m) { return f.month(m.month); });
var titles = months.map(function (m) { return f.monthLong(m.month) + (m.partial ? ' · ' + t('gscinsights.overview.partial') : ''); });
var lastYear = function (k) { return { label: t('gscinsights.overview.last_year'), color: '#9ca3af', values: months.map(function (m) { return m.ly ? m.ly[k] : null; }) }; };
var out = { chartKind: 'bars', chartFmt: null, chartInvert: false, brandNote: '', chartTitle: t('gscinsights.overview.' + metric + '_per_month') };
if (metric === 'ctr' || metric === 'position') {
angular.extend(out, { chartKind: 'line', chartFmt: metric === 'ctr' ? function (v) { return f.pct(v); } : f.pos, chartInvert: metric === 'position',
chart: { labels: labels, titles: titles, series: [
{ label: t('gscinsights.kpi.' + metric), color: '#3b82f6', values: col(metric), width: 2.5, strong: true },
angular.extend(lastYear(metric), { dash: true })
] } });
return out;
}
if (metric === 'revenue' || metric === 'conversions') {
out.chartFmt = metric === 'revenue' ? f.euro : null;
out.chart = { labels: labels, titles: titles, faded: col('partial'), line: lastYear(metric),
series: [{ label: t('gscinsights.kpi.' + metric), color: '#047857', values: col(metric) }] };
return out;
}
if (metric !== 'impressions') {
metric = 'clicks';
out.chartTitle = t('gscinsights.overview.clicks_per_month');
}
var brandKey = metric === 'clicks' ? 'brand' : 'brand_impressions';
out.chart = {
labels: labels, titles: titles, faded: col('partial'), line: lastYear(metric),
series: brand ? [
{ label: t('gscinsights.overview.brand'), color: '#1e40af', values: col(brandKey) },
{ label: t('gscinsights.overview.nonbrand'), color: '#60a5fa', values: months.map(function (m) { return m[metric] === null ? null : m[metric] - m[brandKey]; }) }
] : [{ label: t('gscinsights.kpi.' + metric), color: '#3b82f6', values: col(metric) }],
// bij klikken in de tooltip ook de opbrengst van die maand
extra: metric === 'clicks' ? months.map(function (m) { return m.revenue !== null ? [{ label: t('gscinsights.overview.revenue'), value: f.euro(m.revenue), color: '#047857' }] : []; }) : null
};
out.brandNote = brand ? t('gscinsights.overview.brand_terms', { terms: d.brand.join(', ') }) : '';
return out;
},
};
return view;
}]);
app.controller('gscinsights', ['$scope', '$rootScope', '$location', '$routeParams', '$http', '$translate', '$timeout', 'h', 'mtFmt', 'gsciView', function ($scope, $rootScope, $location, $routeParams, $http, $translate, $timeout, h, mtFmt, gsciView) {
// Search Console Insights: /search-insights (overzicht), /search-insights/onderwerpen(/:id), /gevolgd, /verkennen.
// Server: rest/data/gscinsights/, regels in rest/include/gscinsights.php. Bouwblokken: js/ui.js (docs/ui.md).
// Nog niet gelanceerd: zichtbaar via user.site.show.gscinsights.
// in een rapport (newlayout/gscinsights/report_page.html): de maand van het rapport tegenover de maand ervoor,
// alleen lezen. custominit.urlview: overview | topics | tracked | actions
var report = $scope.custominit && $scope.custominit.gsci ? $scope.custominit : null;
if (!report && (!$rootScope.user || !$rootScope.user.site || !$rootScope.user.site.show || !$rootScope.user.site.show.gscinsights)) {
$location.path('/');
return;
}
var TABS = { '': 'overview', onderwerpen: 'topics', gevolgd: 'tracked', verkennen: 'explore' };
var BASE = '/search-insights';
var t = function (k, v) { return $translate.instant(k, v); };
var f = mtFmt;
if (gsciState.site !== h.siteId()) {
gsciState = { site: h.siteId(), cache: {}, explore: null, compare: 'prev' };
}
var c = $scope.c = {
f: f,
tab: report ? (report.urlview === 'actions' ? 'overview' : report.urlview) : (TABS[$routeParams.tab || ''] || 'overview'),
topicId: report ? null : ($routeParams.id || null),
compare: report ? 'prev' : gsciState.compare,
report: report ? report.urlview : null,
status: gsciState.cache.status || null,
data: null,
loading: true,
error: null,
v: {},
// welke tegel op het overzicht de grafiek bepaalt
metric: 'clicks',
init: function () {
var self = this;
self.tabs = [
{ key: 'overview', label: t('gscinsights.tab.overview'), href: BASE },
{ key: 'topics', label: t('gscinsights.tab.topics'), href: BASE + '/onderwerpen' },
{ key: 'tracked', label: t('gscinsights.tab.tracked'), href: BASE + '/gevolgd' },
{ key: 'explore', label: t('gscinsights.tab.explore'), href: BASE + '/verkennen' }
];
self.siteId = h.siteId();
self.get('status', {}).then(function (s) {
self.status = s;
// de properties van het gekoppelde account alleen voor wie de koppeling mag beheren (bij een white label: alle klanten)
if (s.gsc === 'no_property' && s.can_manage) { self.loadProperties(); }
if (s.gsc !== 'ok') { self.loading = false; return; }
self.load();
});
},
// gekoppeld zonder property: de lijst om uit te kiezen, en kiezen
loadProperties: function () {
var self = this;
$http.get('/rest/gscinsights/getcustomer', { params: { websiteid: h.siteId() } }).then(function (res) {
// sc-domain:x.nl is een domein-property (alle subdomeinen en http/https): toon x.nl met het label Domein;
// een URL-prefix toont het adres. Wat bij deze site past, staat bovenaan.
var site = String(($rootScope.user && $rootScope.user.site && $rootScope.user.site.domain) || '').replace(/^www\./, '');
self.properties = (res.data.data || []).map(function (p, i) {
var dom = /^sc-domain:/.test(p.siteUrl);
var name = dom ? p.siteUrl.replace(/^sc-domain:/, '') : p.siteUrl.replace(/^https?:\/\//, '').replace(/\/$/, '');
var host = name.split('/')[0];
var bare = host.replace(/^www\./, '');
return { url: p.siteUrl, name: name, host: host, kind: dom ? 'domain' : 'url', i: i,
match: !!site && (bare === site || (dom && site.slice(-bare.length - 1) === '.' + bare)) };
}).sort(function (a, b) { return (b.match - a.match) || (a.i - b.i); });
});
},
chooseProperty: function (p) {
var self = this;
$http.get('/rest/gscinsights/setcustomer', { params: { websiteid: h.siteId(), customer: p } }).then(function () {
gsciState.cache = {};
self.status = null;
self.init();
});
},
// GET /rest/gscinsights/; het vorige antwoord voor hetzelfde adres staat er meteen
get: function (what, params, onCached) {
var key = what + '|' + JSON.stringify(params);
if (gsciState.cache[key] && onCached) { onCached(gsciState.cache[key]); }
return $http.get('/rest/gscinsights/' + what, { params: angular.extend({ websiteid: h.siteId() }, params) }).then(function (res) {
gsciState.cache[key] = res.data;
return res.data;
});
},
// elke pagina in delen, tegelijk opgevraagd; elke kaart vult zich zodra zijn deel er is (placeholders ertussen)
PARTS: { overview: ['kpis', 'chart', 'why', 'actions'], topics: ['stats', 'values'], topic: ['main', 'chart', 'extra'], explore: ['summary', 'rows'], tracked: [null] },
load: function () {
var self = this;
var what = self.tab === 'topics' ? (self.topicId ? 'topic' : 'topics') : self.tab;
var params = self.tab === 'explore' ? self.exploreParams() : (self.tab === 'tracked' ? {} : { compare: self.compare });
if (self.topicId) { params.id = self.topicId; }
var parts = self.PARTS[what];
if (report) {
var dates = $rootScope.reportdates || {};
angular.extend(params, { start: dates.start, end: dates.end });
// een rapportpagina vraagt alleen de delen op die ze toont
if (what === 'overview') { parts = report.urlview === 'actions' ? ['actions'] : ['kpis', 'chart', 'why']; }
}
var key = what + '|' + JSON.stringify(params);
if (!self.data || self.data.key !== key) { self.data = { key: key, parts: {} }; }
self.loading = false;
self.error = null;
self.refresh();
parts.forEach(function (part) {
var p = part ? angular.extend({ part: part }, params) : params;
var show = function (d) {
if (self.data.key !== key) { return; }
if (d.error) { self.error = d; return; }
self.data.parts[part || 'all'] = d;
self.refresh();
};
self.get(what, p, show).then(show, function (res) {
self.error = { error: res.status === 403 ? 'no access' : 'google', message: (res.data && res.data.error) || '' };
});
});
},
setCompare: function (v) {
if (this.compare === v) { return; }
this.compare = gsciState.compare = v;
this.loading = !this.data;
this.load();
},
// alles wat ng-repeat toont, één keer per nieuwe data (geen functies in ng-repeat: eindeloze digest)
refresh: function () {
var self = this, d = self.data;
self.v = {};
if (self.tab === 'overview') { self.v = self.viewOverview(d); }
if (self.tab === 'topics' && !self.topicId) { self.v = self.viewTopics(d); }
if (self.tab === 'topics' && self.topicId) { self.v = self.viewTopic(d); }
if (self.tab === 'tracked') { self.v = d.parts.all ? self.viewTracked(d.parts.all) : { loading: true }; }
if (self.tab === 'explore') { self.v = self.viewExplore(d); }
},
// ------------------------------------------------------------------ gedeeld
kpis: function (now, before, ga4) { return gsciView.kpis(now, before, ga4); },
causeText: function (r) { return gsciView.causeText(r); },
actionText: function (a) { return gsciView.actionText(a); },
exploreHref: function (q) { return gsciView.exploreHref(q); },
// ------------------------------------------------------------------ overzicht
viewOverview: function (all) {
var self = this, p = all.parts || {};
var v = { kpis: null, chart: null, why: null, actions: null };
// tegels: tot de cijfers er zijn vijf tegels met alleen het label (geen verspringen)
v.kpis = p.kpis ? self.kpis(p.kpis.totals.now, p.kpis.totals.before, p.kpis.ga4) : ['clicks', 'impressions', 'ctr', 'position', 'revenue'].map(function (k) { return { label: t('gscinsights.kpi.' + k), loading: true }; });
if (p.chart) { v.chartData = p.chart; angular.extend(v, self.viewOverviewChart(p.chart, self.metric)); }
if (p.why) {
var w = gsciView.why(p.why.why);
v.whyDown = w.down;
v.whyText = w.text;
v.why = w.rows;
}
if (p.actions) {
v.actions = p.actions.actions.map(function (a) {
return { kind: a.kind, text: self.actionText(a), gain: a.gain, value: p.actions.has_revenue && a.value ? f.euro(a.value) : null, href: self.exploreHref(a.query) };
});
}
return v;
},
// Arjen (2026-10-28): een klik op een tegel zet die waarde in de grafiek. Klikken en vertoningen als staven (met het
// merkdeel), CTR en positie als lijn (gemiddelden stapel je niet), GA4 als staven; steeds met vorig jaar erbij.
pickMetric: function (key) {
this.metric = key;
if (this.v.chartData) { angular.extend(this.v, this.viewOverviewChart(this.v.chartData, key)); }
},
viewOverviewChart: function (d, metric) { return gsciView.overviewChart(d, metric); },
// ------------------------------------------------------------------ onderwerpen
viewTopics: function (all) {
var self = this, d = all.parts.stats, val = all.parts.values;
// de toestand van de clustering: uit het antwoord, anders al bekend uit de status (meteen goed scherm)
var cs = (d && d.clustering) || (val && val.clustering) || (self.status && self.status.clustering) || {};
var v = { state: cs, stepText: cs.step ? t('gscinsights.topics.step.' + cs.step, { done: cs.done, total: cs.total }) : '' };
// bij een stap die lang duurt (het groeperen) staat de balk stil: laat zien dat hij nog loopt
if (v.stepText && cs.minutes) { v.stepText += ' · ' + t('gscinsights.topics.minutes', { n: cs.minutes }); }
if (cs.state === 'working') { self.poll(); }
v.loadingTiles = (cs.state === 'ready' || cs.refreshing) && !(d && d.topics);
if (!d || !d.topics) { return v; }
var values = (val && val.has_revenue && val.values) || {};
var named = d.topics.filter(function (x) { return x.id !== '_none'; });
var none = d.topics.filter(function (x) { return x.id === '_none'; })[0];
v.heroText = t('gscinsights.topics.hero_text', { queries: f.num(d.total_queries), topics: named.length });
v.heroMeta = cs.completed_at ? t('gscinsights.topics.hero_meta', { date: f.day(cs.completed_at * 1000) }) : '';
v.tiles = named.map(function (x, i) {
// de grootste groot, daarna kleiner: 3x2, 2x1, 1x1
var cols = i < 2 ? 3 : (i < 5 ? 2 : 1), rows = i < 2 ? 2 : 1;
var up = x.change === null ? null : x.change >= 0;
return {
id: x.id, name: x.name, cols: cols, rows: rows, big: cols >= 3,
sub: x.custom ? t('gscinsights.topics.custom') : (x.intent ? t('gscinsights.topics.intent.' + x.intent) : '') + (x.brand ? ' · ' + t('gscinsights.topics.brand') : ''),
clicks: f.num(x.now.clicks), change: x.change, value: values[x.id] ? f.euro(values[x.id]) : null,
cls: up === null ? 'bg-white border-gray-200' : (up ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'),
href: BASE + '/onderwerpen/' + encodeURIComponent(x.id)
};
});
v.none = none ? { clicks: f.num(none.now.clicks), queries: none.queries, href: BASE + '/onderwerpen/_none' } : null;
if (d.split) {
var colors = { informational: '#2563eb', commercial: '#7c3aed', transactional: '#db2777', navigational: '#0f766e' };
v.intent = Object.keys(d.split.intent).filter(function (k) { return d.split.intent[k] > 0; }).map(function (k) {
return { label: t('gscinsights.topics.intent.' + k), pct: d.split.intent[k], color: colors[k] || '#64748b' };
});
v.brand = d.split.brand;
}
return v;
},
// de clustering loopt: elke 5 seconden de toestand, tot hij klaar is (dan de onderwerpen)
poll: function () {
var self = this;
if (self._poll) { return; }
self._poll = $timeout(function () {
self._poll = null;
if ($scope.$$destroyed) { return; }
self.get('status', {}).then(function (s) {
self.status = s;
var cs = s.clustering;
if (cs.state === 'working') {
if (self.data.parts.stats) { self.data.parts.stats.clustering = cs; }
self.refresh();
} else {
self.load();
}
});
}, 5000);
},
startClustering: function () {
var self = this;
$http.post('/rest/gscinsights/topics_start', { websiteid: h.siteId() }).then(function () {
self.status.clustering = { state: 'working', progress: 5, step: 'starting' };
self.data.parts = {};
self.refresh();
});
},
viewTopic: function (all) {
var self = this, m = all.parts.main, c = all.parts.chart, x = all.parts.extra;
var tp = (m || c || x || {}).topic || {};
var hasRev = x && x.has_revenue;
var v = { name: tp.id === '_none' ? t('gscinsights.topics.unassigned') : (tp.name || ''), hasRev: hasRev, chart: null, actions: null, pages: null, keywords: null };
v.badges = [tp.custom ? t('gscinsights.topics.custom') : (tp.intent ? t('gscinsights.topics.intent.' + tp.intent) : null), tp.brand ? t('gscinsights.topics.brand') : (tp.custom || tp.id === '_none' || !tp.id ? null : t('gscinsights.topics.nonbrand'))].filter(Boolean);
if (m) {
v.queries = t('gscinsights.topics.queries', { n: f.num(m.keywords.length) });
v.kpis = self.kpis(m.totals.now, m.totals.before, null).slice(0, 4);
var values = (x && x.values) || {};
v.keywords = m.keywords.map(function (k) { k.value = values[k.query] || 0; return k; });
} else {
v.kpis = ['clicks', 'impressions', 'ctr', 'position'].map(function (k) { return { label: t('gscinsights.kpi.' + k), loading: true }; });
}
if (hasRev && m) { v.kpis[3] = { label: t('gscinsights.topics.kpi_value'), value: f.euro(x.value_total) }; }
if (c) {
v.chart = {
labels: c.months.map(function (mm) { return f.month(mm.month); }),
titles: c.months.map(function (mm) { return f.monthLong(mm.month); }),
series: [{ label: t('gscinsights.kpi.clicks'), color: '#3b82f6', values: c.months.map(function (mm) { return mm.clicks; }) }],
faded: c.months.map(function (mm, i) { return i === c.months.length - 1; })
};
v.partialNote = c.months_partial ? t('gscinsights.topics.per_month_partial', { n: f.num(c.months_partial) }) : '';
}
if (x) {
v.actions = x.actions.map(function (a) { return { kind: a.kind, text: self.actionText(a), gain: a.gain, value: hasRev && a.value ? f.euro(a.value) : null }; });
v.pages = x.pages.map(function (p) { return { url: p.page.replace(/^https?:\/\/[^\/]+/, '') || '/', full: p.page, share: p.share, value: hasRev ? f.euro(p.value) : null, conv: p.conversions !== null ? t('gscinsights.topics.conversions', { n: f.num(p.conversions) }) : null }; });
}
return v;
},
kwSort: { key: 'clicks', desc: true },
kwLimit: 25,
// volgen of niet (vanuit Onderwerpen en Verkennen); de ster verandert meteen
toggleTrack: function (r) {
var self = this;
r.tracked = !r.tracked;
$http.post('/rest/gscinsights/track', { websiteid: h.siteId(), keywords: [r.query], action: r.tracked ? 'add' : 'remove' }).then(function () {
// Gevolgde zoekwoorden opnieuw ophalen bij het volgende bezoek
Object.keys(gsciState.cache).forEach(function (k) { if (k.indexOf('tracked|') === 0) { delete gsciState.cache[k]; } });
});
},
// ------------------------------------------------------------------ gevolgde zoekwoorden
viewTracked: function (d) {
var self = this, months = d.months || [];
var all = [], top3 = 0, top3y = 0, clicks = 0, clicksY = 0, since = null, withYear = false;
(d.groups || []).forEach(function (g) {
g.keywords.forEach(function (k) {
all.push(k);
clicks += k.now.clicks;
if (k.year) { clicksY += k.year.clicks; withYear = true; if (k.year.position && k.year.position <= 3) { top3y++; } }
if (k.now.position && k.now.position <= 3) { top3++; }
if (k.since && (!since || k.since < since)) { since = k.since; }
k.sparkLabels = months.map(function (m) { return f.monthLong(m); });
k.measuredText = k.measured ? (k.measured.pos ? String(k.measured.pos) : t('gscinsights.tracked.not_top20')) : '–';
// Arjen (2026-10-28): geen kolom "Vorig jaar" meer; vorig jaar in de tooltip bij Klikken, en Trend
// (positie tegenover vorig jaar) in plaats van Gezondheid
k.clickTip = { t: k.query, r: [{ label: t('gscinsights.tracked.last_28'), value: f.num(k.now.clicks) }, { label: t('gscinsights.tracked.col.last_year'), value: k.year ? f.num(k.year.clicks) : '–' }] };
k.trend = self.trend(k);
});
});
var histMonths = since ? Math.max(1, Math.round((Date.now() - new Date(since).getTime()) / (30.44 * 86400000))) : 0;
return {
months: months,
count: all.length,
kpis: [
{ label: t('gscinsights.tracked.kpi_clicks'), value: f.num(clicks), delta: withYear && clicksY ? 100 * (clicks - clicksY) / clicksY : null, note: t('gscinsights.vs_year') },
{ label: t('gscinsights.tracked.kpi_top3'), value: top3 + ' ' + t('gscinsights.tracked.of', { n: all.length }), note: withYear ? t('gscinsights.vs_year') + ': ' + top3y : '' },
{ label: t('gscinsights.tracked.kpi_history'), value: t('gscinsights.tracked.months', { n: histMonths }), note: t('gscinsights.tracked.kpi_history_note') }
],
groups: (d.groups || []).map(function (g) {
// de kolom Gemeten alleen als een zoekwoord in de groep in de Rank tracker staat
return { name: g.name || t('gscinsights.tracked.no_group'), raw: g.name, change: g.change, sum: t('gscinsights.tracked.group_sum', { n: g.keywords.length, clicks: f.num(g.clicks) }), keywords: g.keywords,
hasMeasured: g.keywords.some(function (k) { return !!k.measured; }) };
}),
groupNames: (d.groups || []).map(function (g) { return g.name; }).filter(Boolean)
};
},
// stijgt, daalt of stabiel: de positie van nu tegenover dezelfde 28 dagen vorig jaar (10%, minstens 1 plaats)
trend: function (k) {
var now = k.now && k.now.position, ly = k.year && k.year.position;
if (!now) { return null; }
if (!ly) { return { key: 'new', cls: 'bg-blue-50 text-blue-700', tip: { t: t('gscinsights.tracked.col.trend'), r: [{ label: t('gscinsights.tracked.trend_new'), value: '' }] } }; }
var d = ly - now, lim = Math.max(1, ly * 0.1);
var key = d > lim ? 'up' : (d < -lim ? 'down' : 'flat');
return { key: key, cls: { up: 'bg-green-50 text-green-700', down: 'bg-red-50 text-red-700', flat: 'bg-gray-100 text-gray-600' }[key],
tip: { t: t('gscinsights.tracked.col.trend'), r: [{ label: t('gscinsights.tracked.trend_why', { from: f.pos(ly), to: f.pos(now) }), value: '' }] } };
},
selectedAny: function () { return this.selected().length > 0; },
// ------------------------------------------------------------------ uitklap (Gevolgd en Verkennen)
// één zoekwoord of pagina openklappen, in Gevolgd en Verkennen dezelfde uitklap (gscinsights/detail): grafiek
// (positie, klikken, vertoningen, CTR; per dag of per maand) en daaronder de pagina's (bij een pagina: de
// zoekwoorden), landen en apparaten met dezelfde waarde als de grafiek
open: null,
// vast per soort (geen array-literal in ng-repeat: die maakt elke digest nieuwe objecten)
DETAIL_BLOCKS: {
page: [{ k: 'page', t: 'gscinsights.explore.dim.page' }, { k: 'country', t: 'gscinsights.explore.dim.country' }, { k: 'device', t: 'gscinsights.explore.dim.device' }],
query: [{ k: 'query', t: 'gscinsights.explore.dim.query' }, { k: 'country', t: 'gscinsights.explore.dim.country' }, { k: 'device', t: 'gscinsights.explore.dim.device' }]
},
detailMetrics: ['position', 'clicks', 'impressions', 'ctr'],
DEVICE_ICON: { DESKTOP: 'fa-desktop', MOBILE: 'fa-mobile', TABLET: 'fa-tablet' },
isOpen: function (kind, key) { return !!this.open && this.open.kind === kind && this.open.key === key; },
toggleDetail: function (kind, key, clicks) {
var self = this;
if (self.isOpen(kind, key)) { self.open = null; return; }
// in Verkennen met de filters en de periode van daar
var p = self.tab === 'explore' ? self.exploreParams() : {};
delete p.dim;
p[kind] = key;
// zonder klikken opent de grafiek op de positie (een lijn op 0 zegt niets)
var o = self.open = { kind: kind, key: key, metric: clicks > 0 ? 'clicks' : 'position', per: 'month', d: null };
var show = function (d) {
if (self.open !== o) { return; }
var l = d.lists || {};
(l.page || []).forEach(function (r) { r.label = r.page.replace(/^https?:\/\/[^\/]+/, '') || '/'; });
(l.query || []).forEach(function (r) { r.label = r.query; });
(l.country || []).forEach(function (r) { r.label = self.countryName(r.cc) || r.country; r.flag = r.cc ? '/static/img/flags/' + String(r.cc).toLowerCase() + '.svg' : null; });
(l.device || []).forEach(function (r) { r.label = t('gscinsights.explore.device_name.' + r.device); r.icon = self.DEVICE_ICON[r.device] || 'fa-question'; });
o.d = d;
o.blocks = self.DETAIL_BLOCKS[d.by] || self.DETAIL_BLOCKS.page;
self.detailChart();
};
self.get('detail', p, show).then(show);
},
detailSet: function (key, val) { this.open[key] = val; this.detailChart(); },
// nieuw data-object voor mt-line bij elke keuze, en de lijstjes eronder gesorteerd op dezelfde waarde
detailChart: function () {
var o = this.open, d = o && o.d, m = o && o.metric;
if (!d) { return; }
var rows = o.per === 'day' ? d.daily : d.monthly;
// jaartal bij januari (24 maanden: anders twee keer nov … okt)
var lbl = function (r) { return o.per === 'day' ? f.day(r.date) : (r.month.slice(5) === '01' ? f.month(r.month) + ' ’' + r.month.slice(2, 4) : f.month(r.month)); };
var labels = rows.map(lbl);
var series = [{ label: t('gscinsights.explore.col.' + m) + (m === 'position' && d.measured ? ' (Search Console)' : ''), color: '#2563eb', width: 2.25, nodots: true, values: rows.map(function (r) { return r[m]; }) }];
if (m === 'position' && d.measured) {
series.push({ label: t('gscinsights.tracked.measured_line'), color: '#f59e0b', dash: true, connect: true, nodots: true, values: rows.map(function (r) { return r.measured === undefined ? null : r.measured; }) });
}
o.chart = {
labels: o.per === 'day' ? labels.map(function (l, i) { return i % 14 === 0 ? l : ''; }) : labels,
titles: rows.map(function (r) { return o.per === 'day' ? f.day(r.date) : f.monthLong(r.month); }),
series: series
};
o.fmt = m === 'ctr' ? function (v) { return f.pct(v); } : (m === 'position' ? function (v) { return f.pos(v); } : null);
o.ints = m === 'clicks' || m === 'impressions';
o.valueFmt = m === 'ctr' ? function (v) { return f.pct(v); } : (m === 'position' ? f.pos : f.num);
// gesorteerd op de gekozen waarde (positie: de beste eerst)
o.lists = {};
angular.forEach(d.lists || {}, function (list, k) {
o.lists[k] = list.slice().sort(function (a, b) { return m === 'position' ? (a.position || 999) - (b.position || 999) : (b[m] || 0) - (a[m] || 0); });
});
},
// klik op een pagina in de uitklap: Verkennen op die pagina (vanuit Gevolgd via het adres)
detailPage: function (url) {
if (this.tab === 'explore') { this.exFilterPage(url); return; }
$location.path('/search-insights/verkennen').search({ page: url, page_op: 'equals' });
},
add: { show: false, text: '', group: '', busy: false },
bulk: { group: '' },
openAdd: function () { this.add = { show: true, text: '', group: '', busy: false }; },
saveAdd: function () {
var self = this;
var kws = self.add.text.split(/\n/).map(function (s) { return s.trim(); }).filter(Boolean);
if (!kws.length) { return; }
self.add.busy = true;
$http.post('/rest/gscinsights/track', { websiteid: h.siteId(), keywords: kws, action: 'add', group: self.add.group }).then(function () {
self.add = { show: false, text: '', group: '', busy: false };
self.load();
}, function () { self.add.busy = false; });
},
selected: function () {
var out = [];
angular.forEach(this.v.groups || [], function (g) { g.keywords.forEach(function (k) { if (k.sel) { out.push(k.query); } }); });
return out;
},
bulkGroup: function () {
var self = this, kws = self.selected();
if (!kws.length) { return; }
$http.post('/rest/gscinsights/group', { websiteid: h.siteId(), keywords: kws, group: self.bulk.group }).then(function () { self.bulk.group = ''; self.load(); });
},
bulkRemove: function () {
var self = this, kws = self.selected();
if (!kws.length) { return; }
$http.post('/rest/gscinsights/track', { websiteid: h.siteId(), keywords: kws, action: 'remove' }).then(function () { self.load(); });
},
// ------------------------------------------------------------------ verkennen
ex: gsciState.explore || { dim: 'query', q: '', q_op: 'contains', page: '', page_op: 'contains', country: '', device: '', brand: '', days: 28, compare: 'prev' },
exQuick: '',
exSort: { key: 'clicks', desc: true },
exLimit: 100,
exploreParams: function () {
var p = {};
angular.forEach(this.ex, function (v, k) { if (v !== '' && v !== null) { p[k] = v; } });
return p;
},
exChanged: function (now) {
var self = this;
gsciState.explore = self.ex;
if (self._exTimer) { $timeout.cancel(self._exTimer); }
self._exTimer = $timeout(function () { self.exLimit = 100; self.open = null; self.load(); }, now ? 0 : 500);
},
exReset: function () {
this.ex = { dim: this.ex.dim, q: '', q_op: 'contains', page: '', page_op: 'contains', country: '', device: '', brand: '', days: 28, compare: 'prev' };
this.exQuick = '';
this.exChanged(true);
},
exDim: function (d) { this.ex.dim = d; this.exQuick = ''; this.exSort = { key: d === 'date' ? 'date' : 'clicks', desc: true }; this.exChanged(true); },
setQuick: function (q) { this.exQuick = this.exQuick === q ? '' : q; this.refresh(); },
viewExplore: function (all) {
var self = this, ex = self.ex, sm = all.parts.summary, d = all.parts.rows;
var v = { longNoCompare: ex.days > 180, chart: null, rows: null };
if (sm) {
v.kpis = self.kpis(sm.totals.now, sm.totals.before || null, undefined).slice(0, 4);
var labels = (sm.daily || []).map(function (r) { return f.day(r.date); });
var series = [{ label: t('gscinsights.explore.this_period'), color: '#2563eb', values: (sm.daily || []).map(function (r) { return r.clicks; }), strong: true }];
if (sm.daily_before) {
series.push({ label: t('gscinsights.explore.prev_period'), color: '#9ca3af', dash: true, values: sm.daily_before.map(function (r) { return r.clicks; }) });
}
v.chart = { labels: labels.length > 12 ? labels.map(function (l, i) { return i % Math.ceil(labels.length / 8) === 0 ? l : ''; }) : labels, titles: labels, series: series };
} else {
v.kpis = ['clicks', 'impressions', 'ctr', 'position'].map(function (k) { return { label: t('gscinsights.kpi.' + k), loading: true }; });
}
if (!d) { return v; }
if (ex.dim === 'newlost') {
v.newLost = [{ k: 'new', rows: d['new'] || [] }, { k: 'lost', rows: d.lost || [] }];
return v;
}
var rows = d.rows || [];
var Q = /^(wie|wat|waar|wanneer|waarom|hoe|welke|welk|kan|kun|mag|moet|is|zijn|heb|how|what|why|when|where|which|who|can|is|does|do)\b/i;
if (self.exQuick === 'chance') { rows = rows.filter(function (r) { return r.position >= 4 && r.position <= 20 && r.impressions >= 100; }); }
if (self.exQuick === 'lowctr') { rows = rows.filter(function (r) { return r.position <= 3 && r.impressions >= 200 && r.ctr < 5; }); }
if (self.exQuick === 'down') { rows = rows.filter(function (r) { return r.change !== null && r.change < 0; }); }
if (self.exQuick === 'questions') { rows = rows.filter(function (r) { return Q.test(r.query || ''); }); }
rows.forEach(function (r) {
if (ex.dim === 'page') { r.label = r.page.replace(/^https?:\/\/[^\/]+/, '') || '/'; }
else if (ex.dim === 'device') { r.label = t('gscinsights.explore.device_name.' + r.device); }
else if (ex.dim === 'country') { r.label = self.countryName(r.cc) || r.country; }
else if (ex.dim === 'date') { r.label = f.day(r.date); }
else { r.label = r.query; }
});
v.rows = rows;
v.total = d.total_rows;
v.ga4 = ex.dim === 'page' && d.ga4;
v.hasRev = d.has_revenue;
return v;
},
countryName: function (cc) {
try { return cc ? new Intl.DisplayNames([$translate.use() || 'nl'], { type: 'region' }).of(cc) : ''; } catch (e) { return cc; }
},
// klik op een pagina in de uitklap: filter Verkennen op die pagina
exFilterPage: function (url) { this.ex.page = url; this.ex.page_op = 'equals'; this.ex.dim = 'query'; this.exChanged(true); },
exExport: function () {
var rows = this.v.rows || [], dim = this.ex.dim;
var cols = [dim, 'clicks', 'impressions', 'ctr', 'position'];
if (this.v.ga4) { cols = cols.concat(['sessions', 'conversions', 'revenue']); }
var esc = function (x) { x = x === null || x === undefined ? '' : String(x); return /[",\n;]/.test(x) ? '"' + x.replace(/"/g, '""') + '"' : x; };
var csv = cols.join(',') + '\n' + rows.map(function (r) { return cols.map(function (k) { return esc(r[k]); }).join(','); }).join('\n');
var a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
a.download = 'search-console-' + dim + '-' + new Date().toISOString().substr(0, 10) + '.csv';
a.click();
}
};
// Verkennen: filters uit het adres (een link vanuit Overzicht of een onderwerp)
if (c.tab === 'explore') {
var qs = $location.search();
if (qs.q !== undefined || qs.page !== undefined) {
c.ex = { dim: 'query', q: qs.q || '', q_op: qs.q_op || 'contains', page: qs.page || '', page_op: qs.page_op || 'contains', country: '', device: '', brand: '', days: 28, compare: 'prev' };
gsciState.explore = c.ex;
}
}
$scope.$on('$destroy', function () { if (c._poll) { $timeout.cancel(c._poll); } });
// de teksten worden in de controller vertaald (instant): pas beginnen als de vertalingen er zijn
$translate.onReady().then(function () { c.init(); });
}]);
// AI Insights: /ai-insights (overzicht), /ai-insights/antwoorden(/:id), /concurrenten, /verkeer.
// Word je genoemd door ChatGPT, Gemini en Perplexity (onze metingen), komen ze (GA4) en wat levert het op.
// Server: rest/data/aiinsights/, regels in rest/include/ai.php. Ontwerp: docs/superpowers/specs/2026-10-27-ai-insights-design.md.
// Bouwblokken: js/ui.js (docs/ui.md). Nog niet gelanceerd: zichtbaar via user.site.show.aiinsights.
// wat bewaard blijft tussen de pagina's: de laatst opgehaalde delen (meteen tonen, op de achtergrond vernieuwen)
var aiState = { site: null, cache: {} };
// AI Insights in tegels, grafiek en "wat nu"; gedeeld met het Dashboard (js/controller/home.js).
app.factory('aiView', ['$translate', 'mtFmt', function ($translate, mtFmt) {
var t = function (k, v) { return $translate.instant(k, v); };
var f = mtFmt;
var BASE = '/ai-insights';
// AI in het bezoek (GA4) => kleur; dezelfde volgorde als in de grafiek
var COLORS = { chatgpt: '#1e3a8a', perplexity: '#3b82f6', gemini: '#93c5fd', other: '#cbd5e1' };
var NAMES = { chatgpt: 'ChatGPT', perplexity: 'Perplexity', gemini: 'Gemini', copilot: 'Copilot', claude: 'Claude', deepseek: 'DeepSeek', meta: 'Meta AI', grok: 'Grok', mistral: 'Mistral', you: 'You.com', phind: 'Phind', poe: 'Poe', openai: 'ChatGPT', google: 'Gemini', other: '' };
var view = {
COLORS: COLORS,
NAMES: NAMES,
name: function (e) { return NAMES[e] || e; },
promptHref: function (id) { return BASE + '/antwoorden/' + encodeURIComponent(id); },
// part kpis: genoemd, als bron, bezoek uit AI, opbrengst of conversies; zonder gegevens vier lege tegels
kpis: function (k) {
var out;
if (!k) {
out = ['mentioned', 'linked', 'visits', 'revenue'].map(function (x) { return { label: t('aiinsights.kpi.' + x), loading: true }; });
} else {
var an = k.answers.now, ab = k.answers.before;
var rate = function (c, key) { return c && c.answers ? 100 * c[key] / c.answers : null; };
var noPrompts = !an.answers;
out = [
{ label: t('aiinsights.kpi.mentioned'), value: noPrompts ? '–' : f.pct(rate(an, 'mentioned'), 0), delta: ab && !noPrompts ? rate(an, 'mentioned') - rate(ab, 'mentioned') : null, unit: 'pt',
note: noPrompts ? t('aiinsights.kpi.no_answers') : t('aiinsights.kpi.mentioned_note', { n: an.mentioned, total: an.answers }) },
{ label: t('aiinsights.kpi.linked'), value: noPrompts ? '–' : f.pct(rate(an, 'linked'), 0), delta: ab && !noPrompts ? rate(an, 'linked') - rate(ab, 'linked') : null, unit: 'pt',
note: noPrompts ? '' : t('aiinsights.kpi.linked_note', { n: an.linked, total: an.answers }) }
];
var tr = k.traffic;
if (!tr) {
out.push({ label: t('aiinsights.kpi.visits'), value: '–', note: t('aiinsights.kpi.no_ga4') }, { label: t('aiinsights.kpi.revenue'), value: '–', note: t('aiinsights.kpi.no_ga4') });
} else {
var ch = function (x, y) { return y ? 100 * (x - y) / y : null; };
out.push({ label: t('aiinsights.kpi.visits'), value: f.num(tr.now.sessions), delta: ch(tr.now.sessions, tr.before.sessions),
note: tr.all_sessions ? t('aiinsights.kpi.share', { pct: f.pct(100 * tr.now.sessions / tr.all_sessions) }) : '' });
out.push(tr.now.revenue >= 10 || tr.before.revenue >= 10
? { label: t('aiinsights.kpi.revenue'), value: f.euro(tr.now.revenue), delta: ch(tr.now.revenue, tr.before.revenue), note: t('aiinsights.kpi.conversions_note', { n: f.num(tr.now.conversions) }) }
: { label: t('aiinsights.kpi.conversions'), value: f.num(tr.now.conversions), delta: ch(tr.now.conversions, tr.before.conversions), note: t('aiinsights.kpi.from_ai') });
}
}
return out;
},
// part chart: AI-bezoek per maand per AI (null zonder GA4)
chart: function (c) {
var self = view, months = c.months || [];
return c.ga4 ? {
labels: months.map(function (m) { return f.month(m.month); }),
titles: months.map(function (m) { return f.monthLong(m.month) + (m.partial ? ' · ' + t('aiinsights.chart.partial') : ''); }),
series: ['other', 'gemini', 'perplexity', 'chatgpt'].map(function (e) {
return { label: e === 'other' ? t('aiinsights.engines.other') : self.name(e), color: COLORS[e], values: months.map(function (m) { return m[e]; }) };
}),
faded: months.map(function (m) { return m.partial; }),
extra: months.map(function (m) { return m.revenue ? [{ label: t('aiinsights.kpi.revenue'), value: f.euro(m.revenue), color: '#047857' }] : []; })
} : null;
},
// part actions: wat nu, met de link erbij
actions: function (list) {
var self = view;
return list.map(function (x) {
var o = { kind: x.kind };
if (x.kind === 'not_mentioned') { o.text = t('aiinsights.action.not_mentioned', { q: x.q, others: x.others.join(', ') }); o.href = self.promptHref(x.prompt_id); o.go = t('aiinsights.goto.prompt'); }
if (x.kind === 'lost') { o.text = t('aiinsights.action.lost', { q: x.q, engine: self.name(x.engine) }); o.href = self.promptHref(x.prompt_id); o.go = t('aiinsights.goto.prompt'); }
if (x.kind === 'no_conversion') { o.text = t('aiinsights.action.no_conversion', { page: x.page, n: f.num(x.sessions) }); o.href = BASE + '/verkeer'; o.go = t('aiinsights.tab.traffic'); }
if (x.kind === 'idea') { o.text = t('aiinsights.action.idea', { q: x.q, n: f.num(x.impressions) }); o.href = BASE + '/antwoorden?add=' + encodeURIComponent(x.q); o.go = t('aiinsights.goto.add'); }
return o;
});
}
};
return view;
}]);
app.controller('aiinsights', ['$scope', '$rootScope', '$location', '$routeParams', '$http', '$translate', '$timeout', '$sce', 'h', 'mtFmt', 'aiView', function ($scope, $rootScope, $location, $routeParams, $http, $translate, $timeout, $sce, h, mtFmt, aiView) {
// in een rapport (newlayout/aiinsights/report.html): de maand van het rapport, alleen lezen
var report = $scope.custominit && $scope.custominit.ai ? $scope.custominit : null;
if (!report && (!$rootScope.user || !$rootScope.user.site || !$rootScope.user.site.show || !$rootScope.user.site.show.aiinsights)) {
$location.path('/');
return;
}
var TABS = { '': 'overview', antwoorden: 'answers', concurrenten: 'competitors', verkeer: 'traffic' };
var BASE = '/ai-insights';
var ENGINES = ['openai', 'google', 'perplexity'];
// AI in het bezoek (GA4) => kleur; dezelfde volgorde als in de grafiek
var COLORS = aiView.COLORS;
var TRAFFIC_OF = { openai: 'chatgpt', google: 'gemini', perplexity: 'perplexity' };
var NAMES = aiView.NAMES;
// een week in de rij van 12: kleur per status
var WEEK = { linked: 'bg-green-600', mentioned: 'bg-blue-400', none: 'bg-gray-200', nosearch: 'bg-amber-200' };
// het pakket in metingen per ronde, één meting = één prompt bij één AI (zelfde lijsten als ai::PLAN_FREQ en
// ai::ANSWER_STEPS in rest/include/ai.php)
var FREQS = ['monthly', 'biweekly', 'weekly'];
var ANSWER_STEPS = [30, 75, 150, 300, 600];
// de AI's van de vorige nieuwe prompt, als beginkeuze voor de volgende (per browser)
var LAST_ENGINES = 'mt.aiinsights.engines';
var t = function (k, v) { return $translate.instant(k, v); };
var f = mtFmt;
if (aiState.site !== h.siteId()) {
aiState = { site: h.siteId(), cache: {} };
}
var esc = function (s) { return String(s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); };
var a = $scope.a = {
f: f,
tab: report ? report.urlview : (TABS[$routeParams.tab || ''] || 'overview'),
promptId: report ? null : ($routeParams.id || null),
report: !!report,
status: aiState.cache.status || null,
data: null,
loading: true,
error: null,
v: {},
weekCls: WEEK,
freqs: FREQS,
answerSteps: ANSWER_STEPS,
init: function () {
var self = this;
self.tabs = [
{ key: 'overview', label: t('aiinsights.tab.overview'), href: BASE },
{ key: 'answers', label: t('aiinsights.tab.answers'), href: BASE + '/antwoorden' },
{ key: 'competitors', label: t('aiinsights.tab.competitors'), href: BASE + '/concurrenten' },
{ key: 'traffic', label: t('aiinsights.tab.traffic'), href: BASE + '/verkeer' }
];
self.get('status', {}, function (s) { self.status = s; }).then(function (s) {
self.status = aiState.cache.status = s;
self.load();
// een idee uit "Wat nu" (?add=vraag): meteen het formulier open
if ($location.search().add && self.tab === 'answers' && s.can_write) {
self.openEdit(null, $location.search().add);
$location.search('add', null);
}
}, function (res) {
self.loading = false;
self.error = { error: res.status === 403 ? 'no access' : 'server' };
});
},
// GET /rest/aiinsights/; het vorige antwoord voor hetzelfde adres staat er meteen
get: function (what, params, onCached) {
var key = what + '|' + JSON.stringify(params);
if (aiState.cache[key] && onCached) { onCached(aiState.cache[key]); }
return $http.get('/rest/aiinsights/' + what, { params: angular.extend({ websiteid: h.siteId() }, params) }).then(function (res) {
aiState.cache[key] = res.data;
return res.data;
});
},
// elke pagina in delen, tegelijk opgevraagd; elke kaart vult zich zodra zijn deel er is
PARTS: { overview: ['kpis', 'engines', 'chart', 'actions'], answers: [null], prompt: [null], competitors: [null], traffic: [null] },
load: function () {
var self = this;
var what = self.tab === 'answers' && self.promptId ? 'prompt' : self.tab;
var params = self.promptId ? { id: self.promptId } : {};
if (report) {
var dates = $rootScope.reportdates || {};
angular.extend(params, { start: dates.start, end: dates.end });
}
var key = what + '|' + JSON.stringify(params);
if (!self.data || self.data.key !== key) { self.data = { key: key, parts: {} }; }
self.loading = false;
self.error = null;
self.refresh();
self.PARTS[what].forEach(function (part) {
var p = part ? angular.extend({ part: part }, params) : params;
var show = function (d) {
if (self.data.key !== key) { return; }
self.data.parts[part || 'all'] = d;
self.refresh();
};
self.get(what, p, show).then(show, function (res) {
self.error = { error: res.status === 403 ? 'no access' : (res.status === 404 ? 'not found' : 'server'), message: (res.data && res.data.error) || '' };
});
});
},
// alles wat ng-repeat toont, één keer per nieuwe data (geen functies in ng-repeat: eindeloze digest)
refresh: function () {
var self = this, d = self.data;
if (self.tab === 'overview') { self.v = self.viewOverview(d.parts); }
if (self.tab === 'answers' && !self.promptId) { self.v = d.parts.all ? self.viewAnswers(d.parts.all) : { loading: true }; }
if (self.tab === 'answers' && self.promptId) { self.v = d.parts.all ? self.viewPrompt(d.parts.all) : { loading: true }; }
if (self.tab === 'competitors') { self.v = d.parts.all ? self.viewCompetitors(d.parts.all) : { loading: true }; }
if (self.tab === 'traffic') { self.v = d.parts.all ? self.viewTraffic(d.parts.all) : { loading: true }; }
},
// ------------------------------------------------------------------ gedeeld
name: function (e) { return aiView.name(e); },
pct: function (n, of) { return of ? 100 * n / of : null; },
ofText: function (n, of) { return t('aiinsights.of', { n: f.num(n), total: f.num(of) }); },
promptHref: function (id) { return aiView.promptHref(id); },
// ------------------------------------------------------------------ overzicht
viewOverview: function (p) {
var self = this, v = {};
v.kpis = aiView.kpis(p.kpis);
if (p.engines) {
v.engines = p.engines.engines.map(function (e) {
var c = e.counts, tr = e.traffic;
return {
name: e.engine === 'other' ? (e.names.length ? e.names.map(function (n) { return self.name(n); }).join(', ') : t('aiinsights.engines.other')) : e.name,
sub: e.engine === 'other' ? t('aiinsights.engines.other_note') : (c.answers ? t('aiinsights.engines.measured', { n: c.answers }) : t('aiinsights.engines.not_measured')),
mentioned: c && c.answers ? self.pct(c.mentioned, c.answers) : null,
mentionedText: c && c.answers ? self.ofText(c.mentioned, c.answers) : '–',
linkedText: c && c.answers ? self.ofText(c.linked, c.answers) : '–',
sessions: tr ? f.num(tr.sessions) : '–', conversions: tr ? f.num(tr.conversions) : '–', revenue: tr && tr.revenue ? f.euro(tr.revenue) : '–'
};
});
}
if (p.chart) {
v.noGa4 = !p.chart.ga4;
v.chart = aiView.chart(p.chart);
}
if (p.actions) { v.actions = aiView.actions(p.actions.actions); }
return v;
},
// ------------------------------------------------------------------ AI-antwoorden
viewAnswers: function (d) {
var self = this;
return {
weeks: d.weeks.map(function (w) { return f.day(w); }),
count: d.prompts.length,
plan: d.plan,
used: d.used,
planPct: Math.min(100, 100 * d.used / d.plan.answers),
canPlan: d.can_plan || d.can_buy,
rows: d.prompts.map(function (p) {
return angular.extend(p, {
href: self.promptHref(p.id),
cellList: ENGINES.map(function (e) { var c = p.cells[e]; return { status: c ? c.status : '', pos: c && c.pos ? c.pos : '', off: !c }; }),
weekList: p.weeks.map(function (s, i) { return { cls: s ? WEEK[s] : 'bg-gray-50 border border-gray-200', title: f.day(d.weeks[i]) + ': ' + (s ? t('aiinsights.week.' + s) : t('aiinsights.week.none_measured')) }; }),
whoText: p.who.join(', '),
errorText: p.paused ? '' : (p.errors.length ? t('aiinsights.answers.error', { engines: p.errors.map(function (e) { return self.name(e); }).join(', ') }) : '')
});
}),
aio: d.aio,
aioRows: d.aio.rows.map(function (r) { return angular.extend(r, { status: r.pos ? 'linked' : 'none', whoText: r.who.join(', ') }); })
};
},
// prompt toevoegen of wijzigen
edit: { show: false },
openEdit: function (p, q) {
var last = null;
try { last = JSON.parse(window.localStorage.getItem(LAST_ENGINES)); } catch (e) { last = null; }
var on = p ? p.providers : (angular.isArray(last) && last.length ? last : ENGINES);
this.edit = {
show: true, busy: false, error: '', id: p ? p.id : null, q: p ? p.q : (q || ''),
providers: { openai: on.indexOf('openai') > -1, google: on.indexOf('google') > -1, perplexity: on.indexOf('perplexity') > -1 },
// wat deze prompt nu al van het pakket gebruikt (bij wijzigen telt die ruimte als vrij)
was: p ? p.providers.length : 0,
brands: p ? p.brands.join(', ') : ''
};
},
// metingen van de prompt in het formulier, en wat er daarna nog vrij is (ai::weight: geen keuze = alle AI's)
editWeight: function () {
var e = this.edit;
return ENGINES.filter(function (x) { return e.providers[x]; }).length || ENGINES.length;
},
editLeft: function () {
return this.v.plan ? this.v.plan.answers - this.v.used + this.edit.was - this.editWeight() : null;
},
saveEdit: function () {
var self = this, e = self.edit;
e.busy = true;
e.error = '';
if (!e.id) {
try { window.localStorage.setItem(LAST_ENGINES, JSON.stringify(ENGINES.filter(function (x) { return e.providers[x]; }))); } catch (err) { /* geen opslag: niet erg */ }
}
$http.post('/rest/aiinsights/prompt_save', {
websiteid: h.siteId(), id: e.id || '', q: e.q,
providers: ENGINES.filter(function (x) { return e.providers[x]; }),
brands: e.brands.split(',').map(function (b) { return b.trim(); }).filter(Boolean)
}).then(function () {
self.edit = { show: false };
aiState.cache = { status: aiState.cache.status };
self.load();
}, function (res) {
e.busy = false;
var err = (res.data && res.data.error) || 'server';
e.error = t('aiinsights.edit.error.' + (['limit', 'exists', 'empty'].indexOf(err) > -1 ? err : 'server'), res.data || {});
});
},
removePrompt: function () {
var self = this;
if (!self.edit.id || !window.confirm(t('aiinsights.edit.remove_confirm'))) { return; }
$http.post('/rest/aiinsights/prompt_delete', { websiteid: h.siteId(), id: self.edit.id }).then(function () {
self.edit = { show: false };
aiState.cache = { status: aiState.cache.status };
if (self.promptId) { $location.path(BASE + '/antwoorden'); } else { self.load(); }
});
},
// ------------------------------------------------------------------ pakket (zoals de Rank tracker)
// white label-admins zetten het (aiinsights/plan), de eigenaar van een directe site koopt het (aiinsights/buy);
// de prijs komt van de server
settings: null,
openPlan: function () {
var self = this;
$http.get('/rest/aiinsights/plan', { params: { websiteid: h.siteId() } }).then(function (res) {
var p = res.data.plan;
self.settings = {
own: res.data.own, buy: !!res.data.buy, now: res.data.now, price: res.data.price,
freq: Math.max(0, FREQS.indexOf(p.freq)),
answers: Math.max(0, ANSWER_STEPS.filter(function (n) { return n <= p.answers; }).length - 1)
};
});
},
planOf: function () { return { freq: FREQS[+this.settings.freq], answers: ANSWER_STEPS[+this.settings.answers] }; },
planChanged: function () {
var self = this;
if (self._priceTimer) { $timeout.cancel(self._priceTimer); }
self._priceTimer = $timeout(function () {
$http.get('/rest/aiinsights/plan', { params: angular.extend({ websiteid: h.siteId() }, self.planOf()) }).then(function (res) {
if (self.settings) { self.settings.price = res.data.price; self.settings.now = res.data.now; }
});
}, 250);
},
planSaved: function () {
this.settings = null;
aiState.cache = {};
this.init();
},
savePlan: function (followDefault) {
var self = this;
$http.post('/rest/aiinsights/plan', { websiteid: h.siteId(), plan: followDefault ? null : self.planOf() }).then(function () { self.planSaved(); });
},
// directe klant: naar Mollie als er nu iets te betalen is, anders staat het pakket meteen aan
buyPlan: function () {
var self = this;
self.settings.busy = true;
$http.post('/rest/aiinsights/buy', { websiteid: h.siteId(), plan: self.planOf() }).then(function (res) {
if (res.data.checkout) { window.location.href = res.data.checkout; return; }
self.planSaved();
}, function () { self.settings.busy = false; });
},
// ------------------------------------------------------------------ één prompt
viewPrompt: function (d) {
var self = this;
var withAnswer = d.engines.filter(function (e) { return e.answer; });
var mentioned = withAnswer.filter(function (e) { return e.answer.status === 'linked' || e.answer.status === 'mentioned'; });
var best = null;
withAnswer.forEach(function (e) { if (e.answer.pos && (!best || e.answer.pos < best.pos)) { best = { pos: e.answer.pos, name: e.name }; } });
var visits = d.pages.reduce(function (s, p) { return s + (p.traffic ? p.traffic.sessions : 0); }, 0);
var terms = ((self.status && self.status.brand) || []).filter(function (x) { return x.length >= 3; });
// het antwoord als leesbare tekst: eerst escapen, dan de markdown van de AI's (vet, kopjes, opsommingen,
// links als alleen de tekst), dan je merknaam markeren
var mark = function (text) {
var h = esc(text || '')
.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '$1')
.replace(/^#{1,6}\s*(.+)$/gm, '$1 ')
.replace(/\*\*([^*\n]+)\*\*/g, '$1 ')
.replace(/^\s*[*-]\s+/gm, '• ');
terms.forEach(function (term) {
h = h.replace(new RegExp('(^|[^\\p{L}\\p{N}])(' + esc(term).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')(?![\\p{L}\\p{N}])', 'giu'), '$1$2 ');
});
return $sce.trustAsHtml(h.replace(/\n/g, ' '));
};
return {
prompt: d.prompt,
kpis: [
{ label: t('aiinsights.prompt.kpi_mentioned'), value: t('aiinsights.prompt.n_of_ai', { n: mentioned.length, total: withAnswer.length }), note: mentioned.map(function (e) { return e.name; }).join(', ') },
{ label: t('aiinsights.prompt.kpi_best'), value: best ? String(best.pos) : '–', note: best ? best.name : t('aiinsights.prompt.not_linked') },
{ label: t('aiinsights.prompt.kpi_visits'), value: d.pages.length ? f.num(visits) : '–', note: d.pages.length ? t('aiinsights.prompt.kpi_visits_note') : t('aiinsights.prompt.no_page') }
],
weeks: d.weeks.map(function (w) { return f.day(w); }),
grid: d.engines.map(function (e) {
return { name: e.name, cells: e.weeks.map(function (c, i) { return { cls: c ? WEEK[c.status] : 'bg-gray-50 border border-gray-200', label: c && c.pos ? c.pos : '', title: f.day(d.weeks[i]) + ': ' + (c ? t('aiinsights.week.' + c.status) : t('aiinsights.week.none_measured')) }; }) };
}),
answers: d.engines.map(function (e) {
var x = e.answer;
return {
name: e.name, has: !!x,
status: x ? x.status : '', pos: x && x.pos ? x.pos : '',
meta: x ? [x.model, f.day(x.day)].filter(Boolean).join(' · ') : '',
html: x ? mark(x.text) : '',
sources: x ? x.sources : [],
noSources: x && !x.sources.length,
brands: x ? x.others.filter(function (o) { return o.mentioned; }).map(function (o) { return o.name; }).join(', ') : ''
};
}),
pages: d.pages.map(function (p) {
return { page: p.page, cited: p.cited.map(function (c) { return self.name(c.engine) + (c.pos ? ' (' + c.pos + ')' : ''); }).join(', '),
sessions: p.traffic ? f.num(p.traffic.sessions) : '–', conversions: p.traffic ? f.num(p.traffic.conversions) : '–' };
})
};
},
// ------------------------------------------------------------------ concurrenten
viewCompetitors: function (d) {
var self = this, total = d.answers;
var row = function (o, own) {
var eng = o.engines || {};
return {
name: o.name, own: own, followed: !!o.followed,
appears: own ? o.mentioned : o.answers,
share: total ? 100 * (own ? o.mentioned : o.answers) / total : 0,
shareText: self.ofText(own ? o.mentioned : o.answers, total),
linkedText: self.ofText(o.linked, total),
perEngine: ENGINES.map(function (e) { return eng[e] || 0; }).join(' · '),
diff: o.before === null || o.before === undefined ? null : (own ? o.mentioned : o.answers) - o.before
};
};
var rows = [row(d.own, true)].concat(d.others.map(function (o) { return row(o, false); }));
rows.sort(function (x, y) { return y.appears - x.appears || (x.own ? -1 : 1); });
return {
total: total,
rows: rows,
pages: d.pages,
followed: d.followed,
canWrite: d.can_write
};
},
toggleFollow: function (r) {
var self = this, list = (self.v.followed || []).slice();
var i = list.indexOf(r.name);
if (i > -1) { list.splice(i, 1); } else if (list.length < 6) { list.push(r.name); } else { return; }
r.followed = i === -1;
self.v.followed = list;
$http.post('/rest/aiinsights/competitors_save', { websiteid: h.siteId(), competitors: list }).then(function (res) {
self.v.followed = res.data.competitors;
if (self.status) { self.status.competitors = res.data.competitors; }
});
},
// ------------------------------------------------------------------ AI-verkeer
trafficFilter: '',
setTrafficFilter: function (e) { this.trafficFilter = this.trafficFilter === e ? '' : e; this.refresh(); },
viewTraffic: function (d) {
var self = this;
if (!d.ga4) { return { noGa4: true }; }
var ch = function (x, y) { return y ? 100 * (x - y) / y : null; };
var n = d.total.now, b = d.total.before;
var main = ['chatgpt', 'perplexity', 'gemini'];
var flt = self.trafficFilter;
// het bezoek van één pagina uit de gekozen AI (of alle AI's); daarop sorteren, de 50 grootste tonen
var other = function (p) { return Object.keys(p.engines).filter(function (x) { return main.indexOf(x) === -1; }).reduce(function (t, x) { return t + p.engines[x]; }, 0); };
var from = function (p) { return !flt ? p.sessions : (flt === 'other' ? other(p) : (p.engines[flt] || 0)); };
var pages = d.pages.filter(function (p) { return from(p) > 0; }).sort(function (a, b) { return from(b) - from(a); }).slice(0, 50).map(function (p) {
var bar = main.concat(['other']).map(function (e) {
var s = e === 'other' ? other(p) : (p.engines[e] || 0);
return { color: COLORS[e], pct: p.sessions ? 100 * s / p.sessions : 0, title: (e === 'other' ? t('aiinsights.engines.other') : self.name(e)) + ': ' + f.num(s) };
});
return {
page: p.page === '(not set)' ? t('aiinsights.traffic.not_set') : p.page,
// met een filter: het bezoek uit die AI (de balk laat de verdeling over alle AI's zien)
sessions: f.num(from(p)), delta: flt ? null : ch(p.sessions, p.before), bar: bar,
engaged: f.pct(p.sessions ? 100 * p.engaged / p.sessions : null, 0),
conversions: f.num(p.conversions), revenue: p.revenue ? f.euro(p.revenue) : '–',
cited: p.cited ? self.ofText(p.cited, d.prompts) : '–'
};
});
return {
kpis: [
{ label: t('aiinsights.kpi.visits'), value: f.num(n.sessions), delta: ch(n.sessions, b.sessions) },
{ label: t('aiinsights.traffic.share'), value: d.total.all_sessions ? f.pct(100 * n.sessions / d.total.all_sessions) : '–' },
{ label: t('aiinsights.traffic.engaged'), value: f.pct(n.sessions ? 100 * n.engaged / n.sessions : null, 0) },
{ label: t('aiinsights.kpi.conversions'), value: f.num(n.conversions), delta: ch(n.conversions, b.conversions) },
{ label: t('aiinsights.kpi.revenue'), value: n.revenue ? f.euro(n.revenue) : '–', delta: n.revenue ? ch(n.revenue, b.revenue) : null }
],
filters: [{ key: '', label: t('aiinsights.traffic.all') }].concat(main.map(function (e) { return { key: e, label: self.name(e) }; })).concat([{ key: 'other', label: t('aiinsights.engines.other') }]),
legend: main.concat(['other']).map(function (e) { return { color: COLORS[e], label: e === 'other' ? t('aiinsights.engines.other') : self.name(e) }; }),
pages: pages
};
}
};
// terug van Mollie: de webhook verwerkt de betaling, meestal binnen een paar seconden. Tot 30 seconden de status
// van de bestelling opvragen (serp/order: de bestellingen van beide add-ons) en zeggen wat er echt gebeurde.
if (!report && $location.search().betaald) {
var orderId = $location.search().betaald, tries = 0;
$location.search('betaald', null);
a.paidMessage = 'serp.paid_wait';
var check = function () {
$http.get('/rest/serp/order', { params: { websiteid: h.siteId(), id: orderId } }).then(function (res) {
var st = res.data.status;
if (st === 'paid') {
a.paidMessage = 'aiinsights.plan.paid_ok';
aiState.cache = {};
a.init();
} else if (st && st !== 'open') {
a.paidMessage = 'aiinsights.plan.paid_failed';
} else if (++tries < 15) {
return $timeout(check, 2000);
}
$timeout(function () { a.paidMessage = null; }, 8000);
});
};
check();
}
$translate.onReady().then(function () { a.init(); });
}]);
app.controller('contentcheck', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'h', 'mtFmt', function ($scope, $http, $timeout, $routeParams, $location, h, mtFmt) {
// Contentcheck (2026-10-27). Verstopt: alleen eigen admins. Teksten nog vast in het Nederlands; vertalingen bij de
// lancering. Arjen: niets over hoe het werkt op de pagina of in deze code (die is openbaar).
var BASE = '/content-intelligence/check';
var f = mtFmt;
// factoren: naam, uitleg, hoe de waarde te tonen (null: alleen de balk, geen waarde)
var FACTORS = {
query: ['Aansluiting op de zoekvraag', 'Hoe goed je tekst aansluit op waar mensen naar zoeken.', null],
like_top: ['Lijkt op je concurrenten', 'Hoe dicht je artikel bij concurrenten ligt die beter ranken.', null],
novelty: ['Eigen zijpaden', 'Hoeveel je artikel afdwaalt van wat concurrenten die beter ranken behandelen.', null],
coherence: ['Samenhang', 'Hoe goed je tekst bij het onderwerp blijft.', null],
answer_first: ['Direct antwoord bovenaan', 'Of je meteen antwoord geeft op de zoekvraag.', null],
question_heads: ['Vraagkoppen', 'Welk deel van je koppen een vraag is.', function (v) { return Math.round(100 * v) + '%'; }],
address: ['Aanspreekvorm', 'Hoe vaak je de lezer aanspreekt (je, jij, jouw).', function (v) { return Math.round(100 * v) + '%'; }],
age: ['Actualiteit', 'Hoe lang geleden de pagina is gepubliceerd of bijgewerkt.', function (v) { return Math.round(v) + ' dagen'; }],
words: ['Lengte', 'Woorden in de hoofdtekst.', function (v) { return f.num(Math.round(v)); }]
};
var STEPS = { queued: 'In de rij', fetch: 'Pagina\'s ophalen', done: 'Advies schrijven' };
var ERRORS = {
no_serp: 'Voor dit zoekwoord zijn nog geen zoekresultaten bekend. Voeg het toe aan de Rank tracker; na de eerste meting kan de check.',
too_few_articles: 'Te weinig vergelijkbare artikelen van concurrenten. Voor dit zoekwoord toont Google vooral andere soorten pagina\'s.',
own_unreadable: 'Jouw pagina was niet te lezen.',
bad_url: 'De pagina moet van deze site zijn.',
seoserver: 'De check kon niet starten. Probeer het zo nog eens.',
timeout: 'De analyse duurde te lang en is gestopt.',
crash: 'De analyse liep vast.'
};
var c = $scope.cc = {
id: $routeParams.id || null,
f: f,
q: $location.search().q || '',
url: $location.search().url || '',
filter: '',
loading: true,
init: function () {
var self = this;
if (self.id) { self.poll(); return; }
$http.get('/rest/contentcheck/options', { params: { websiteid: h.siteId() } }).then(function (res) {
self.loading = false;
self.options = res.data.options;
self.gsc = res.data.gsc;
self.checks = res.data.checks;
}, function (res) {
self.loading = false;
self.denied = res.status === 403;
});
},
filtered: function () {
var t = this.filter.toLowerCase();
return (this.options || []).filter(function (o) { return !t || o.q.indexOf(t) > -1 || o.url.toLowerCase().indexOf(t) > -1; }).slice(0, 60);
},
path: function (u) { return String(u || '').replace(/^https?:\/\/[^\/]+/, '') || '/'; },
pick: function (o) { this.q = o.q; this.url = o.url; this.start(); },
start: function () {
var self = this;
self.busy = true;
self.error = '';
$http.post('/rest/contentcheck/start', { websiteid: h.siteId(), q: self.q, url: self.url }).then(function (res) {
$location.path(BASE + '/' + res.data.id).search({});
}, function (res) {
self.busy = false;
self.error = ERRORS[(res.data && res.data.error) || 'seoserver'] || ERRORS.seoserver;
});
},
// ---------------------------------------------------------------- één check
poll: function () {
var self = this;
$http.get('/rest/contentcheck/get', { params: { id: self.id } }).then(function (res) {
self.loading = false;
self.check = res.data;
if (res.data.state === 'running') {
self.stepLabel = STEPS[res.data.step] || 'Analyseren';
self._t = $timeout(function () { self.poll(); }, 3000);
} else if (res.data.state === 'done') {
self.v = self.view(res.data.result);
} else {
self.errorText = ERRORS[(res.data.result && res.data.result.error) || 'crash'] || ERRORS.crash;
self.v = null;
}
}, function (res) {
self.loading = false;
self.denied = res.status === 403;
});
},
view: function (r) {
var missing = r.missing || [];
return {
r: r, missing: missing, topics: r.topics || [], extra: r.extra || [],
nee: missing.filter(function (t) { return t.me === 'nee'; }).length,
half: missing.filter(function (t) { return t.me === 'half'; }).length,
factors: (r.factors || []).filter(function (x) { return FACTORS[x.key]; }).map(function (x) {
var d = x.pct - x.top_pct, fmt = FACTORS[x.key][2];
return angular.extend(x, { name: FACTORS[x.key][0], note: FACTORS[x.key][1], meText: fmt && x.me !== undefined && x.me !== null ? fmt(x.me) : null,
topText: fmt && x.top !== undefined && x.top !== null ? fmt(x.top) : null, state: d >= -10 ? 'ok' : (d >= -25 ? 'part' : 'miss') });
}),
summary: r.summary || (missing.length
? 'Concurrenten die beter ranken behandelen ' + missing.length + ' onderwerp' + (missing.length > 1 ? 'en' : '') + ' die jouw artikel overslaat of maar kort noemt. Begin met "' + missing[0].name + '".'
: 'Je artikel behandelt de onderwerpen die concurrenten die beter ranken ook behandelen. Kijk naar de punten hieronder.')
};
},
strengthLabel: function (s) {
return { almost_all: 'Bijna alle concurrenten die beter ranken behandelen dit', most: 'De meeste concurrenten die beter ranken behandelen dit',
half: 'Ongeveer de helft van de concurrenten die beter ranken behandelt dit', some: 'Een deel van de concurrenten die beter ranken behandelt dit' }[s];
},
stateLabel: function (s) { return { ok: 'goed', part: 'kan beter', miss: 'aandacht' }[s]; },
meLabel: function (m) { return { ja: 'behandel je', half: 'te kort', nee: 'ontbreekt' }[m]; }
};
$scope.$on('$destroy', function () { if (c._t) { $timeout.cancel(c._t); } });
c.init();
}]);
// Core Web Vitals (CrUX): /crux (Overzicht), /crux/bezoekers (Snelheid en bezoekers), /crux/diagnose (Diagnose).
// Echte Chrome-gebruikers uit het Chrome UX Report, op site-niveau: jij naast je concurrenten (uit de Rank tracker),
// per week, mobiel / desktop / alle apparaten. Server: rest/data/crux/, regels in rest/include/crux.php (ook alle
// zinnen: die komen als soort + getallen, hier alleen vertaald). Ontwerp: docs/superpowers/specs/2026-10-28-crux-design.md
// (v2). Bouwblokken: js/ui.js (docs/ui.md). Nog niet gelanceerd: zichtbaar via user.site.show.crux (rol 1-2 eigen reseller).
// wat bewaard blijft tussen de tabs: de laatst opgehaalde delen (meteen tonen, op de achtergrond vernieuwen)
var cruxState = { site: null, cache: {} };
app.controller('crux', ['$scope', '$rootScope', '$location', '$routeParams', '$http', '$translate', 'h', 'mtFmt', 'mtCwv', 'mtChart', function ($scope, $rootScope, $location, $routeParams, $http, $translate, h, mtFmt, mtCwv, mtChart) {
if (!$rootScope.user || !$rootScope.user.site || !$rootScope.user.site.show || !$rootScope.user.site.show.crux) {
$location.path('/');
return;
}
var TABS = { '': 'overview', bezoekers: 'visitors', diagnose: 'diagnose' };
var PART = { overview: 'get', visitors: 'visitors', diagnose: 'diagnose' };
var BASE = '/crux';
var FF_KEY = 'mt.crux.ff';
var FFS = ['PHONE', 'DESKTOP', 'ALL'];
var CWV = ['lcp', 'inp', 'cls'];
// vaste kleur per concurrent (zijn plek in de lijst van de Rank tracker, nooit zijn rang); jij altijd blauw.
// Gecontroleerd met de dataviz-validator (kleurenblind-veilig naast elkaar); de tekst staat ernaast (legenda, eind).
var COMP_COLORS = ['#eb6834', '#1baf7a', '#eda100', '#e87ba4', '#008300', '#6250d6'];
// de vier LCP-stappen (zoals het ontwerp)
var PARTS = ['ttfb', 'delay', 'duration', 'render'];
var PART_COLORS = { ttfb: '#0f766e', delay: '#7c3aed', duration: '#d97706', render: '#64748b' };
// navigatietypes: kleur en tekstkleur in het blokje
var NAV = [['navigate', '#cbd5e1', '#1f2937'], ['navigate_cache', '#93c5fd', '#1e3a8a'], ['back_forward_cache', '#0f766e', '#ffffff'],
['prerender', '#7c3aed', '#ffffff'], ['back_forward', '#f59e0b', '#1f2937'], ['reload', '#94a3b8', '#1f2937'], ['restore', '#e5e7eb', '#1f2937']];
var VISIT = ['clicks', 'sessions', 'bounce', 'pps'];
var t = function (k, v) { return $translate.instant(k, v); };
var f = mtFmt;
if (cruxState.site !== h.siteId()) {
cruxState = { site: h.siteId(), cache: {} };
}
var ff = 'PHONE';
try { ff = window.localStorage.getItem(FF_KEY) || 'PHONE'; } catch (e) { ff = 'PHONE'; }
if (FFS.indexOf(ff) < 0) { ff = 'PHONE'; }
var none = function (v) { return v === null || v === undefined || isNaN(v); };
// een grens kort: 2,5 s, 200 ms, 0,1
var limitText = function (m, v) {
if (m === 'lcp' || m === 'fcp') { return f.num(v / 1000, v % 1000 ? 1 : 0) + ' s'; }
if (m === 'cls') { return f.num(v, (String(v).split('.')[1] || '').length); }
return f.num(v) + ' ms';
};
var sec1 = function (v) { return f.num(v / 1000, 1) + ' s'; };
var c = $scope.c = {
f: f,
cwv: mtCwv,
tab: TABS[$routeParams.tab || ''] || 'overview',
ff: ff,
ffs: FFS,
tabs: [],
d: {},
v: {},
error: null,
saving: false,
mode: 'group',
speed: 'lcp',
visit: 'clicks',
cwvMetrics: CWV,
visitMetrics: VISIT,
coredash: 'https://www.coredash.app',
init: function () {
this.tabs = [
{ key: 'overview', label: t('crux.tab.overview'), href: BASE },
{ key: 'visitors', label: t('crux.tab.visitors'), href: BASE + '/bezoekers' },
{ key: 'diagnose', label: t('crux.tab.diagnose'), href: BASE + '/diagnose' }
];
this.load();
},
// GET /rest/crux/; het vorige antwoord voor hetzelfde adres staat er meteen
load: function () {
var self = this, what = PART[self.tab], key = what + '|' + self.ff;
self.error = null;
self.d = {};
if (cruxState.cache[key]) { self.d.head = cruxState.cache[key]; }
self.refresh();
$http.get('/rest/crux/' + what, { params: { websiteid: h.siteId(), ff: self.ff } }).then(function (res) {
cruxState.cache[key] = res.data;
if (key === PART[self.tab] + '|' + self.ff) { self.d.head = res.data; self.refresh(); }
}, function (res) {
self.error = res.status === 403 ? 'no_access' : 'server';
});
},
setFf: function (v) {
if (v === this.ff) { return; }
this.ff = v;
try { window.localStorage.setItem(FF_KEY, v); } catch (e) { /* privévenster */ }
this.load();
},
setEnabled: function (on) {
var self = this;
self.saving = true;
$http.post('/rest/crux/set', { websiteid: h.siteId(), enabled: on }).then(function () {
self.saving = false;
cruxState.cache = {};
self.load();
}, function () {
self.saving = false;
self.error = 'server';
});
},
// een voorgestelde concurrent toevoegen aan de Rank tracker (de hele lijst), dan meteen zijn CrUX-gegevens halen
addCompetitor: function (domain) {
var self = this, list = (self.d.head.competitors || []).concat([domain]);
self.saving = true;
$http.post('/rest/serp/competitors', { websiteid: h.siteId(), competitors: list }).then(function () {
self.setEnabled(true);
}, function () {
self.saving = false;
self.error = 'server';
});
},
setMode: function (m) { this.mode = m; },
setSpeed: function (m) { this.speed = m; this.refresh(); },
setVisit: function (m) { this.visit = m; this.refresh(); },
// ------------------------------------------------------------------ alles wat de pagina toont, één keer per nieuwe data
refresh: function () {
var self = this, g = self.d.head;
self.v = { status: self.statusLine(g) };
if (!g || !g.enabled) { return; }
if (self.tab === 'overview' && g.rows) {
self.v.kpis = self.viewKpis(g);
self.v.notable = g.notable ? self.viewNotable(g.notable) : null;
self.v.board = self.viewBoard(g);
self.v.trend = self.viewTrend(g);
}
if (self.tab === 'visitors' && g.visit) { self.v.vis = self.viewVisitors(g); }
if (self.tab === 'diagnose' && g.rows) { self.v.diag = self.viewDiagnose(g); }
},
statusLine: function (g) {
if (!g) { return ''; }
if (!g.enabled) { return t('crux.status_off'); }
if (!g.newest) { return t('crux.status_pending'); }
if (this.tab === 'visitors') { return t('crux.status_visitors', { device: t('crux.ffl.' + this.ff) }); }
return t('crux.status_line', { date: f.day(g.newest) });
},
compColor: function (r) { return r.own ? '#2563eb' : COMP_COLORS[(r.color >= 0 ? r.color : 0) % COMP_COLORS.length]; },
weekTitle: function (e) { return t('crux.week', { date: f.day(e) }); },
nodataText: function () { return this.ff === 'ALL' ? t('crux.nodata') : t('crux.nodata_all'); },
// de eerste week van elke maand, met minstens twee weken tussen twee labels
months: function (weeks) {
var out = [], last = null, lastI = -9;
weeks.forEach(function (e, i) {
var mo = e.slice(0, 7);
if (mo !== last) {
last = mo;
if (i - lastI >= 3) { out.push({ i: i, label: f.month(e) }); lastI = i; }
}
});
return out;
},
// ------------------------------------------------------------------ Overzicht
viewKpis: function (g) {
var own = g.rows[0], out = [], self = this;
if (!own || own.pending) {
return ['status', 'lcp', 'inp', 'cls', 'place'].map(function (k) { return { label: t('crux.kpi.' + k), loading: true }; });
}
var bad = CWV.filter(function (m) { var r = mtCwv.rating(m, own.p75[m]); return r && r !== 'good'; });
out.push({
label: t('crux.kpi.status'),
value: own.status ? t('crux.kpi.' + own.status) : t('crux.kpi.nodata'),
mark: own.status === 'pass' ? 'good' : (own.status === 'fail' ? 'poor' : 'none'),
note: own.status === 'pass' ? t(own.partial ? 'crux.kpi.pass_partial' : 'crux.kpi.pass_note')
: (own.status === 'fail' ? t('crux.kpi.fail_note', { metrics: bad.map(function (m) { return t('crux.metric.' + m); }).join(', ') }) : self.nodataText())
});
CWV.forEach(function (m) {
var l = own.last[m], lim = ' · ' + t('crux.kpi.good_to', { v: limitText(m, mtCwv.LIMITS[m][0]) });
if (!l) {
out.push({ label: t('crux.kpi.' + m), value: '–', note: self.nodataText() });
return;
}
var delta = l.stale ? null : own.pct[m];
out.push({ label: t('crux.kpi.' + m), value: mtCwv.fmt(m, l.v), delta: delta,
note: (l.stale ? t('crux.kpi.stale', { date: f.day(l.end) }) : (delta !== null ? t('crux.kpi.vs4w') : t('crux.kpi.p75'))) + lim });
});
if (g.place) {
var lcps = g.rows.filter(function (r) { return r.status && r.p75.lcp !== null; }).map(function (r) { return r.p75.lcp; });
var fastest = own.p75.lcp !== null && own.p75.lcp <= Math.min.apply(null, lcps);
out.push({ label: t('crux.kpi.place'), value: '#' + g.place.pos, suffix: t('crux.kpi.of', { of: g.place.of }),
note: g.place.of > 1 && fastest ? t('crux.kpi.fastest', { n: g.place.of }) : t('crux.kpi.place_note') });
} else {
out.push({ label: t('crux.kpi.place'), value: '–', note: g.competitors.length ? self.nodataText() : t('crux.kpi.place_none') });
}
return out;
},
// "Opvallend": een vette zin en de rest (crux::notable)
viewNotable: function (n) {
var head = n.own ? t('crux.note.own_' + n.dir, { pct: n.pct }) : t('crux.note.' + n.dir, { domain: n.domain, pct: n.pct });
var rest = t('crux.note.lcp', { from: f.num(n.from / 1000, 2), to: mtCwv.fmt('lcp', n.to) });
if (n.cls) {
var better = n.cls[1] < n.cls[0];
rest += ' ' + t(better ? 'crux.note.cls_better' : 'crux.note.cls_worse', { from: mtCwv.fmt('cls', n.cls[0]), to: mtCwv.fmt('cls', n.cls[1]) });
}
rest += '.';
if (n.lead) { rest += ' ' + t('crux.note.' + n.lead[0], { from: sec1(n.lead[1]), to: sec1(n.lead[2]) }); }
return { head: head, rest: rest };
},
// het scorebord: op plek (geslaagd, aandeel goed, snelste LCP), domeinen zonder data onderaan
viewBoard: function (g) {
var self = this;
var rows = g.rows.slice().sort(function (a, b) { return (a.rank || 99) - (b.rank || 99); });
return rows.map(function (r) {
var stale = r.last.lcp && r.last.lcp.stale;
return {
domain: r.domain, own: r.own, rank: r.rank || '–', pending: r.pending, nodata: !r.pending && !r.status,
badge: r.status ? 'cwv_' + r.status : 'cwv_nodata', partial: r.partial,
cells: CWV.map(function (m) {
var l = r.last[m];
return { m: m, value: l ? l.v : null, note: l && l.stale ? t('crux.board.last_measured') : '', title: l && l.stale ? self.weekTitle(l.end) : '' };
}),
change: stale ? null : r.pct.lcp
};
});
},
// de trend: drie kleine grafieken (LCP, INP, CLS) naast elkaar ()
viewTrend: function (g) {
var self = this, weeks = g.weeks || [], n = weeks.length;
if (!n) { return { empty: true }; }
var own = g.rows[0], comps = g.rows.filter(function (r) { return !r.own && r.hist; });
var panels = CWV.map(function (m) {
var lim = mtCwv.LIMITS[m], all = [];
var push = function (vals) { (vals || []).forEach(function (v) { if (!none(v)) { all.push(+v); } }); };
if (own.hist) { push(own.hist[m]); }
comps.forEach(function (r) { push(r.hist[m]); });
var top = Math.min(Math.max(Math.max.apply(null, all.concat([0])), lim[0] * 1.2), lim[1] * 2);
var max = mtChart.niceMax(top), step = mtChart.niceMax(max / 3), ticks = [];
for (var v = 0; v <= max + 1e-9; v += step) { ticks.push(+v.toFixed(4)); }
var l = own.last[m];
return {
title: t('crux.kpi.' + m),
sub: l ? t('crux.trend.now', { v: mtCwv.fmt(m, l.v) }) + (l.stale ? ' (' + self.weekTitle(l.end) + ')' : '') : self.nodataText(),
fmt: mtCwv.fmtOf(m), tick: function (x) { return limitText(m, x); }, max: max, ticks: ticks,
good: lim[0], poor: lim[1], limitLabel: t('crux.kpi.good_to', { v: limitText(m, lim[0]) }),
own: own.hist ? own.hist[m] : [], band: g.groups[m], med: g.groups[m].med,
others: comps.map(function (r) { return { label: r.domain, color: self.compColor(r), values: r.hist[m] }; }),
takeaway: self.takeText(m, g.takeaways[m] || [])
};
});
return {
n: n, weeks: n,
data: {
n: n, titles: weeks.map(function (e) { return self.weekTitle(e); }), months: self.months(weeks),
labels: { own: t('crux.trend.you'), med: t('crux.trend.median'), nodata: t('crux.trend.too_little') },
others: comps.map(function (r) { return { label: r.domain, color: self.compColor(r) }; }), panels: panels
},
hasComps: comps.length > 0
};
},
// de zinnen onder een grafiek (crux::takeaways)
takeText: function (m, list) {
var metric = t('crux.metric.' + m);
return list.map(function (x) {
switch (x.kind) {
case 'always_best': case 'best_now': case 'worst_now': case 'all_good': return t('crux.take.' + x.kind + '.' + m);
case 'middle': return t('crux.take.middle', { pos: x.pos, of: x.of });
case 'recovered': return t('crux.take.recovered', { domain: x.domain, zone: t('crux.take.zone.' + x.zone) });
case 'own_change': return t('crux.take.own_change.' + x.dir, { m: metric, pct: x.pct });
default: return t('crux.take.' + x.kind, { domain: x.domain, m: metric });
}
}).join(' ');
},
// ------------------------------------------------------------------ Snelheid en bezoekers
viewVisitors: function (g) {
var self = this, n = (g.weeks || []).length, sm = self.speed, vm = self.visit;
if (!n) { return { empty: true }; }
var sv = g.speed[sm], vis = g.visit[vm], lim = mtCwv.LIMITS[sm];
var smax = mtChart.niceMax(Math.max.apply(null, sv.filter(function (x) { return !none(x); }).concat([lim[0] * 0.2])) * 1.05);
var vfmt = self.visitFmt(vm);
var vmax = vis.state === 'ok' ? mtChart.niceMax(Math.max.apply(null, vis.values.filter(function (x) { return !none(x); }).concat([vm === 'bounce' ? 10 : 1]))) : 1;
var data = {
n: n, titles: g.weeks.map(function (e) { return self.weekTitle(e); }), months: self.months(g.weeks),
top: { title: t('crux.vis.speed_week', { m: t('crux.metric.' + sm) }), sub: t(sm === 'cls' ? 'crux.vis.lower_stable' : 'crux.vis.lower_faster'),
label: t('crux.metric.' + sm), values: sv, fmt: mtCwv.fmtOf(sm), tick: function (x) { return limitText(sm, x); },
max: smax, good: lim[0], poor: lim[1] },
bottom: { title: t('crux.vis.' + vm + '_week'), sub: t('crux.vis.src_' + vis.source, { device: t('crux.ffl.' + self.ff) }), label: t('crux.vis.short.' + vm),
values: vis.state === 'ok' ? vis.values : null, fmt: vfmt, tick: function (x) { return vm === 'bounce' ? f.num(x, 0) + '%' : (x >= 1e4 ? f.num(x / 1000, 0) + 'K' : f.num(x, vm === 'pps' && x % 1 ? 1 : 0)); }, max: vmax }
};
var cmp = g.compare[sm] ? g.compare[sm][vm] : null, tiles;
var sfmt = mtCwv.fmtOf(sm);
if (vis.state !== 'ok') {
tiles = null;
} else if (!cmp || cmp.flat) {
tiles = ['fast', 'slow', 'diff'].map(function (k) { return { label: t('crux.vis.tile_' + k, { n: 10 }), value: '–', note: t(cmp ? 'crux.vis.flat' : 'crux.vis.too_few') }; });
} else {
var d = cmp.diff, diffText;
if (vm === 'bounce') { diffText = t('crux.vis.diff_pt', { v: (d > 0 ? '+' : '') + f.num(d, 1) }); }
else if (vm === 'pps') { diffText = (d > 0 ? '+' : '') + f.num(d, 2); }
else { diffText = cmp.pct === null ? '–' : (cmp.pct > 0 ? '+' : '') + f.num(cmp.pct, 0) + '%'; }
tiles = [
{ label: t('crux.vis.tile_fast', { n: cmp.n }), value: t('crux.vis.value.' + vm, { v: vfmt(cmp.fast.visit) }), note: t('crux.vis.avg', { m: t('crux.metric.' + sm), v: sfmt(cmp.fast.speed) }) },
{ label: t('crux.vis.tile_slow', { n: cmp.n }), value: t('crux.vis.value.' + vm, { v: vfmt(cmp.slow.visit) }), note: t('crux.vis.avg', { m: t('crux.metric.' + sm), v: sfmt(cmp.slow.speed) }) },
{ label: t('crux.vis.tile_diff'), value: diffText, note: t('crux.vis.caveat') }
];
}
return { data: data, state: vis.state, source: vis.source, tiles: tiles };
},
visitFmt: function (vm) {
if (vm === 'bounce') { return function (v) { return f.num(v, 0) + '%'; }; }
if (vm === 'pps') { return function (v) { return f.num(v, 2); }; }
return function (v) { return f.short(v); };
},
// ------------------------------------------------------------------ Diagnose
viewDiagnose: function (g) {
var self = this, maxSum = 0;
var staleNote = function (l) { return l && l.stale ? self.weekTitle(l.end) : ''; };
g.rows.forEach(function (r) {
if (r.parts) { maxSum = Math.max(maxSum, PARTS.reduce(function (a, k) { return a + (r.parts.v[k] || 0); }, 0)); }
});
var rating = function (m, v) { return mtCwv.rating(m, v) || 'none'; };
var val = function (l) { return l ? l.v : null; };
return {
findings: (g.findings || []).map(function (x) { return self.findingText(x); }),
partsMax: maxSum ? mtChart.niceMax(maxSum) : 0,
partsLegend: PARTS.map(function (k) { return { label: t('crux.part.' + k), color: PART_COLORS[k] }; }),
navLegend: NAV.filter(function (k) { return k[0] !== 'restore'; }).map(function (k) { return { label: t('crux.nav.' + k[0]), color: k[1] }; }),
ms: mtCwv.ms,
rows: g.rows.map(function (r) {
var parts = r.parts ? PARTS.map(function (k) { var v = r.parts.v[k]; return { label: t('crux.part.' + k), value: v, color: PART_COLORS[k], text: mtCwv.ms(v) }; }) : null;
var nav = r.nav ? NAV.filter(function (k) { return r.nav.v[k[0]] > 0; }).map(function (k) {
var v = r.nav.v[k[0]];
return { label: t('crux.nav.' + k[0]), value: 100 * v, color: k[1], ink: k[2], text: v >= 0.06 ? f.num(100 * v, 0) + '%' : '' };
}) : null;
var navNote = '';
if (r.nav) {
var fast = (r.nav.v.back_forward_cache || 0) + (r.nav.v.prerender || 0), back = r.nav.v.back_forward || 0;
navNote = back > fast ? t('crux.diag.nav_back', { pct: f.num(100 * back, 0) }) : t('crux.diag.nav_fast', { pct: f.num(100 * fast, 0) });
}
var dens = val(r.ad_density);
return {
domain: r.domain, own: r.own, pending: r.pending,
parts: parts, partsSum: parts ? mtCwv.ms(PARTS.reduce(function (a, k) { return a + (r.parts.v[k] || 0); }, 0)) : '–', partsNote: staleNote(r.parts),
nav: nav, navNote: navNote, navStale: staleNote(r.nav),
ttfb: mtCwv.ms(val(r.ttfb)), ttfbR: rating('ttfb', val(r.ttfb)), fcp: mtCwv.ms(val(r.fcp)), fcpR: rating('fcp', val(r.fcp)),
rtt: none(val(r.rtt)) ? '–' : f.num(val(r.rtt)) + ' ms', netStale: staleNote(r.ttfb),
dens: none(dens) ? '–' : f.num(dens, 0) + '%', densW: none(dens) ? 0 : Math.min(100, 100 * dens / 25),
kb: none(val(r.ad_kb)) ? '–' : f.num(val(r.ad_kb) / 1000, 1) + ' MB', cpu: mtCwv.ms(val(r.ad_cpu)),
adsStale: staleNote(r.ad_density) || staleNote(r.ad_cpu), hasAds: !none(dens) || !none(val(r.ad_kb)) || !none(val(r.ad_cpu))
};
})
};
},
// een bevinding (crux::findings) naar het kaartje
findingText: function (x) {
var part = x.part ? t('crux.part.' + x.part).toLowerCase() : '';
var pct = function (v) { return f.num(100 * v, 0); };
switch (x.kind) {
case 'ads_own':
return { kind: t('crux.find.kind.ads'), color: '#2563eb', head: t('crux.find.ads_own', { pct: x.pct }), text: t(x.safe ? 'crux.find.ads_own_safe' : 'crux.find.ads_own_risk', { min: x.min, max: x.max }) };
case 'ads_comp':
return { kind: t('crux.find.kind.ads'), color: '#2563eb', head: t('crux.find.ads_comp', { domain: x.domain, pct: x.pct }), text: x.own === null ? '' : t('crux.find.ads_comp_own', { pct: x.own }) };
case 'part_comp':
return { kind: t('crux.find.kind.comp'), color: PART_COLORS[x.part], head: t('crux.find.part_comp', { domain: x.domain, v: mtCwv.ms(x.value), part: part }),
text: t('crux.find.why.' + x.part) + (x.ahead ? ' ' + t('crux.find.ahead') : '') };
case 'part_own':
return { kind: t('crux.find.kind.own'), color: PART_COLORS[x.part], head: t('crux.find.part_own', { v: mtCwv.ms(x.value), part: part }), text: t('crux.find.fix.' + x.part) };
case 'bfcache_comp': case 'bfcache_own':
return { kind: t(x.kind === 'bfcache_own' ? 'crux.find.kind.own' : 'crux.find.kind.comp'), color: '#f59e0b',
head: t('crux.find.' + x.kind, { domain: x.domain }),
text: t('crux.find.' + x.kind + '_text', { pct: pct(x.back) }) + (x.best ? ' ' + t('crux.find.bfcache_best', { domain: x.best, pct: pct(x.best_share) }) : '') };
case 'ttfb_own':
return { kind: t('crux.find.kind.server'), color: '#0f766e', head: t('crux.find.ttfb_own', { v: mtCwv.ms(x.value) }), text: t('crux.find.ttfb_own_text') };
default:
return { kind: '', head: '', text: '' };
}
}
};
// de vertalingen kunnen later binnenkomen dan de gegevens: dan alleen opnieuw tekenen
var offTranslate = $rootScope.$on('$translateChangeSuccess', function () {
c.tabs = c.tabs.map(function (x) { return angular.extend({}, x, { label: t('crux.tab.' + x.key) }); });
c.refresh();
});
$scope.$on('$destroy', offTranslate);
c.init();
}]);