(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();
}
}
};
}
app.config(['$routeProvider', '$locationProvider', 'USER', '$sceProvider', '$translateProvider', function AppConfig($routeProvider, $locationProvider, USER, $sceProvider, $translateProvider) {
angular.lowercase = angular.$$lowercase;
$translateProvider.useStaticFilesLoader({
prefix: '/translations/',
suffix: '.json?r=' + Math.random()
});
/* ZET */
$translateProvider.preferredLanguage(USER.site.locale.lang);
$translateProvider.useSanitizeValueStrategy('sce');
$sceProvider.enabled(false);
$routeProvider
.when('/', {
title: 'APP',
templateUrl: viewfolder + '/home/index.html?r=' + r,
nav: 'dashboard'
})
/* help*/
.when('/help', {
title: 'Help',
templateUrl: viewfolder + '/help/index.html?r=' + r,
nav: 'help'
})
/* uptime-monitor */
.when('/uptime', {
title: 'Uptime',
templateUrl: viewfolder + '/uptime/index.html?r=' + r,
nav: 'uptime'
})
/* Posities in Google (SERP-tracker): overzicht, /serp/keywords en /serp/competitors; zichtbaar via user.site.show.serp */
.when('/serp', {
title: 'Rank tracker',
templateUrl: viewfolder + '/serp/index.html?r=' + r,
nav: 'serp'
})
/* Search Console Insights: overzicht, /onderwerpen(/:id), /gevolgd, /verkennen; zichtbaar via user.site.show.gscinsights (js/controller/gscinsights.js) */
.when('/search-insights', {
title: 'Search Insights',
templateUrl: viewfolder + '/gscinsights/index.html?r=' + r,
nav: 'gscinsights'
})
.when('/search-insights/:tab', {
title: 'Search Insights',
templateUrl: viewfolder + '/gscinsights/index.html?r=' + r,
nav: 'gscinsights',
reloadOnSearch: false
})
.when('/search-insights/:tab/:id', {
title: 'Search Insights',
templateUrl: viewfolder + '/gscinsights/index.html?r=' + r,
nav: 'gscinsights'
})
.when('/ai-insights', {
title: 'AI Insights',
templateUrl: viewfolder + '/aiinsights/index.html?r=' + r,
nav: 'aiinsights'
})
.when('/ai-insights/:tab', {
title: 'AI Insights',
templateUrl: viewfolder + '/aiinsights/index.html?r=' + r,
nav: 'aiinsights'
})
.when('/ai-insights/:tab/:id', {
title: 'AI Insights',
templateUrl: viewfolder + '/aiinsights/index.html?r=' + r,
nav: 'aiinsights'
})
.when('/serp/:tab', {
title: 'Rank tracker',
templateUrl: viewfolder + '/serp/index.html?r=' + r,
nav: 'serp'
})
/* contentcheck prototype (verstopt, alleen brabbels, demodata) */
.when('/contentcheck-preview', {
title: 'Contentcheck',
templateUrl: viewfolder + '/content/check-preview.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'
})
.when('/llm-visibility', {
title: 'llmvisibility',
templateUrl: viewfolder + '/llm-visibility/index.html?r=' + r,
nav: 'llmvisibility'
})
/* 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;
},
})
.when('/hub/:report/:view', {
title: 'Performarnce Hub',
templateUrl: viewfolder + '/api/hub.html?r=' + r,
nav: 'hub',
controller: function ($scope, $routeParams) {
$scope.urlreport = $routeParams.report;
$scope.urlview = $routeParams.view;
},
})
.when('/hub/:report/:view/:urlfilter', {
title: 'Performarnce Hub',
templateUrl: viewfolder + '/api/hub.html?r=' + r,
nav: 'hub',
controller: function ($scope, $routeParams) {
$scope.urlreport = $routeParams.report;
$scope.urlview = $routeParams.view;
$scope.urlfilter = $routeParams.urlfilter;
},
})
.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);
});*/
});
}
}
]);
app.controller('home', ['$scope', '$timeout', '$http', '$rootScope', '$filter', 'h',
function($scope, $timeout, $http, $rootScope, $filter, h) {
//if ($rootScope.user.firstrun) {
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;
});
}
$scope.pagecharts = {};
$scope.curtemplate = {};
$scope.colors = h.colors();
$scope.page = {
'widgets': [{
'type': 'analytics_timeline',
'metrics': ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll', 'uniquePageviews', 'avgTimeOnPage', 'entrances', 'exitRate', 'pageValue', 'transactions', 'uniquePurchases', 'totalValue', 'revenuePerTransaction', 'itemQuantity', 'itemsPerPurchase'],
'metric': 'sessions',
'colspan': 6
}, {
'type': 'analytics_chart',
'dimensions': ['channelGrouping', 'keyword', 'source', 'country', 'userAgeBracket', 'userGender', 'socialNetwork', 'deviceCategory', 'adMatchedQuery', 'productName', 'productCategory', 'pagePath', 'landingPagePath', 'exitPagePath'],
'dimension': 'landingPagePath',
'charts': ['pie', 'bar', 'progress'],
'chart': 'bar',
'metrics': ['sessions', 'users', 'pageviews', 'pageviewsPerSession', 'avgSessionDuration', 'percentNewSessions', 'bounceRate', 'goalCompletionsAll', 'goalValueAll', 'goalConversionRateAll', 'uniquePageviews', 'avgTimeOnPage', 'entrances', 'exitRate', 'pageValue', 'transactions', 'uniquePurchases', 'totalValue', 'revenuePerTransaction', 'itemQuantity', 'itemsPerPurchase'],
'metric': 'sessions',
'colspan': 6
},
{
'type': 'analytics4_timeline',
'class': 'mt-4',
'metrics': ['sessions', 'totalUsers', 'screenPageViews', 'screenPageViewsPerSession', 'averageSessionDuration', 'newUsers', 'bounceRate', 'eventCount', 'eventCountPerUser'],
'metric': 'sessions',
'colspan': 6
},
{
'type': 'searchconsole_timeline',
'class': 'mt-4',
'metrics': ['clicks', 'ctr', 'position', 'impressions'],
'metric': 'clicks',
'colspan': 6
}, {
'type': 'searchconsole_chart',
'dimensions': ['query', 'page', 'country', 'device'],
'dimension': 'query',
'charts': ['pie', 'bar', 'progress'],
'chart': 'bar',
'metrics': ['clicks', 'ctr', 'position', 'impressions'],
'metric': 'clicks',
'colspan': 6
}, {
'type': 'searchconsole_keywords',
'colspan': 6
}, {
'type': 'googleads_timeline',
'class': 'mt-4',
'metrics': ['Clicks', 'Impressions', 'Cost', 'AverageCpc', 'Ctr', 'Conversions', 'CostPerConversion', 'ConversionRate', 'ConversionValue', 'SearchImpressionShare', 'SearchAbsoluteTopImpressionShare', 'roas'],
'metric': 'Clicks',
'colspan': 6
}, {
'type': 'googleads_performance_chart',
'dimensions': ['CampaignName', 'AdGroupName', 'Query'],
'dimension': 'CampaignName',
'charts': ['pie', 'bar', 'progress'],
'chart': 'bar',
'metrics': ['Clicks', 'Impressions', 'Cost', 'AverageCpc', 'Ctr', 'Conversions', 'CostPerConversion', 'ConversionRate', 'ConversionValue', 'SearchImpressionShare', 'SearchAbsoluteTopImpressionShare', 'roas'],
'metric': 'Clicks',
'colspan': 6
}, {
'type': 'googleads_conversion_chart',
'dimensions': ['ConversionTypeName'],
'dimension': 'ConversionTypeName',
'charts': ['pie', 'bar', 'progress'],
'chart': 'bar',
'metrics': ['Conversions', 'CostPerConversion', 'ConversionRate', 'ConversionValue'],
'metric': 'Conversions',
'colspan': 6
}, {
'type': 'facebookinsights_timeline',
'class': 'mt-4',
'metrics': ['page_fans_total', 'page_impressions', 'engagements'],
'metric': 'page_fans_total',
'colspan': 6
}, {
'type': 'facebookads_timeline',
'metrics': ['clicks', 'impressions', 'cpc', 'cpm', 'spend', 'ctr'],
'metric': 'clicks',
'colspan': 6
}, {
'type': 'ranktracer',
'class': 'mt-4',
'colspan': 6
}, {
'type': 'onpage_seo',
'colspan': 12
}, {
'type': 'onpage_changes',
'colspan': 6
}, {
'type': 'onpage_changes_donut',
'colspan': 6
}],
'widths': [
{ 'colspan': 3, 'width': '25%' },
{ 'colspan': 4, 'width': '33%' },
{ 'colspan': 6, 'width': '50%' },
{ 'colspan': 8, 'width': '66%' },
{ 'colspan': 9, 'width': '75%' },
{ 'colspan': 12, 'width': '100%' },
]
}
$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.do = function(obj, method, param) {
if (obj == 'fnWidgets') {
fnWidgets[method](param);
} else if (obj == 'fnTemplates') {
fnTemplates[method](param);
}
}
var fnTemplates = {
list: function() {
$http.get('/rest/home/template.list', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
$scope.templates = res.data;
});
},
use: function(widgets) {
$scope.widgets = widgets;
fnWidgets.save();
$scope.fnCloseModal();
},
create: function() {
var cleanwidgets = angular.copy($scope.widgets).map(function(c) {
delete c.widget;
return c;
})
$scope.curtemplate.widgets = cleanwidgets;
$http.post('/rest/home/template.save', { websiteid: h.siteId(), 'template': $scope.curtemplate }).then(function successCallback(res) {
$scope.fnCloseModal();
});
},
save: function(template) {
var cleanwidgets = angular.copy($scope.widgets).map(function(c) {
delete c.widget;
return c;
})
template.widgets = cleanwidgets;
template.name = ($scope.curtemplate.name) ? $scope.curtemplate.name : template.name;
template.description = ($scope.curtemplate.description) ? $scope.curtemplate.description : template.description;
$http.post('/rest/home/template.save', { websiteid: h.siteId(), 'template': template }).then(function successCallback(res) {
$scope.fnCloseModal();
});
},
delete: function(template) {
$http.post('/rest/home/template.delete', template).then(function successCallback(res) {
$scope.fnCloseModal();
});
},
}
var fnWidgets = {
'add': function(widget) {
$http.post('/rest/home/widget.add', angular.extend({ websiteid: h.siteId() }, widget)).then(function successCallback(res) {
$scope.fnCloseModal();
fnWidgets.list();
$scope.page.edit = false;
});
},
'save': function() {
$http.post('/rest/home/widgets.save', angular.extend({ websiteid: h.siteId() }, { 'widgets': $scope.widgets })).then(function successCallback(res) {
$scope.fnCloseModal();
fnWidgets.list();
$scope.page.edit = false;
});
},
'delete': function(widget) {
angular.forEach($scope.widgets, function(v, k) {
if (v.k == widget.k) {
$scope.widgets.splice(k, 1);
fnWidgets.save();
}
})
},
'reorder': function(o) {
o.widget.order = o.widget.order - 0.1;
angular.forEach($filter('orderBy')($scope.widgets, 'order', false), function(v, k) {
v.order = k + 1;
});
},
'pre_add': function() {
fnTemplates.list();
$scope.fnModal('home/modal.widgets');
},
'pre_edit': function() {
$scope.page.edit = true;
},
'list': function() {
$http.get('/rest/home/widgets.list', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
if (res.data.length == 0) {
res.data = [
{ "type": "onpage_seo", "metric": null, 'colspan': 12, 'order': 1, 'k': 'k1' },
{ "type": "analytics4_timeline", "metric": "sessions", 'colspan': 4, 'order': 2, 'k': 'k2' },
{ "type": "onpage_changes", "metric": null, 'colspan': 4, 'order': 3, 'k': 'k3' },
{ "type": "searchconsole_keywords", "metric": null, 'colspan': 4, 'order': 4, 'k': 'k4' },
{ "type": "searchconsole_timeline", "metric": 'clicks', 'colspan': 6, 'order': 5, 'k': 'k5' },
{ "type": "ranktracer", "metric": null, 'colspan': 6, 'order': 6, 'k': 'k6' },
];
}
$scope.widgets = res.data;
angular.forEach($scope.widgets, function(v, k) {
fnWidgetGenerator[v.type](v);
})
});
}
}
fnWidgets.list();
var _charts = {
init: function(type, dataset, dimension, metric, dateformat) {
return _charts[type](dataset, dimension, metric, dateformat);
},
progress: function(dataset, dimension, metric) {
return $filter('orderBy')(dataset, metric, true).slice(0, 5);
},
bar: function(dataset, dimension, metric) {
dataset = $filter('orderBy')(dataset, metric, true).slice(0, 8);
//inject color attribute with value
if (typeof dataset != undefined) {
for (var i = 0; i < dataset.length; i++) { dataset[i].color = $scope.colors[i]; }
}
return {
data: dataset.slice(0, 6),
type: 'serial',
categoryField: dimension,
rotate: false,
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,
}],
}
},
pie: function(dataset, dimension, metric) {
dataset = $filter('orderBy')(dataset, metric, true).slice(0, 12);
return {
data: dataset.slice(0, 9),
type: 'pie',
categoryField: dimension,
"precision": 0,
"decimalSeparator": ",",
"thousandsSeparator": ".",
legend: {
enabled: false,
position: 'right',
autoMargins: false,
marginRight: 100,
},
titleField: dimension,
valueField: metric,
labelRadius: 5,
radius: '42%',
innerRadius: '60%',
labelText: '[[title]]',
}
},
timeline(dataset, dimension, metric, dateformat) {
return {
data: dataset,
type: 'serial',
categoryField: dimension,
rotate: false,
dataDateFormat: dateformat,
categoryAxis: { gridPosition: "start", parseDates: true, gridThickness: 0 },
valueAxes: [{ position: "left", axisAlpha: 0, labelsEnabled: true, dashLength: 4, color: '#dedede', }],
graphs: [{
'balloonText': "Amount [[value]] ",
"bullet": "round",
"bulletBorderAlpha": 1,
"bulletColor": "#FFFFFF",
"useLineColorForBulletBorder": true,
"fillAlphas": 0.3,
"lineThickness": 2,
"lineAlpha": 1,
"bulletSize": 7,
"title": "Expenses",
"valueField": metric,
'lineColor': fnWidgetGenerator.color()
}],
}
}
}
var fnWidgetGenerator = {
i: -1,
'color': function(i) {
var colors = h.colors();
fnWidgetGenerator.i += 1;
return colors[fnWidgetGenerator.i];
},
'analytics_timeline': function(widget) {
$http.get('/rest/analytics/v4', {
params: { 't':'123','dimension': 'date', 'end': 'TODAY', 'metrics': widget.metric, 'start': '30DAYSAGO', websiteid: h.siteId(), }
}).then(function successCallback(res) {
widget.widget = _charts.init('timeline', res.data.data, 'ga:date', widget.metric, 'YYYYMMDD')
widget.widget.action = res.data.action;
});
},
'analytics4_timeline': function(widget) {
$http.get('/rest/analytics4/report', {
params: { 't':'123','dimension': 'date', 'end': 'TODAY', 'metrics': widget.metric, 'start': '30DAYSAGO', websiteid: h.siteId(), }
}).then(function successCallback(res) {
widget.widget = _charts.init('timeline', res.data.data, 'date', widget.metric, 'YYYYMMDD')
widget.widget.action = res.data.action;
});
},
'analytics_chart': function(widget) {
$http.get('/rest/analytics/v4', {
params: { 'dimension': widget.dimension, 'end': 'TODAY', 'metrics': widget.metric, 'start': '30DAYSAGO', websiteid: h.siteId() }
}).then(function successCallback(res) {
widget.widget = _charts.init(widget.chart, res.data.data, 'ga:' + widget.dimension, widget.metric)
widget.widget.action = res.data.action;
});
},
'googleads_timeline': function(widget) {
$http.get('/rest/googlexads/report', {
params: { 'dimension': 'Date', 'end': 'TODAY', 'metrics': widget.metric, 'start': '30DAYSAGO', websiteid: h.siteId(), 'report': 'ACCOUNT_PERFORMANCE_REPORT' }
}).then(function successCallback(res) {
widget.widget = _charts.init('timeline', res.data.data, 'Date', widget.metric, 'YYYY-MM-DD')
widget.widget.action = res.data.action;
});
},
'googleads_performance_chart': function(widget) {
var report = { 'CampaignName': 'CAMPAIGN_PERFORMANCE_REPORT', 'AdGroupName': 'ADGROUP_PERFORMANCE_REPORT', 'Query': 'SEARCH_QUERY_PERFORMANCE_REPORT' };
$http.get('/rest/googlexads/report', {
params: { 'dimension': widget.dimension, 'end': 'TODAY', 'metrics': widget.metric, 'start': '30DAYSAGO', websiteid: h.siteId(), 'report': report[widget.dimension] }
}).then(function successCallback(res) {
widget.widget = _charts.init(widget.chart, res.data.data, widget.dimension, widget.metric)
widget.widget.action = res.data.action;
});
},
'googleads_conversion_chart': function(widget) {
$http.get('/rest/googlexads/report', {
params: { 'dimension': widget.dimension, 'end': 'TODAY', 'metrics': widget.metric, 'start': '30DAYSAGO', websiteid: h.siteId(), 'report': 'ACCOUNT_PERFORMANCE_REPORT' }
}).then(function successCallback(res) {
widget.widget = _charts.init(widget.chart, res.data.data, widget.dimension, widget.metric)
widget.widget.action = res.data.action;
});
},
'facebookinsights_timeline': function(widget) {
$http.get('/rest/facebookinsights/report', {
params: { 'dimension': 'date', 'end': 'TODAY', 'start': '30DAYSAGO', websiteid: h.siteId(), 'endpoint': 'insights', 'report': 'timeline' }
}).then(function successCallback(res) {
if (widget.metric == 'engagements') {
var data = res.data.sets[widget.metric].data.reduce(function(acc, cur) {
if (cur.type == 'engagements') {
acc.push({ 'change': cur.value, 'date': cur.date });
}
return acc;
}, []);
} else {
var data = res.data.sets[widget.metric].data
}
widget.widget = _charts.init('timeline', data, 'date', 'change', 'YYYY-MM-DD')
widget.widget.action = res.data.action;
});
},
'facebookads_timeline': function(widget) {
$http.get('/rest/facebookads/report', {
params: { 'dimension': 'date', 'end': 'TODAY', 'start': '30DAYSAGO', websiteid: h.siteId(), 'level': 'account', 'endpoint': 'insights', 'metrics': widget.metric }
}).then(function successCallback(res) {
widget.widget = _charts.init('timeline', res.data.data, 'date', widget.metric, 'YYYY-MM-DD')
widget.widget.action = res.data.action;
});
},
'onpage_changes': function(widget) {
$http.get('/rest/onpage/rules', { 'params': { 'lang': $scope.user.site.locale.lang } }).then(function successCallback(res) {
$scope.onpage = res.data;
$http.get('/rest/onpage/changes', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
widget.widget = res.data.data;
});
});
},
'onpage_changes_donut': function(widget) {
$http.get('/rest/onpage/changes', { 'params': { websiteid: h.siteId() } }).then(function successCallback(res) {
var stats = [{ 'label': 'change', 'value': 0, 'color': '#808080' }, { 'label': 'done', 'value': 0, 'color': '#4caf50' }, { 'label': 'issue', 'value': 0, 'color': '#ff9800' }];
for (var i in res.data.data) {
if (res.data.data[i].r === true) {
stats[1].value += 1;
} else if (res.data.data[i].r === false) {
stats[2].value += 1;
} else {
stats[0].value += 1;
}
}
widget.widget = { data: stats, type: 'pie', marginLeft: 0, marginRight: 0, marginTop: 10, categoryField: 'label', "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]]', }
});
},
'onpage_seo': function(widget) {
$http.get('/rest/onpage/list', {
params: {
websiteid: h.siteId(),
}
}).then(function successCallback(res) {
widget.widget = res.data;
widget.widget.avg = { 'all': 0, 'technical': 0, 'content': 0, 'mobile': 0, 'indexability': 0 }
if(widget.widget.history){
var l = widget.widget.history.length;
angular.forEach(widget.widget.history, function(value, key) {
widget.widget.avg['all'] += value.all / l
widget.widget.avg['technical'] += value.technical / l
widget.widget.avg['content'] += value.content / l
widget.widget.avg['mobile'] += value.mobile / l
widget.widget.avg['indexability'] += value.indexability / l
});
}
});
},
'ranktracer': function(widget) {
// gevolgde zoekwoorden uit Search Insights (gscinsights/tracked), in de vorm die de widget kent
$http.get('/rest/gscinsights/tracked', { params: { websiteid: h.siteId() } }).then(function successCallback(res) {
var keywords = [];
angular.forEach((res.data && res.data.groups) || [], function (g) {
angular.forEach(g.keywords, function (k) {
keywords.push({
keyword: k.query,
metrics_current: { clicks: k.now.clicks, position: k.now.position },
metrics: { mom: { change: {
clicks: k.year ? k.now.clicks - k.year.clicks : null,
position: k.year && k.year.position && k.now.position ? k.now.position - k.year.position : null
} } },
health_score: k.health
});
});
});
// Sort by clicks descending, take top 8
keywords.sort(function(a, b) {
return (b.metrics_current ? b.metrics_current.clicks : 0) - (a.metrics_current ? a.metrics_current.clicks : 0);
});
widget.widget = keywords.slice(0, 8);
});
},
'searchconsole_keywords': function(widget) {
$http.get('/rest/gscinsights/report', {
params: { 'dimension': 'query', 'end': 'TODAY', 'metrics': 'clicks,ctr,position,impressions', 'start': '30DAYSAGO', 'report': 'searchAnalytics', websiteid: h.siteId(), }
}).then(function successCallback(res) {
widget.widget = res.data;
});
},
'searchconsole_chart': function(widget) {
$http.get('/rest/gscinsights/report', {
params: { 'dimension': widget.dimension, 'end': 'TODAY', 'metrics': widget.metric, 'start': '30DAYSAGO', websiteid: h.siteId(), }
}).then(function successCallback(res) {
widget.widget = _charts.init(widget.chart, res.data.data, widget.dimension, widget.metric);
widget.widget.action = res.data.action;
});
},
'searchconsole_timeline': function(widget) {
$http.get('/rest/gscinsights/report', {
params: { 't':'123','dimension': 'date', 'end': 'TODAY', 'metrics': widget.metric, 'start': '30DAYSAGO', websiteid: h.siteId(), 'report': 'searchAnalytics' }
}).then(function successCallback(res) {
widget.widget = _charts.init('timeline', res.data.data, 'date', widget.metric, 'YYYY-MM-DD')
widget.widget.action = res.data.action;
});
},
};
}
]);
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();
}
]);
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();
}
]);
/*
The current navigation is too literal. It names the data dimension, not the insight. We'll change it to be benefit-driven.
Current Text Proposed Change Why It's Better
Keywords Keyword Opportunities Focuses on the goal: finding new keyword targets.
Pages Top Content Highlights that these are the most important pages.
Country Geographic Performance Sounds more professional and comprehensive.
Device Device Analytics Aligns with industry-standard terminology.
Clusters Topic Intelligence Elevates the feature from a simple grouping to a strategic insight. This is the key change.
Gaps & Gains Gaps & Gains Clearly communicates finding new opportunities and plugging leaks.
*/
app.controller("hub", [
"$scope",
"$timeout",
"$http",
"$rootScope",
"$controller",
"$filter",
"h",
function ($scope, $timeout, $http, $rootScope, $controller, $filter, h) {
$scope.cfg = {
authurl: "https://app.marketingtracer.com/rest/searchconsole/connect?websiteid=" + h.siteId(),
dateformat: "YYYY-MM-DD",
dimensionprefix: "",
urlbase: "hub",
report_endpoint: "/rest/hub/report",
};
if ($rootScope.user.reseller?.config?.authdomain) {
$scope.cfg.authurl = "https://" + $rootScope.user.reseller.config.authdomain + "/rest/searchconsole/connect?websiteid=" + h.siteId();
}
$scope.search = {
updateFilter: function (query) {
$scope.search.query = query;
},
query: "",
};
$scope.gloMetrics = {
...h.metrics("searchconsole"),
...h.metrics("analytics4"),
};
$scope.limit = 100;
$scope.clusterProgress = 0;
$scope.topicMode = "smart";
$scope.changeMode = "percentage";
$scope.clusterValue = { key: "none", hasValue: false };
$scope.starredKeywords = new Set();
$scope.starringKeywords = new Set();
$scope.customTopic = {
displayName: "",
exact_keywords: ["", "", ""],
example_keywords: ["", "", ""],
boost_terms: ["", ""],
type: "exact",
};
$scope.customTopicSubmitting = false;
$scope.customTopicMessages = {};
$scope.setChangeMode = function (mode) {
$scope.changeMode = mode;
};
$scope.setTopicMode = function (mode) {
$scope.topicMode = mode;
var starredSet = new Set($scope.analytics.second.data.starred || []);
$scope.starredKeywords = starredSet;
if (mode === "keywords") {
$scope.currentKeywords = $scope.analytics.second.data.keywords || [];
} else if (mode === "starred") {
// Filter keywords to only show starred ones
var allKeywords = $scope.analytics.second.data.keywords || [];
$scope.currentKeywords = allKeywords.filter(function (keyword) {
return starredSet.has(keyword.query);
});
console.log("Starred keywords filtered:", $scope.currentKeywords.length, "out of", allKeywords.length);
}
};
$scope.resetCustomTopicForm = function (form) {
$scope.customTopic = {
displayName: "",
exact_keywords: ["", "", ""],
example_keywords: ["", "", ""],
boost_terms: ["", ""],
type: "default",
};
if (form) {
form.$setPristine();
form.$setUntouched();
}
};
$scope.submitCustomTopic = function (form) {
if (form && form.$invalid) {
form.$setSubmitted();
return;
}
var payload = {
display_name: ($scope.customTopic.displayName || "").trim(),
type: $scope.customTopic.type || "default",
example_keywords: $scope.customTopic.example_keywords
.map(function (keyword) {
return (keyword || "").trim();
})
.filter(function (keyword) {
return keyword.length > 0;
}),
exact_keywords: $scope.customTopic.exact_keywords
.map(function (term) {
return (term || "").trim();
})
.filter(function (term) {
return term.length > 0;
}),
boost_terms: $scope.customTopic.boost_terms
.map(function (term) {
return (term || "").trim();
})
.filter(function (term) {
return term.length > 0;
}),
};
// Validate based on type
if (!payload.display_name) {
$scope.customTopicMessages = {
error: "Provide a display name.",
};
if (form) {
form.$setSubmitted();
}
return;
}
if (payload.type === "exact" && payload.exact_keywords.length === 0) {
$scope.customTopicMessages = {
error: "Provide at least one exact keyword.",
};
if (form) {
form.$setSubmitted();
}
return;
}
if (payload.type !== "exact" && payload.example_keywords.length < 3) {
$scope.customTopicMessages = {
error: "Provide at least three example keywords.",
};
if (form) {
form.$setSubmitted();
}
return;
}
// If editing, add the cluster ID to the payload
if ($scope.customTopic.customClusterId) {
payload.custom_cluster_id = $scope.customTopic.customClusterId;
}
$scope.customTopicSubmitting = true;
$scope.customTopicMessages = {};
var endpoint = $scope.customTopic.customClusterId ? "/rest/searchconsole/cluster_custom_edit?websiteid=" + h.siteId() : "/rest/searchconsole/cluster_custom_add?websiteid=" + h.siteId();
$http
.post(endpoint, payload)
.then(function (response) {
$scope.customTopicMessages = {
success: $scope.customTopic.customClusterId ? "Custom topic edited successfully." : "Custom topic saved successfully.",
};
$scope.resetCustomTopicForm(form);
if (angular.isFunction($scope.getReportData)) {
$scope.getReportData();
}
})
.catch(function (error) {
var message = error && error.data && error.data.message ? error.data.message : "Unable to save custom topic at this time.";
$scope.customTopicMessages = {
error: message,
};
})
.finally(function () {
$scope.customTopicSubmitting = false;
});
};
$scope.fnEditCustomCluster = function (cluster) {
// FIRST: Set the type so the form shows the right section
$scope.customTopic.type = cluster.type || "default";
$scope.customTopic.displayName = cluster.cluster_name;
$scope.customTopic.customClusterId = cluster.custom_cluster_id;
// Load example keywords if they exist
if (cluster.example_keywords && Array.isArray(cluster.example_keywords)) {
var keywordStrings = cluster.example_keywords.map(function (kw) {
return typeof kw === "object" && kw.keyword ? kw.keyword : typeof kw === "string" ? kw : "";
});
while (keywordStrings.length < 3) {
keywordStrings.push("");
}
$scope.customTopic.example_keywords = keywordStrings;
}
// Load exact keywords if they exist
if (cluster.exact_keywords && Array.isArray(cluster.exact_keywords)) {
var exactKeywordStrings = cluster.exact_keywords.map(function (ek) {
return typeof ek === "object" && ek.term ? ek.term : typeof ek === "string" ? ek : "";
});
while (exactKeywordStrings.length < 3) {
exactKeywordStrings.push("");
}
$scope.customTopic.exact_keywords = exactKeywordStrings;
} else {
$scope.customTopic.exact_keywords = ["", "", ""];
}
// Load boost terms if they exist
if (cluster.boost_terms && Array.isArray(cluster.boost_terms)) {
var boostTermStrings = cluster.boost_terms.map(function (bt) {
return typeof bt === "object" && bt.term ? bt.term : typeof bt === "string" ? bt : "";
});
while (boostTermStrings.length < 2) {
boostTermStrings.push("");
}
$scope.customTopic.boost_terms = boostTermStrings;
} else {
$scope.customTopic.boost_terms = ["", ""];
}
// Scroll to form
document.querySelector('[name="customTopicForm"]').scrollIntoView({ behavior: "smooth" });
};
$scope.fnAddKeywordField = function () {
if (!$scope.customTopic.example_keywords) {
$scope.customTopic.example_keywords = ["", "", ""];
}
$scope.customTopic.example_keywords.push("");
};
$scope.fnAddBoostTermField = function () {
if (!$scope.customTopic.boost_terms) {
$scope.customTopic.boost_terms = ["", "", ""];
}
$scope.customTopic.boost_terms.push("");
};
$scope.fnRemoveBoostTerm = function (index) {
if ($scope.customTopic.boost_terms && index > 1 && index < $scope.customTopic.boost_terms.length) {
$scope.customTopic.boost_terms.splice(index, 1);
}
};
$scope.fnAddExactKeywordField = function () {
if (!$scope.customTopic.exact_keywords) {
$scope.customTopic.exact_keywords = ["", "", ""];
}
$scope.customTopic.exact_keywords.push("");
};
$scope.fnRemoveExactKeyword = function (index) {
if ($scope.customTopic.exact_keywords && index > 2 && index < $scope.customTopic.exact_keywords.length) {
$scope.customTopic.exact_keywords.splice(index, 1);
}
};
$scope.fnRemoveKeyword = function (index) {
if ($scope.customTopic.example_keywords && index > 2 && index < $scope.customTopic.example_keywords.length) {
$scope.customTopic.example_keywords.splice(index, 1);
}
};
$scope.range = function (start, end) {
var result = [];
for (var i = start; i < end; i++) {
result.push(i);
}
return result;
};
$scope.fnDeleteCustomCluster = function (cluster) {
if (!confirm("Are you sure you want to delete this custom topic cluster? This action cannot be undone.")) {
return;
}
$http
.post("/rest/searchconsole/cluster_custom_remove?websiteid=" + h.siteId(), {
custom_cluster_id: cluster.custom_cluster_id,
})
.then(function (response) {
console.log("Cluster deleted successfully");
if (angular.isFunction($scope.getReportData)) {
$scope.getReportData();
}
})
.catch(function (error) {
var message = error && error.data && error.data.message ? error.data.message : "Unable to delete custom topic at this time.";
alert("Error: " + message);
});
};
$scope.fnMoveKeywordToCluster = function (keyword, targetCluster) {
$http
.post("/rest/searchconsole/cluster_custom_move_keyword?websiteid=" + h.siteId(), {
keyword: keyword.query,
from_cluster_id: $scope.currentCluster.custom_cluster_id,
to_cluster_id: targetCluster.custom_cluster_id,
})
.then(function (response) {
console.log("Keyword moved successfully");
$scope.keywordMoveDropdown = null;
if (angular.isFunction($scope.getReportData)) {
$scope.getReportData();
}
})
.catch(function (error) {
var message = error && error.data && error.data.message ? error.data.message : "Unable to move keyword at this time.";
alert("Error: " + message);
});
};
$scope.fnToggleStarKeyword = function (keyword) {
var isStarred = $scope.starredKeywords.has(keyword.query);
var action = isStarred ? "unstar" : "star";
$scope.starringKeywords.add(keyword.query);
$http
.post("/rest/searchconsole/star?websiteid=" + h.siteId(), {
keyword: keyword.query,
action: action,
})
.then(function (response) {
$scope.starringKeywords.delete(keyword.query);
// ensure array exists
if (!$scope.analytics.second.data.starred) {
$scope.analytics.second.data.starred = [];
}
if (isStarred) {
$scope.starredKeywords.delete(keyword.query);
var index = $scope.analytics.second.data.starred.indexOf(keyword.query);
if (index > -1) {
$scope.analytics.second.data.starred.splice(index, 1);
}
} else {
$scope.analytics.second.data.starred.push(keyword.query);
$scope.starredKeywords.add(keyword.query);
}
})
.catch(function (error) {
$scope.starringKeywords.delete(keyword.query);
var message = error && error.data && error.data.message ? error.data.message : "Unable to star keyword at this time.";
alert("Error: " + message);
});
};
$scope.fnIsKeywordStarred = function (keyword) {
return $scope.starredKeywords.has(keyword.query);
};
$scope.starForm = { keyword: "" };
$scope.fnAddStarredKeywords = function () {
var raw = ($scope.starForm.keyword || "");
var lines = raw.split("\n");
var seen = {};
var keywords = [];
for (var i = 0; i < lines.length; i++) {
var kw = lines[i].trim().toLowerCase();
if (!kw || kw.length <= 2) continue;
if (seen[kw]) continue;
seen[kw] = true;
// Skip if already tracked
var exists = $scope.gscTrackerKeywords.some(function (t) { return t.keyword === kw; });
if (exists) continue;
keywords.push(kw);
}
if (keywords.length === 0) {
alert("No new keywords to add. Keywords must be longer than 2 characters and not already tracked.");
return;
}
$scope.importSource = "adding_keywords";
$scope.importing = true;
$scope.importProgress = 0;
$scope.importTotal = keywords.length;
var processKeyword = function (index) {
if (index >= keywords.length) {
$scope.importing = false;
$scope.starForm.keyword = "";
$scope.getReportData();
return;
}
$http.post("/rest/searchconsole/star?websiteid=" + h.siteId(), {
keyword: keywords[index],
action: "star"
}).then(function () {
$scope.importProgress++;
processKeyword(index + 1);
}, function () {
$scope.importProgress++;
processKeyword(index + 1);
});
};
processKeyword(0);
};
$scope.fnRemoveGscKeyword = function (kw, $event) {
if ($event) $event.stopPropagation();
if (!confirm('Remove "' + kw.keyword + '" from tracking?')) return;
$http
.post("/rest/searchconsole/star?websiteid=" + h.siteId(), {
keyword: kw.keyword,
action: "unstar",
})
.then(function () {
$scope.getReportData();
});
};
$scope.fnImportRankTracker = function () {
if (!confirm('Import keywords from legacy Rank Tracker? This may take a while.')) return;
$scope.importSource = "importing_ranktracker";
$scope.importing = true;
$scope.importProgress = 0;
$scope.importTotal = 0;
$http.get("/rest/searchconsole/import_ranktracker?websiteid=" + h.siteId())
.then(function (res) {
var keywords = res.data.keywords;
if (!keywords || keywords.length === 0) {
alert("No new keywords found to import.");
$scope.importing = false;
return;
}
$scope.importTotal = keywords.length;
var processKeyword = function(index) {
if(index >= keywords.length) {
$scope.importing = false;
$scope.getReportData(); // Refresh list
alert("Import completed!");
return;
}
var kw = keywords[index];
// Handle both string (old script) and object (new script) formats for robustness
var keyword = (typeof kw === 'object') ? kw.keyword : kw;
var group = (typeof kw === 'object') ? kw.group : '';
$http.post("/rest/searchconsole/star?websiteid=" + h.siteId(), {
keyword: keyword,
group: group,
action: "star"
}).then(function() {
$scope.importProgress++;
processKeyword(index + 1);
}, function() {
// Even if error, continue
$scope.importProgress++;
processKeyword(index + 1);
});
};
processKeyword(0);
});
};
$scope.fnToggleGscDetail = function (kw) {
$scope.gscDetailOpen[kw.keyword] = !$scope.gscDetailOpen[kw.keyword];
if ($scope.gscDetailOpen[kw.keyword] && kw.history && kw.history.length > 0 && !kw.historyChartId) {
$scope.fnBuildGscChart(kw);
}
};
$scope.fnSetGscHistoryMetric = function (metric, kw) {
$scope.gscHistoryMetric = metric;
if (kw && kw.history && kw.history.length > 0) {
$scope.fnBuildGscChart(kw);
}
};
$scope.fnBuildGscChart = function (kw) {
if (!kw || !kw.history || kw.history.length === 0) return;
var metric = $scope.gscHistoryMetric || "clicks";
// Merge daily and monthly data
var combinedData = [];
var dataMap = {};
if (kw.history) {
kw.history.forEach(function(item) {
var d = item.date;
if (!dataMap[d]) {
dataMap[d] = angular.copy(item);
combinedData.push(dataMap[d]);
}
});
}
if (kw.history_monthly) {
kw.history_monthly.forEach(function(item) {
var d = item.date; // Should be YYYY-MM-01
if (!dataMap[d]) {
dataMap[d] = { date: d };
combinedData.push(dataMap[d]);
}
// Add monthly metric with prefix
dataMap[d]['monthly_' + metric] = item[metric];
});
}
var sortedData = combinedData.sort(function (a, b) {
return new Date(a.date) - new Date(b.date);
});
var chartId = "gsc_" + kw.keyword.replace(/\s+/g, "_").toLowerCase();
$scope.pagecharts = $scope.pagecharts || {};
var graphs = [{
balloonText: "Daily " + metric + ": [[value]] ",
bullet: "round", bulletBorderAlpha: 1, bulletColor: "#FFFFFF",
useLineColorForBulletBorder: true, fillAlphas: 0.2,
lineThickness: 2, lineAlpha: 0.5, bulletSize: 4,
connect: true, type: "smoothedLine",
title: "Daily " + metric, valueField: metric, lineColor: "#3F51B5"
}];
// Add monthly graph if data exists
if (kw.history_monthly && kw.history_monthly.length > 0) {
graphs.push({
balloonText: "Monthly " + metric + ": [[value]] ",
bullet: "square", bulletBorderAlpha: 1, bulletColor: "#FFFFFF",
useLineColorForBulletBorder: true, fillAlphas: 0,
lineThickness: 3, lineAlpha: 1, bulletSize: 6,
connect: true, type: "smoothedLine",
title: "Monthly " + metric, valueField: 'monthly_' + metric, lineColor: "#FF9800"
});
}
$scope.pagecharts[chartId] = {
data: sortedData,
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: graphs,
legend: { useGraphSettings: true, position: "top" }
};
kw.historyChartId = chartId;
if (!$scope.$$phase) {
$scope.$apply();
}
};
$scope.topLandingPages = [];
$scope.keywordDetailState = {};
$scope.checkAccessToken = function (data) {
if (data.action == "connect") {
$scope.modaloverride = true;
$scope.showConnectNudge = true;
}
$scope.cfg.urlbase = "searchconsole";
};
$scope.getClusterPendingState = function (data) {
if (data?.data?.status && data?.data?.status !== "completed") {
$scope.startClusterPolling();
}
// if($scope.clusterValueKey === "none") {
// $scope.hasRevenueContribution = false;
// }
// check if we have custom clusters
var hasCustomClusters = data?.data?.custom_cluster_metrics && data.data.custom_cluster_metrics.length > 0;
if (hasCustomClusters) {
$scope.topicMode = "custom";
}
__getRevenueInfo(data);
};
// --- GSC Tracker ---
$scope.gscTrackerKeywords = [];
$scope.gscDetailOpen = {};
$scope.gscHistoryMetric = "position";
$scope.gscSearch = { keyword: "" };
$scope.compareMode = 'mom';
$scope.fnSetCompareMode = function(mode) { $scope.compareMode = mode; };
// Country filter
$scope.gscCountry = null;
$scope.gscCountryOptions = [
{ code: 'nld', name: 'Netherlands' },
{ code: 'bel', name: 'Belgium' },
{ code: 'deu', name: 'Germany' },
{ code: 'gbr', name: 'United Kingdom' },
{ code: 'usa', name: 'United States' },
{ code: 'fra', name: 'France' },
{ code: 'esp', name: 'Spain' },
{ code: 'ita', name: 'Italy' },
{ code: 'aut', name: 'Austria' },
{ code: 'che', name: 'Switzerland' },
{ code: 'pol', name: 'Poland' },
{ code: 'swe', name: 'Sweden' },
{ code: 'nor', name: 'Norway' },
{ code: 'dnk', name: 'Denmark' },
{ code: 'fin', name: 'Finland' },
{ code: 'prt', name: 'Portugal' },
{ code: 'irl', name: 'Ireland' },
{ code: 'can', name: 'Canada' },
{ code: 'aus', name: 'Australia' },
{ code: 'bra', name: 'Brazil' },
{ code: 'ind', name: 'India' },
{ code: 'jpn', name: 'Japan' },
];
$scope.fnSetGscCountry = function(country) {
$scope.gscCountry = country;
$scope.fnRefreshGscTracker();
};
$scope.fnRefreshGscTracker = function() {
if ($scope.gscCountry) {
// Country-filtered: call dedicated endpoint with live GSC API
$http.get('/rest/searchconsole/gsctracker_country', {
params: {
websiteid: h.siteId(),
country: $scope.gscCountry.code,
start: $rootScope.reportdates.start,
end: $rootScope.reportdates.end
}
}).then(function(res) {
$scope.getStarredKeywords(res.data);
});
} else {
// No country: use standard coreapi fetch (reads from stored history)
$scope.getReportData();
}
};
// Callback for core API when data loads
$scope.getStarredKeywords = function (data) {
$scope.checkAccessToken(data);
$scope.gscTrackerKeywords = (data && data.data) ? data.data : [];
$scope.fnGroupKeywords();
// Visibility chart
if (data && data.visibility_history && data.visibility_history.length) {
$scope.fnBuildVisibilityChart(data.visibility_history);
}
};
$scope.fnBuildVisibilityChart = function(history) {
var sorted = history.slice().sort(function(a, b) { return a.date.localeCompare(b.date); });
$scope.visibilityChart = {
type: 'serial',
data: sorted,
categoryField: 'date',
dataDateFormat: 'YYYY-MM-DD',
categoryAxis: { parseDates: true, minPeriod: 'DD' },
graphs: [{
valueField: 'score',
type: 'smoothedLine',
lineColor: '#3b82f6',
fillAlphas: 0.1,
lineThickness: 2,
balloonText: '[[category]]: [[value]]'
}],
valueAxes: [{ minimum: 0, maximum: 100, title: 'Visibility Score' }],
chartCursor: { cursorAlpha: 0.1 }
};
};
$scope.fnGroupKeywords = function() {
if(!$scope.gscTrackerKeywords) return;
var groups = {};
// Initialize 'Ungrouped'
groups[''] = { name: 'Ungrouped', keywords: [] };
$scope.gscTrackerKeywords.forEach(function(kw) {
var groupName = kw.group || '';
var groupKey = groupName.toLowerCase();
if(!groups[groupKey]) {
groups[groupKey] = { name: groupName, keywords: [] };
}
groups[groupKey].keywords.push(kw);
});
// Convert to array for sorting, put Ungrouped last or first?
// Let's sort alphabetically, with Ungrouped at the bottom if empty?
// Actually, let's just convert to array.
$scope.gscTrackerGroups = [];
Object.keys(groups).forEach(function(key) {
if(groups[key].keywords.length > 0) {
$scope.gscTrackerGroups.push(groups[key]);
}
});
// Sort: named groups first, then ungrouped? Or just alpha.
$scope.gscTrackerGroups.sort(function(a, b) {
if(a.name === 'Ungrouped') return 1;
if(b.name === 'Ungrouped') return -1;
return a.name.localeCompare(b.name);
});
};
$scope.fnUpdateKeywordGroup = function (keyword) {
if (!keyword || !keyword.keyword) return;
$http.post("/rest/searchconsole/gsctracker_update?websiteid=" + h.siteId(), {
keyword: keyword.keyword,
group: keyword.group
}).then(function() {
keyword.isEditing = false;
// Re-group after update
$scope.fnGroupKeywords();
});
};
$scope.getFilteredCluster = function (data) {
// Load starred keywords from the data
$scope.starredKeywords = new Set();
if (data && data.data && Array.isArray(data.data.starred)) {
data.data.starred.forEach(function (keyword) {
$scope.starredKeywords.add(keyword);
});
}
if ($scope.urlfilter && data?.data?.cluster_metrics) {
// ic cluster starts with custom
if ($scope.urlfilter.startsWith("custom_")) {
$scope.currentCluster = data.data.custom_cluster_metrics.filter((c) => c.custom_cluster_id == $scope.urlfilter)?.[0];
$scope.currentKeywords = data.data.keywords?.filter((k) => k.custom_cluster_id == $scope.urlfilter) || [];
} else {
$scope.currentCluster = data.data.cluster_metrics.filter((c) => c.cluster_id == $scope.urlfilter)?.[0];
$scope.currentKeywords = data.data.keywords?.filter((k) => k.cluster_id == $scope.urlfilter) || [];
}
$scope.topLandingPages = $scope.buildTopLandingPages($scope.currentKeywords);
$scope.keywordDetailState = {};
}
__getRevenueInfo(data);
// Hide revenue contribution on cluster detail view
$scope.selectedValueFields = { value: null, percent: null };
};
var __getRevenueInfo = function (data) {
// check if we have a Revenue Contribution anywhere?
var hasRevenueContribution = false;
if (data?.data?.cluster_metrics) {
//hasRevenueContribution = data.data.cluster_metrics.some((metric) => metric.estimated_value_percent !== undefined && metric.estimated_value_percent > 0);
}
$scope.clusterValue.key = data?.data?.clusterValueKey ? data.data.clusterValueKey : "none";
// Map the selected cluster value key to the appropriate metric fields
var valueFieldMap = {
"ecommerce-value": { value: "estimated_ecommerce_value", percent: "estimated_ecommerce_value_percent" },
"goal-value": { value: "estimated_goal_value", percent: "estimated_goal_value_percent" },
"goal-number": { value: "estimated_goal_number", percent: "estimated_goal_number_percent" },
none: { value: null, percent: null },
};
$scope.selectedValueFields = valueFieldMap[$scope.clusterValue.key] || valueFieldMap["none"];
// re-check hasRevenueContribution based on selected cluster value key
// hasRevenueContribution = data.data.cluster_metrics.some((metric) => metric[$scope.selectedValueFields.value] !== undefined && metric[$scope.selectedValueFields.value] > 0);
$scope.clusterValue.hasValue = hasRevenueContribution;
/* OVERRIDE FOR NOW */
$scope.clusterValue.hasValue = false;
if (data.data?.custom_cluster_metrics_needsrefresh == true) {
$scope.clusterNeedsRefresh = true;
} else {
$scope.clusterNeedsRefresh = false;
}
};
$scope.buildTopLandingPages = function (keywords) {
if (!angular.isArray(keywords) || !keywords.length) {
return [];
}
var aggregated = {};
angular.forEach(keywords, function (keyword) {
if (!keyword || !angular.isArray(keyword.urls)) {
return;
}
angular.forEach(keyword.urls, function (urlInfo) {
if (!urlInfo || !urlInfo.url) {
return;
}
var key = urlInfo.url;
if (!aggregated[key]) {
aggregated[key] = {
url: urlInfo.url,
clicks: 0,
estimatedValue: 0,
keywords: [],
};
}
aggregated[key].clicks += urlInfo.clicks || 0;
aggregated[key].estimatedValue += urlInfo.estimatedValue || 0;
if (keyword.query && aggregated[key].keywords.indexOf(keyword.query) === -1) {
aggregated[key].keywords.push(keyword.query);
}
});
});
var rows = [];
angular.forEach(aggregated, function (item) {
rows.push({
url: item.url,
clicks: item.clicks,
estimatedValue: item.estimatedValue,
keywordCount: item.keywords.length,
topKeyword: item.keywords[0] || "",
});
});
rows.sort(function (a, b) {
return (b.estimatedValue || 0) - (a.estimatedValue || 0);
});
return rows.slice(0, 20);
};
$scope.fnExportCSV = function () {
var data = [];
var filename = "export.csv";
var headers = [];
if ($scope.topicMode === "smart") {
data = $scope.analytics.second.data.cluster_metrics;
headers = ["Cluster Name", "Keyword Count", "Position", "Position Change", "Position Change %", "Clicks", "Clicks Change", "Clicks Change %", "Impressions", "CTR"];
filename = "smart_clusters.csv";
} else if ($scope.topicMode === "custom") {
data = $scope.analytics.second.data.custom_cluster_metrics;
headers = ["Cluster Name", "Keyword Count", "Position", "Position Change", "Position Change %", "Clicks", "Clicks Change", "Clicks Change %", "Impressions", "CTR"];
filename = "custom_clusters.csv";
} else if ($scope.topicMode === "keywords" || $scope.topicMode === "starred") {
data = $scope.currentKeywords;
headers = ["Keyword", "Position", "Position Change", "Position Change %", "Clicks", "Clicks Change", "Clicks Change %", "Impressions", "CTR", "URL"];
filename = ($scope.topicMode === "starred" ? "starred_" : "") + "keywords.csv";
}
if (!data || data.length === 0) {
alert("No data to export");
return;
}
var csvContent = headers.join(",") + "\n";
data.forEach(function (row) {
var line = [];
// Calculate Changes
var posCurrent = row.metrics_current?.position || 0;
var posPrev = row.metrics_previous?.position || 0;
var posChange = posPrev > 0 ? posCurrent - posPrev : 0;
var posChangePct = posPrev > 0 ? (posCurrent - posPrev) / posPrev * 100 : 0;
var clicksCurrent = row.metrics_current?.clicks || 0;
var clicksPrev = row.metrics_previous?.clicks || 0;
var clicksChange = clicksCurrent - clicksPrev;
var clicksChangePct = clicksPrev > 0 ? (clicksCurrent - clicksPrev) / clicksPrev * 100 : 0;
if ($scope.topicMode === "smart" || $scope.topicMode === "custom") {
line.push('"' + (row.cluster_name || "").replace(/"/g, '""') + '"');
line.push(row.keyword_count || 0);
line.push(posCurrent.toFixed(1));
line.push(posChange.toFixed(1));
line.push(posChangePct.toFixed(1) + "%");
line.push(clicksCurrent);
line.push(clicksChange);
line.push(clicksChangePct.toFixed(1) + "%");
line.push(row.metrics_current?.impressions || 0);
line.push((row.metrics_current?.ctr * 100).toFixed(2) + "%");
} else {
line.push('"' + (row.query || "").replace(/"/g, '""') + '"');
line.push(posCurrent.toFixed(1));
line.push(posChange.toFixed(1));
line.push(posChangePct.toFixed(1) + "%");
line.push(clicksCurrent);
line.push(clicksChange);
line.push(clicksChangePct.toFixed(1) + "%");
line.push(row.metrics_current?.impressions || 0);
line.push((row.metrics_current?.ctr * 100).toFixed(2) + "%");
var urls = row.urls ? row.urls.map(u => u.url).join(";") : "";
line.push('"' + urls.replace(/"/g, '""') + '"');
}
csvContent += line.join(",") + "\n";
});
var blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
var link = document.createElement("a");
if (link.download !== undefined) {
var url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", filename);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
$scope.fnToggleKeywordDetails = function (keyword, $event) {
if ($event) {
$event.stopPropagation();
}
if (!keyword) {
return;
}
var key = keyword.query || keyword._id || keyword.keyword_id;
if (!key) {
return;
}
$scope.keywordDetailState[key] = !$scope.keywordDetailState[key];
// If opening details and history not already loaded, fetch it
if ($scope.keywordDetailState[key] && (!keyword.history || keyword.history.length === 0)) {
var reportUrl =
"/rest/hub/report?context=searchconsole&dimension=date&end=TODAY&metrics=position,clicks,ctr,impressions&report=searchAnalytics&start=1YEARAGO&websiteid=" +
h.siteId() +
"&multi_dimension=page_date&query=" +
encodeURIComponent(keyword.query) +
"&filterdimension=query&filterexpression=" +
encodeURIComponent(keyword.query) +
"&filteroperator=equals";
$http
.get(reportUrl)
.then(function (response) {
if (response.data && response.data.data && Array.isArray(response.data.data)) {
// If the data includes URL breakdowns, process it
var rawData = response.data.data;
// Check if data includes page/URL information
var hasUrlData = rawData.length > 0 && rawData[0].page;
if (hasUrlData) {
// Process data to aggregate by date and page/URL
var dataByDateAndUrl = {};
rawData.forEach(function (item) {
var dateKey = item.date;
if (!dataByDateAndUrl[dateKey]) {
dataByDateAndUrl[dateKey] = {
date: item.date,
};
}
// Store metrics by page/URL
var urlKey = "page_" + item.page;
if (!dataByDateAndUrl[dateKey][urlKey]) {
dataByDateAndUrl[dateKey][urlKey] = {};
}
dataByDateAndUrl[dateKey][urlKey] = {
clicks: item.clicks || 0,
impressions: item.impressions || 0,
ctr: item.ctr || 0,
position: item.position || 0,
};
});
// Convert back to array and add URL-specific metric fields
keyword.history = Object.keys(dataByDateAndUrl).map(function (dateKey) {
var dateData = dataByDateAndUrl[dateKey];
var aggregated = {
date: dateData.date,
clicks: 0,
impressions: 0,
ctr: 0,
position: 0,
urls: {},
};
// Aggregate and store URL-specific data
var urlIndex = 0;
Object.keys(dateData).forEach(function (key) {
if (key !== "date") {
var url = key.replace("page_", "");
aggregated.urls[url] = dateData[key];
// Also create flat fields for chart: url_0_clicks, url_1_clicks, etc.
var urlNum = urlIndex;
aggregated["url_" + urlNum + "_clicks"] = dateData[key].clicks;
aggregated["url_" + urlNum + "_impressions"] = dateData[key].impressions;
aggregated["url_" + urlNum + "_ctr"] = dateData[key].ctr;
aggregated["url_" + urlNum + "_position"] = dateData[key].position;
urlIndex++;
}
});
// Aggregate totals
Object.keys(dateData).forEach(function (key) {
if (key !== "date") {
aggregated.clicks += dateData[key].clicks || 0;
aggregated.impressions += dateData[key].impressions || 0;
aggregated.ctr = Math.max(aggregated.ctr, dateData[key].ctr || 0);
aggregated.position = Math.min(aggregated.position || 999, dateData[key].position || 999);
}
});
return aggregated;
});
} else {
// No URL data from API - use aggregated data but create per-URL fields
// This allows displaying separate lines for each URL tracking the same keyword performance
// Get unique URLs from keyword
var urls = [];
if (keyword.urls && Array.isArray(keyword.urls)) {
urls = keyword.urls.map(function (urlEntry) {
return urlEntry.url || urlEntry;
});
}
// Add URL-specific fields to each data point
keyword.history = rawData.map(function (item) {
var processed = {
date: item.date,
clicks: item.clicks || 0,
impressions: item.impressions || 0,
ctr: item.ctr || 0,
position: item.position || 0,
};
// Create per-URL metric fields - distribute data across URLs proportionally or equally
urls.forEach(function (url, index) {
// For now, assign the same metrics to each URL as a baseline
// You could modify this to distribute clicks proportionally if you have URL-level info
processed["url_" + index + "_clicks"] = item.clicks || 0;
processed["url_" + index + "_impressions"] = item.impressions || 0;
processed["url_" + index + "_ctr"] = item.ctr || 0;
processed["url_" + index + "_position"] = item.position || 0;
});
return processed;
});
}
// Build chart for the currently selected metric
$scope.buildKeywordHistoryChart(keyword);
} else {
}
})
.catch(function (error) {
keyword.history = [];
});
} else if ($scope.keywordDetailState[key] && keyword.history && keyword.history.length > 0) {
// If history is already loaded and details are being opened, rebuild chart (in case metric changed)
$scope.buildKeywordHistoryChart(keyword);
}
};
$scope.keywordHistoryMetric = "clicks"; // Default metric
$scope.fnSetKeywordHistoryMetric = function (metric) {
if (metric && metric !== $scope.keywordHistoryMetric) {
$scope.keywordHistoryMetric = metric;
// Rebuild all open keyword charts with the new metric
if ($scope.analytics && $scope.analytics.second && $scope.analytics.second.data && Array.isArray($scope.analytics.second.data.keywords)) {
$scope.analytics.second.data.keywords.forEach(function (keyword) {
if (keyword.history && keyword.history.length > 0 && $scope.fnKeywordIsOpen(keyword)) {
$scope.buildKeywordHistoryChart(keyword);
}
});
}
}
};
$scope.fnKeywordIsOpen = function (keyword) {
if (!keyword) {
return false;
}
var key = keyword.query || keyword._id || keyword.keyword_id;
if (!key) {
return false;
}
return !!$scope.keywordDetailState[key];
};
$scope.buildKeywordHistoryChart = function (keyword) {
if (!keyword || !keyword.history || !Array.isArray(keyword.history) || keyword.history.length === 0) {
return;
}
// Get the currently selected keyword history metric, default to 'clicks'
var metric = $scope.keywordHistoryMetric || "clicks";
// Sort data by date
var sortedData = keyword.history.slice().sort(function (a, b) {
return new Date(a.date) - new Date(b.date);
});
// Create unique chart ID based on keyword
var chartId = "keyword_history_" + (keyword.query || keyword._id || "").replace(/\s+/g, "_").toLowerCase();
// Create analytics structure for the chart function to use
$scope.analytics = $scope.analytics || {};
$scope.analytics.keywordHistory = {
data: sortedData,
};
// Initialize pagecharts if needed
$scope.pagecharts = $scope.pagecharts || {};
// Format the date strings properly for AmCharts
var datesubstr = $scope.cfg.dateformat == "YYYYMMDD" ? [0, 4, 4, 6, 6, 8] : [0, 4, 5, 7, 8, 10];
// Get unique URLs from the keyword
var urls = [];
if (keyword.urls && Array.isArray(keyword.urls)) {
urls = keyword.urls.map(function (urlEntry) {
return urlEntry.url || urlEntry;
});
}
// Color palette for different lines
var colorPalette = [
"#3F51B5", // Indigo
"#FF6B35", // Orange-Red
"#4ECDC4", // Teal
"#F7B731", // Yellow-Orange
"#5F27CD", // Purple
"#00D2D3", // Cyan
"#FF6348", // Coral
"#95E1D3", // Mint
"#38A3A5", // Dark Teal
"#C73E1D", // Dark Orange
];
// Build graphs array - one for each URL
var graphs = [];
if (urls.length > 0) {
// Multi-URL mode: create a graph for each URL
urls.forEach(function (url, index) {
var fieldName = "url_" + index + "_" + metric;
graphs.push({
balloonText: url + " " + (metric.charAt(0).toUpperCase() + metric.slice(1)) + ": [[value]] ",
bullet: "round",
bulletBorderAlpha: 1,
bulletColor: "#FFFFFF",
useLineColorForBulletBorder: true,
fillAlphas: 0.1,
lineThickness: 2,
lineAlpha: 1,
bulletSize: 5,
connect: false,
type: "smoothedLine",
title: url,
valueField: fieldName,
lineColor: colorPalette[index % colorPalette.length],
});
});
} else {
// Single line mode (fallback if no URLs)
graphs.push({
balloonText: metric.charAt(0).toUpperCase() + metric.slice(1) + " [[value]] ",
bullet: "round",
bulletBorderAlpha: 1,
bulletColor: "#FFFFFF",
useLineColorForBulletBorder: true,
fillAlphas: 0.2,
lineThickness: 2,
lineAlpha: 1,
bulletSize: 7,
connect: true,
type: "smoothedLine",
title: metric.charAt(0).toUpperCase() + metric.slice(1),
valueField: metric,
lineColor: "#3F51B5",
});
}
// Build the AmCharts configuration matching the exact structure from fnLineChart
$scope.pagecharts[chartId] = {
data: sortedData,
type: "serial",
theme: "light",
marginLeft: 0,
marginRight: 0,
marginTop: 10,
fontFamily: "Roboto",
categoryField: "date",
rotate: false,
pathToImages: "vendor/amchart/images/",
dataDateFormat: $scope.cfg.dateformat,
categoryAxis: {
gridPosition: "start",
parseDates: true,
gridThickness: 0,
},
valueAxes: [
{
position: "left",
axisAlpha: 0,
labelsEnabled: true,
dashLength: 4,
color: "#dedede",
},
],
graphs: graphs,
legend:
urls.length > 1
? {
enabled: true,
position: "top",
marginBottom: 10,
}
: undefined,
};
// Store chart reference on keyword for easy access
keyword.historyChartId = chartId;
// Trigger Angular digest to update the view
if (!$scope.$$phase) {
$scope.$apply();
}
};
$scope.setAnalyticsConfig = function (data) {
$scope.cfg.dateformat = "YYYYMMDD";
};
$scope.reportconfig = {
v1: {
views: {
searchperformance: {
name: "New, Lost and Opprotunities",
view: "searchperformance",
fullchart: true,
/*timeline fallback */
first: { dimension: "date", context: "searchconsole", report: "searchAnalytics", start: "1YEARAGO", callback: $scope.checkAccessToken, compare: "PERIOD" },
/* clusters */
second: { dimension: "query", context: "searchconsole", report: "clusters", start: "60DAYSAGO", end: "TODAY", limit: $scope.limit, callback: $scope.getClusterPendingState },
/* panels */
third: { dimension: "date", context: "searchconsole", report: "searchAnalytics", compare: "PERIOD" },
/* branded timeline */
fourth: { dimension: "date", context: "searchconsole", report: "clusters_branded_timeline", dimension2: "label" },
selectedmetric: "clicks",
metrics: ["position", "clicks", "ctr", "impressions"],
hide: false,
panels: { dataset: "third" },
selectedmetrics: { clicks: true, ctr: true, position: true, impressions: true },
charts: {
clicks: [
{ chart: "c1", name: "Clicks", type: "line", dataset: "first", date: true, stacked: true, active: true },
{ chart: "c2", name: "Branded vs Non Branded Clicks", type: "linemulti", dataset: "fourth", date: true, stacked: true, graphs: ["branded", "non_branded"] },
{ chart: "c3", name: "Click Intentions", type: "linemulti", dataset: "fourth", date: true, stacked: true, graphs: ["informational", "navigational", "transactional", "commercial"] },
],
ctr: [
{ chart: "c1", name: "Ctr", type: "line", dataset: "first", date: true, active: true },
{ chart: "c2", name: "Branded vs Non Branded Ctr", type: "linemulti", dataset: "fourth", date: true, graphs: ["branded", "non_branded"] },
{ chart: "c3", name: "Ctr Intentions", type: "linemulti", dataset: "fourth", date: true, graphs: ["informational", "navigational", "transactional", "commercial"] },
],
position: [
{ chart: "c1", name: "Position", type: "line", dataset: "first", date: true, active: true },
{ chart: "c2", name: "Branded vs Non Branded Position", type: "linemulti", dataset: "fourth", date: true, graphs: ["branded", "non_branded"] },
{ chart: "c3", name: "Position Intentions", type: "linemulti", dataset: "fourth", date: true, graphs: ["informational", "navigational", "transactional", "commercial"] },
],
impressions: [
{ chart: "c1", name: "Impressions", type: "line", dataset: "first", date: true, active: true },
{ chart: "c2", name: "Branded vs Non Branded Impressions", type: "linemulti", dataset: "fourth", date: true, graphs: ["branded", "non_branded"] },
{ chart: "c3", name: "Impressions Intentions", type: "linemulti", dataset: "fourth", date: true, graphs: ["informational", "navigational", "transactional", "commercial"] },
],
},
},
keywordperformance: {
name: "Starred Keywords",
view: "keywordperformance",
hide: false,
second: { dimension: "query", context: "searchconsole", report: "gsctracker_list", callback: $scope.getStarredKeywords },
selectedmetric: "clicks",
selectedmetrics: { clicks: true, ctr: true, position: true, impressions: true },
metrics: ["position", "clicks", "ctr", "impressions"],
},
clusterperformance: {
name: $scope.urlfilter,
hide: true,
view: "clusterperformance",
first: { dimension: "date", context: "searchconsole", report: "searchAnalytics" },
second: { dimension: "query", context: "searchconsole", report: "clusters", start: "60DAYSAGO", end: "TODAY", limit: $scope.limit, callback: $scope.getFilteredCluster },
selectedmetric: "clicks",
selectedmetrics: { clicks: true, ctr: true, position: true, impressions: true },
metrics: ["position", "clicks", "ctr", "impressions"],
// charts: {
// clicks: [{ 'chart': 'c1', 'name': 'Clicks', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Clicks per keyword', type: 'progress', dataset: 'third' }],
// ctr: [{ 'chart': 'c1', 'name': 'Ctr', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Ctr per keyword', type: 'progress', dataset: 'third' }],
// position: [{ 'chart': 'c1', 'name': 'Position', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Position per keyword', type: 'progress', dataset: 'third' }],
// impressions: [{ 'chart': 'c1', 'name': 'Impressions', type: 'line', dataset: 'first', date: true }, { 'chart': 'c2', 'name': 'Impressions per keyword', type: 'progress', dataset: 'third' }],
// },
},
searchengines: {
name: "searchengines",
view: "searchengines",
hide: true,
viewfilters: ["goals"],
first: { context: "analytics", dimension: "date", dimension2: "sessionSource", channel: "Organic Search", callback: $scope.setAnalyticsConfig },
second: { context: "analytics", dimension: "sessionSource", sort: "sessions", sortorder: "DESCENDING", allowcomp: true, channel: "Organic Search", callback: $scope.setAnalyticsConfig },
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: "linemulti", dataset: "first", date: true },
{ chart: "c2", name: "Sessions / channel", type: "pie", dataset: "second" },
],
totalUsers: [
{ chart: "c1", name: "Users", type: "linemulti", dataset: "first", date: true },
{ chart: "c2", name: "Users / channel", type: "pie", dataset: "second" },
],
screenPageViews: [
{ chart: "c1", name: "Pagina weergaven", type: "linemulti", dataset: "first", date: true },
{ chart: "c2", name: "screenPageViews / channel", type: "pie", dataset: "second" },
],
screenPageViewsPerSession: [
{ chart: "c1", name: "Pageviews per session", type: "linemulti", dataset: "first", date: true },
{ chart: "c2", name: "screenPageViewsPerSession per session /channel", type: "bar", dataset: "second" },
],
averageSessionDuration: [
{ chart: "c1", name: "Average session duration", type: "linemulti", dataset: "first", date: true },
{ chart: "c2", name: "_", type: "bar", dataset: "second" },
],
newUsers: [
{ chart: "c1", name: "Percentage new Sessions", type: "linemulti", dataset: "first", date: true },
{ chart: "c2", name: "_", type: "bar", dataset: "second" },
],
bounceRate: [
{ chart: "c1", name: "Bounce ratio", type: "linemulti", dataset: "first", date: true },
{ chart: "c2", name: "_", type: "bar", dataset: "second" },
],
eventCount: [
{ chart: "c1", name: "Goals", type: "linemulti", dataset: "first", date: true },
{ chart: "c2", name: "_", type: "pie", dataset: "second" },
],
eventCountPerUser: [
{ chart: "c1", name: "Goalvalue", type: "linemulti", dataset: "first", date: true },
{ chart: "c2", name: "_", type: "pie", dataset: "second" },
],
},
},
llmtraffic: {
name: "llmtraffic",
view: "llmtraffic",
hide: false,
viewfilters: ["goals"],
first: { context: "analytics", dimension: "date", dimension2: "sessionSource", channel: "custom_llm", start: "6MONTHSAGO", end: "TODAY", callback: $scope.setAnalyticsConfig },
second: { context: "analytics", dimension: "sessionSource", sort: "sessions", sortorder: "DESCENDING", allowcomp: true, channel: "custom_llm", start: "6MONTHSAGO", end: "TODAY", callback: $scope.setAnalyticsConfig },
selectedmetric: "sessions",
selectedmetrics: { sessions: true, totalUsers: true, screenPageViews: true, screenPageViewsPerSession: true, averageSessionDuration: true, newUsers: true, bounceRate: true, eventCount: true },
metrics: ["sessions", "totalUsers", "screenPageViews", "screenPageViewsPerSession", "averageSessionDuration", "newUsers", "bounceRate", "eventCount"],
table: { dataset: "second" },
panels: { dataset: "second" },
charts: {
sessions: [
{ chart: "c1", name: "Sessions", type: "linemulti", stacked: true, dataset: "first", date: true },
{ chart: "c2", name: "Sessions / channel", type: "pie", dataset: "second" },
],
totalUsers: [
{ chart: "c1", name: "Users", type: "linemulti", stacked: true, dataset: "first", date: true },
{ chart: "c2", name: "Users / channel", type: "pie", dataset: "second" },
],
screenPageViews: [
{ chart: "c1", name: "Pagina weergaven", type: "linemulti", stacked: true, dataset: "first", date: true },
{ chart: "c2", name: "screenPageViews / channel", type: "pie", dataset: "second" },
],
screenPageViewsPerSession: [
{ chart: "c1", name: "Pageviews per session", type: "linemulti", stacked: true, dataset: "first", date: true },
{ chart: "c2", name: "screenPageViewsPerSession per session /channel", type: "bar", dataset: "second" },
],
averageSessionDuration: [
{ chart: "c1", name: "Average session duration", type: "linemulti", stacked: true, dataset: "first", date: true },
{ chart: "c2", name: "_", type: "bar", dataset: "second" },
],
newUsers: [
{ chart: "c1", name: "Percentage new Sessions", type: "linemulti", stacked: true, dataset: "first", date: true },
{ chart: "c2", name: "_", type: "bar", dataset: "second" },
],
bounceRate: [
{ chart: "c1", name: "Bounce ratio", type: "linemulti", stacked: true, dataset: "first", date: true },
{ chart: "c2", name: "_", type: "bar", dataset: "second" },
],
eventCount: [
{ chart: "c1", name: "Goals", type: "linemulti", stacked: true, dataset: "first", date: true },
{ chart: "c2", name: "_", type: "pie", dataset: "second" },
],
},
},
aioverview: {
name: "aioverview",
view: "aioverview",
hide: false,
viewfilters: ["goals"],
first: { context: "searchconsole", report: "aioverview", dimension: "date", start: "6MONTHSAGO", end: "TODAY" },
second: { context: "searchconsole", report: "aioverview", dimension: "query", start: "6MONTHSAGO", end: "TODAY" },
selectedmetric: "estimated_clicks",
selectedmetrics: { estimated_clicks: true },
metrics: ["estimated_clicks"],
charts: {
estimated_clicks: [{ chart: "c1", name: "Search Visibility", type: "line", dataset: "first", date: true, hideAxisLabels: true }],
},
},
info_and_roadmap: {
name: "info_and_roadmap",
view: "info_and_roadmap",
hide: false,
viewfilters: ["goals"],
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"],
},
},
},
};
$scope.fnSetLimit = function (limit) {
$scope.view.first.limit = limit;
$scope.getReportData();
};
/* CLustering */
$scope.fnStartClustering = function () {
$scope.analytics.second = { action: "pending", data: { status: "pending" } };
$http.get("/rest/searchconsole/cluster_create?websiteid=" + h.siteId()).then(function (response) {
$scope.getReportData();
});
};
$scope.fnStartCustomClustering = function () {
$scope.clusterNeedsRefresh = false;
$scope.analytics.second = { action: "pending", data: { status: "pending" } };
$http.get("/rest/searchconsole/cluster_create_custom?websiteid=" + h.siteId()).then(function (response) {
// refresh after 5 seconds to allow backend to start processing
$timeout(function () {
$scope.getReportData();
}, 5000);
});
};
$scope.fnUpdateClusterOptions = function () {
$http
.post("/rest/searchconsole/cluster_update_options", {
websiteid: h.siteId(),
clusterValueKey: $scope.clusterValue.key,
})
.then(function (response) {
// Optionally refresh data or show success message
$scope.getReportData();
});
};
// Poll cluster status every 10 seconds when action is pending
$scope.startClusterPolling = function () {
// Prevent multiple polling intervals
if ($scope.clusterPollingInterval) {
$timeout.cancel($scope.clusterPollingInterval);
}
var poll = function () {
$http
.get("/rest/searchconsole/cluster_get_names?websiteid=" + h.siteId())
.then(function (response) {
if (response.data && response.data.data) {
var status = response.data.data.status;
// Ensure analytics.second and its data object exist
$scope.analytics.second = $scope.analytics.second || {};
$scope.analytics.second.data = $scope.analytics.second.data || {};
// Update data on scope
$scope.analytics.second.data.cluster_metrics = response.data.data.cluster_metrics;
$scope.analytics.second.data.status = status;
// Update progress: increment by 5 until 80%, then by 1, capped at 99
if ($scope.clusterProgress < 80) {
$scope.clusterProgress = Math.min($scope.clusterProgress + 5, 80);
} else if ($scope.clusterProgress < 99) {
$scope.clusterProgress = Math.min($scope.clusterProgress + 1, 99);
}
// If status is no longer clustering or pending, stop polling
if (status === "completed") {
$timeout.cancel($scope.clusterPollingInterval);
$scope.clusterPollingInterval = null;
// call getReportData to refresh data
$scope.getReportData();
} else {
// Otherwise, continue polling
$scope.clusterPollingInterval = $timeout(poll, 10000);
}
}
})
.catch(function (error) {
// Stop polling on error to prevent infinite loops
$timeout.cancel($scope.clusterPollingInterval);
});
};
// Start the first poll immediately
poll();
};
$controller("coreapi", { $scope: $scope });
$scope.init();
$scope.table.itemsperpage = 140;
// Start polling immediately if the initial state is pending or clustering
if ($scope.analytics && $scope.analytics.second && ($scope.analytics.second.action === "pending" || ($scope.analytics.second.data && ($scope.analytics.second.data.status === "clustering" || $scope.analytics.second.data.status === "pending")))) {
$scope.startClusterPolling();
}
},
]);
/**
* Calculates a weighted visibility score for a given query.
* @param {object} query - The query object from your database, now including a 'status' field.
* @param {string} userDomain - The domain of the user's site.
* @param {Array} providers - An array of all active providers.
* @returns {object} - An object containing the score, status, and ranks.
*/
function calculateVisibilityScore(query, userDomain, providers) {
// Directly use the status from the API.
if (query.status === 'pending') {
return {
score: 0,
status: 'Pending',
ranks: {}
};
}
// The rest of the logic only runs if the status is 'done'.
const processed = {
score: 0,
status: 'Gap', // Default status for a 'done' query with no visibility
ranks: {}
};
const getPointsForRank = (rank) => {
if (rank === null) return 0;
if (rank === 1) return 10;
if (rank === 2) return 9.5;
if (rank === 3) return 8.5;
if (rank >= 4 && rank <= 5) return 8;
if (rank >= 6 && rank <= 10) return 7;
if (rank >= 11 && rank <= 15) return 6;
return 0;
};
let totalPoints = 0;
const cleanUserDomain = userDomain.replace(/^www\./, '');
let latestHistory = {};
(query.history || []).forEach(entry => {
if (!latestHistory[entry.provider] || new Date(entry.date) > new Date(latestHistory[entry.provider].date)) {
latestHistory[entry.provider] = entry;
}
});
providers.forEach(provider => {
const entry = latestHistory[provider];
let rank = null;
if (entry && entry.urls) {
const foundIndex = entry.urls.findIndex(url => {
try {
const urlDomain = url.replace(/^https?:\/\//, '').split('/')[0].replace(/^www\./, '');
return urlDomain === cleanUserDomain;
} catch (e) { return false; }
});
if (foundIndex !== -1) {
rank = foundIndex + 1;
}
}
processed.ranks[provider] = rank;
totalPoints += getPointsForRank(rank);
});
if (providers.length > 0) {
processed.score = totalPoints / providers.length;
}
if (processed.score >= 7) {
processed.status = 'Leading';
} else if (processed.score >= 3) {
processed.status = 'Visible';
} else if (processed.score > 0) {
processed.status = 'At Risk';
}
return processed;
}
app.controller("llmvisibility", [
"$scope",
"$http",
"$rootScope",
"$filter",
"h",
"$sce",
function ($scope, $http, $rootScope, $filter, h, $sce) {
$scope.queries = [];
$scope.kpis = {
overallVisibility: 0,
queriesLeading: 0,
visibilityTrend: 0, // Placeholder
topCompetitor: "N/A",
contentGaps: 0,
queriesWithMentions: 0, // ++ ADD THIS
};
$scope.page = {
newQuery: "",
filter: {},
sortType: "q",
sortReverse: false,
// New fields for add form
newQueryExtraDomains: '',
newQueryBrands: ''
};
// Add a new query to track (with error handling and multi-query split)
$scope.fnAddQuery = function () {
if (!$scope.page.newQuery || !$scope.page.newQuery.trim()) {
return; // Don't add empty queries
}
if ($scope.page.isOverBudget) {
$scope.page.error = "This query exceeds your available credits. Please adjust the providers or frequency.";
return;
}
// If we're editing an existing query, perform update instead of add
if ($scope.page.editing) {
var updateQ = $scope.page.newQuery.trim();
var extradomains = ($scope.page.newQueryExtraDomains || '').split(/[,\s]+/).map(function (d) { return (d || '').trim(); }).filter(Boolean);
var brands = ($scope.page.newQueryBrands || '').split(/[,\s]+/).map(function (d) { return (d || '').trim(); }).filter(Boolean);
$http.post('/rest/llmvisibility/update', {
_id: $scope.page.editing,
q: updateQ,
websiteid: h.siteId(),
extradomains: extradomains,
brands: brands,
providers: Object.keys($scope.page.newQueryProviders).filter(k => $scope.page.newQueryProviders[k]),
frequency: $scope.page.newQueryFrequency
}).then(function(res) {
$scope.getQueries();
$scope.fnCancelEdit();
}, function(err) {
var msg = (err.data && (err.data.error || err.data.message)) ? (err.data.error || err.data.message) : 'Failed to update query.';
$scope.page.error = msg;
});
return;
}
// Split by comma or newline
var queries = $scope.page.newQuery.split(/[\,\n]+/);
var errorOccurred = false;
$scope.page.error = null;
// Prepare domains and brands
var extradomains = ($scope.page.newQueryExtraDomains || '').split(/[,\s]+/).map(function (d) { return (d || '').trim(); }).filter(Boolean);
var brands = ($scope.page.newQueryBrands || '').split(/[,\s]+/).map(function (d) { return (d || '').trim(); }).filter(Boolean);
var addNext = function (index) {
if (index >= queries.length) {
$scope.fnCancelEdit(); // Reset form on completion
return;
}
var query = queries[index].trim();
if (!query) {
addNext(index + 1);
return;
}
$http.post("/rest/llmvisibility/add", {
q: query,
websiteid: h.siteId(),
extradomains: extradomains,
brands: brands,
providers: Object.keys($scope.page.newQueryProviders).filter(k => $scope.page.newQueryProviders[k]),
frequency: $scope.page.newQueryFrequency
}).then(function successCallback(res) {
$scope.getQueries();
addNext(index + 1);
}, function errorCallback(err) {
var msg = (err.data && (err.data.error || err.data.message)) ? (err.data.error || err.data.message) : 'Failed to add query "' + query + '".';
if (!errorOccurred) {
$scope.page.error = msg;
errorOccurred = true;
}
// Stop on first error
});
};
addNext(0);
};
// Retrieve the list of all queries for the site
$scope.getQueries = function () {
$http
.get("/rest/llmvisibility/list", {
params: {
websiteid: h.siteId(),
},
}) // site_id is handled by backend
.then(function successCallback(res) {
console.log("LLM Visibility Queries:", res.data);
$scope.processData(res.data);
});
};
// Remove a query
$scope.fnRemoveQuery = function (query) {
if (!confirm('Are you sure you want to remove this query?')) return;
$http.post('/rest/llmvisibility/delete', {
_id: query._id
}).then(function () {
$scope.getQueries();
});
};
// // Edit a query (inline prompt -> update API)
// $scope.fnEditQuery = function (query) {
// // Prevent editing pending queries that may be in-flight
// var newQ = prompt('Edit query', query.q);
// if (newQ === null) return; // user cancelled
// newQ = ('' + newQ).trim();
// if (!newQ) return alert('Query cannot be empty.');
// $http.post('/rest/llmvisibility/update', {
// _id: query._id,
// q: newQ
// }).then(function (res) {
// $scope.getQueries();
// }, function (err) {
// var msg = (err.data && (err.data.error || err.data.message)) ? (err.data.error || err.data.message) : 'Failed to update query.';
// alert(msg);
// });
// };
// Edit a query: populate the add form and set editing state
$scope.fnEditQuery = function (query) {
$scope.page.newQuery = query.q || '';
// try multiple possible field names stored on the query object
var existingExtra = query.extradomains || query.extraDomains || query.extra_domains || query.domains || '';
var existingBrands = query.brands || query.brand || '';
if (Array.isArray(existingExtra)) existingExtra = existingExtra.join(', ');
if (Array.isArray(existingBrands)) existingBrands = existingBrands.join(', ');
$scope.page.newQueryExtraDomains = existingExtra || '';
$scope.page.newQueryBrands = existingBrands || '';
// -- Populate new fields for editing --
$scope.page.newQueryFrequency = query.frequency || 'weekly';
// Reset providers, then set the ones from the query
$scope.page.newQueryProviders = { google: false, openai: false, perplexity: false };
if (query.providers && query.providers.length) {
query.providers.forEach(p => {
if ($scope.page.newQueryProviders.hasOwnProperty(p)) {
$scope.page.newQueryProviders[p] = true;
}
});
} else {
// Default if not set
$scope.page.newQueryProviders = { google: true, openai: true, perplexity: true };
}
$scope.page.editing = query._id;
// focus the textarea
setTimeout(function(){ try{ var el = document.querySelector('textarea[ng-model="page.newQuery"]'); if(el) el.focus(); }catch(e){} },50);
};
// Cancel editing and reset the add form
$scope.fnCancelEdit = function() {
$scope.page.editing = null;
$scope.page.newQuery = '';
$scope.page.newQueryExtraDomains = $rootScope.user && $rootScope.user.site ? $rootScope.user.site.domain.replace(/^www\./,'') : '';
$scope.page.newQueryBrands = $rootScope.user && $rootScope.user.site && $rootScope.user.site.brand ? $rootScope.user.site.brand : '';
$scope.page.error = null;
// -- Reset new fields --
$scope.page.newQueryProviders = { google: true, openai: true, perplexity: true };
$scope.page.newQueryFrequency = 'weekly';
};
// Helper: Find the rank (1-based) of the user's domain in a list of URLs
function findRankInUrls(urls, domain) {
if (!Array.isArray(urls) || !domain) return null;
for (var i = 0; i < urls.length; i++) {
try {
var urlDomain = urls[i].replace(/^https?:\/\//, '').split('/')[0].replace(/^www\./, '');
if (urlDomain === domain.replace(/^www\./, '')) {
return i + 1;
}
} catch (e) { }
}
return null;
}
// Helper: Extract domain from a URL
function getDomainFromUrl(url) {
if (!url) return null;
try {
return url.replace(/^https?:\/\//, '').split('/')[0].replace(/^www\./, '');
} catch (e) { return null; }
}
// Main data processing function
$scope.processData = function (data) {
const userDomain = $rootScope.user.site.domain.replace(/^www\./, '');
let totalVisibilityScore = 0;
let leadingQueriesCount = 0;
let contentGapsCount = 0;
let allProvidersMap = {};
// Note: The brand mention logic is kept here to ensure compatibility
// if you have already implemented it.
let queriesWithMentions = 0;
// ++ Variables for trend calculation
let totalPastVisibilityScore = 0;
let trendCalculableQueries = 0;
// Competitor scoring system setup
let competitorPoints = {};
let competitorAppearances = {};
const getPointsForRank = (rank) => {
if (rank === null) return 0;
if (rank === 1) return 10; if (rank === 2) return 8; if (rank === 3) return 7;
if (rank >= 4 && rank <= 5) return 5; if (rank >= 6 && rank <= 10) return 3;
if (rank >= 11 && rank <= 15) return 1; return 0;
};
const getDomainFromUrl = (url) => {
if (!url) return null;
try {
return url.replace(/^https?:\/\//, '').split('/')[0].replace(/^www\./, '');
} catch (e) { return null; }
};
// Determine all unique providers from the data
data.forEach(function (query) {
(query.history || []).forEach(function (entry) {
allProvidersMap[entry.provider] = true;
});
});
const allProviders = Object.keys(allProvidersMap).sort();
$scope.allProviders = allProviders;
// Process each query
let visibleQueriesCount = 0;
data.forEach(function (query) {
// ++ Set defaults for existing queries without provider/frequency info ++
if (!query.providers || query.providers.length === 0) {
query.providers = allProviders;
}
if (!query.frequency) {
query.frequency = 'weekly';
}
query.processed = calculateVisibilityScore(query, userDomain, allProviders);
// Individual query sparkline logic (remains unchanged)
let historyByDate = {};
(query.history || []).forEach(function (entry) {
if (!entry.date) return;
var dateStr = entry.date.substr(0, 10);
if (!historyByDate[dateStr]) historyByDate[dateStr] = [];
var rank = null;
if (entry.urls && entry.urls.length) {
for (var i = 0; i < entry.urls.length; ++i) {
var urlDomain = entry.urls[i].replace(/^https?:\/\//, '').split('/')[0].replace(/^www\./, '');
if (urlDomain === userDomain) { rank = i + 1; break; }
}
}
if (rank !== null) historyByDate[dateStr].push(rank);
});
var trendArr = [];
Object.keys(historyByDate).forEach(function (d) {
var arr = historyByDate[d];
if (arr.length) {
var avg = arr.reduce(function (a, b) { return a + b; }, 0) / arr.length;
trendArr.push({ date: d, rank: avg });
}
});
trendArr.sort(function (a, b) { return a.date.localeCompare(b.date); });
if (trendArr.length > 1) {
var minRank = Math.min.apply(null, trendArr.map(function (t) { return t.rank; }));
var maxRank = Math.max.apply(null, trendArr.map(function (t) { return t.rank; }));
if (minRank === maxRank) { minRank = 1; maxRank = minRank + 1; }
var w = 60, h = 18, n = trendArr.length;
query.trendline = trendArr.map(function (t, i) {
var x = Math.round(i * (w / (n - 1)));
var y = Math.round(h - ((t.rank - minRank) / (maxRank - minRank)) * h);
return x + ',' + y;
}).join(' ');
} else {
query.trendline = '';
}
// ++ START: NEW BRAND MENTION LOGIC ++
query.processed.brandMentions = {}; // Object to store mentions by provider
let queryHasAnyMention = false;
const brandsToTrack = query.brands || [];
if (brandsToTrack.length > 0) {
let latestHistory = {};
(query.history || []).forEach(function (entry) {
if (!latestHistory[entry.provider] || new Date(entry.date) > new Date(latestHistory[entry.provider].date)) {
latestHistory[entry.provider] = entry;
}
});
// Create a case-insensitive regex with word boundaries
const brandRegex = new RegExp('\\b(' + brandsToTrack.join('|') + ')\\b', 'gi');
for (const provider in latestHistory) {
const entry = latestHistory[provider];
if (entry.responseText && entry.responseText.match(brandRegex)) {
query.processed.brandMentions[provider] = true;
queryHasAnyMention = true;
} else {
// Explicitly set to false if no mention is found for a provider with a response
if (entry.responseText) {
query.processed.brandMentions[provider] = false;
}
}
}
}
if (queryHasAnyMention) {
queriesWithMentions++;
}
// ++ END: NEW BRAND MENTION LOGIC ++
// Only count non-pending queries for KPIs and visibility
if (query.status !== 'pending') {
totalVisibilityScore += query.processed.score;
visibleQueriesCount++;
if (query.processed.status === "Leading") leadingQueriesCount++;
else if (query.processed.status === "Gap") contentGapsCount++;
}
// ++ START: Overall Trend Calculation Logic ++
if (query.history && query.history.length > 0) {
let queryHistoryByDate = {};
query.history.forEach(entry => {
const dateStr = entry.date.substr(0, 10);
if (!queryHistoryByDate[dateStr]) queryHistoryByDate[dateStr] = [];
queryHistoryByDate[dateStr].push(entry);
});
const sortedDates = Object.keys(queryHistoryByDate).sort();
if (sortedDates.length > 1) {
const oldestDate = sortedDates[0];
let oldestLatestHistory = {};
queryHistoryByDate[oldestDate].forEach(entry => {
if (!oldestLatestHistory[entry.provider] || new Date(entry.date) > new Date(oldestLatestHistory[entry.provider].date)) {
oldestLatestHistory[entry.provider] = entry;
}
});
const pastQuery = { status: 'done', history: Object.values(oldestLatestHistory) };
const pastResult = calculateVisibilityScore(pastQuery, userDomain, allProviders);
totalPastVisibilityScore += pastResult.score;
trendCalculableQueries++;
}
}
// ++ END: Overall Trend Calculation Logic ++
// Competitor Logic (remains unchanged)
let latestHistory = {};
(query.history || []).forEach(function (entry) {
if (!latestHistory[entry.provider] || new Date(entry.date) > new Date(latestHistory[entry.provider].date)) {
latestHistory[entry.provider] = entry;
}
});
for (const provider in latestHistory) {
const entry = latestHistory[provider];
const seenDomains = new Set();
(entry.urls || []).forEach((url, index) => {
const domain = getDomainFromUrl(url);
if (domain && domain !== userDomain) {
if (!competitorPoints[domain]) {
competitorPoints[domain] = 0;
competitorAppearances[domain] = 0;
}
competitorPoints[domain] += getPointsForRank(index + 1);
if (!seenDomains.has(domain)) {
competitorAppearances[domain]++;
seenDomains.add(domain);
}
}
});
}
});
// Final Competitor Score Calculation (remains unchanged)
let topCompetitor = "N/A";
let maxCompetitorScore = -1;
for (const domain in competitorPoints) {
const avgScore = competitorAppearances[domain] > 0 ? competitorPoints[domain] / competitorAppearances[domain] : 0;
if (avgScore > maxCompetitorScore) {
maxCompetitorScore = avgScore;
topCompetitor = domain;
}
}
// Calculate KPIs
$scope.kpis.overallVisibility = visibleQueriesCount > 0 ? (totalVisibilityScore / visibleQueriesCount) : 0;
$scope.kpis.queriesLeading = leadingQueriesCount;
$scope.kpis.contentGaps = contentGapsCount;
$scope.kpis.topCompetitor = topCompetitor;
$scope.kpis.queriesWithMentions = queriesWithMentions;
// ++ ADD FINAL TREND CALCULATION ++
let visibilityTrend = 0;
if (trendCalculableQueries > 0 && visibleQueriesCount > 0) {
// Use the current average, not the sum for trendable queries
const currentAverage = totalVisibilityScore / visibleQueriesCount;
const pastAverage = totalPastVisibilityScore / trendCalculableQueries;
if (pastAverage > 0) {
visibilityTrend = ((currentAverage - pastAverage) / pastAverage) * 100;
} else if (currentAverage > 0) {
visibilityTrend = 100; // From 0 to something positive is a 100% gain for our purposes
}
}
$scope.kpis.visibilityTrend = visibilityTrend;
$scope.queries = data;
};
// REVISED Toggle for details view
$scope.fnQueryHistory = function (query) {
query.active = !query.active;
// Only process the data if we are opening the row for the first time
if (query.active && !query.details) {
const userDomain = $rootScope.user.site.domain.replace(/^www\./, '');
const brandsToTrack = query.brands || [];
// Setup for text highlighting
const highlightTerms = [userDomain, ...brandsToTrack].filter(Boolean);
const highlightRegex = new RegExp('\\b(' + highlightTerms.join('|') + ')\\b', 'gi');
query.details = {
responses: [],
competitors: []
// The separate brandSummary is no longer needed
};
const getPointsForRank = (rank) => {
if (rank === null) return 0;
if (rank === 1) return 10; if (rank === 2) return 8; if (rank === 3) return 7;
if (rank >= 4 && rank <= 5) return 5; if (rank >= 6 && rank <= 10) return 3;
if (rank >= 11 && rank <= 15) return 1; return 0;
};
let competitorPoints = {};
let providerCount = 0;
// Get the most recent entry for each provider
let latestHistory = {};
(query.history || []).forEach(function (entry) {
if (!latestHistory[entry.provider] || new Date(entry.date) > new Date(latestHistory[entry.provider].date)) {
latestHistory[entry.provider] = entry;
}
});
for (const provider in latestHistory) {
providerCount++;
const entry = latestHistory[provider];
// 1. Prepare LLM Response Text (with highlighting)
if (entry.responseText) {
let highlightedText = entry.responseText.replace(highlightRegex, `$& `);
highlightedText = highlightedText
.replace(/\*\*(.*?)\*\*/g, '$1 ')
.replace(/\*(.*?)\*/g, '$1 ')
.replace(/`([^`]+)`/g, '$1')
.replace(/\[(.*?)\]\((.*?)\)/g, '$1 ')
.replace(/\n{3,}/g, '\n\n');
// ++ START: NEW MENTION DETECTION LOGIC ++
let mentionedBrands = [];
if (brandsToTrack.length > 0) {
brandsToTrack.forEach(brand => {
const brandRegex = new RegExp('\\b' + brand + '\\b', 'gi');
if (brandRegex.test(entry.responseText)) {
mentionedBrands.push(brand);
}
});
}
// ++ END: NEW MENTION DETECTION LOGIC ++
query.details.responses.push({
provider: provider,
text: $sce.trustAsHtml(`${highlightedText} `),
urls: entry.urls || [],
mentionedBrands: mentionedBrands // ++ ATTACH MENTIONED BRANDS
});
}
// 2. Calculate points for each competitor in this provider's list
(entry.urls || []).forEach(function (url, index) {
const domain = getDomainFromUrl(url);
if (domain && domain !== userDomain) {
if (!competitorPoints[domain]) {
competitorPoints[domain] = 0;
}
competitorPoints[domain] += getPointsForRank(index + 1);
}
});
}
// 3. Calculate final score for each competitor and sort
for (const domain in competitorPoints) {
query.details.competitors.push({
domain: domain,
score: providerCount > 0 ? competitorPoints[domain] / providerCount : 0
});
}
query.details.competitors.sort((a, b) => b.score - a.score);
// 4. Name clusters using AJAX calls to /name-single-cluster/
query.details.clusterNames = {};
const nameClusters = async () => {
// Group keywords by cluster for naming
const clustersForNaming = {};
query.details.responses.forEach(response => {
if (response.urls && response.urls.length > 0) {
response.urls.forEach((url, index) => {
const keyword = url.replace(/^https?:\/\//, '').split('/')[0].replace(/^www\./, '');
// For now, we'll use a simple clustering approach or you can integrate with your clustering API
// This is a placeholder - you'll need to implement the actual clustering logic
const clusterId = Math.floor(Math.random() * 5); // Placeholder clustering
if (!clustersForNaming[clusterId]) {
clustersForNaming[clusterId] = [];
}
clustersForNaming[clusterId].push(keyword);
});
}
});
// Make AJAX calls for each cluster
for (const [clusterId, keywords] of Object.entries(clustersForNaming)) {
try {
const response = await $http.post('http://135.125.202.210:8000/name-single-cluster/', {
keywords: keywords
});
query.details.clusterNames[clusterId] = response.data.name || `Cluster ${clusterId}`;
} catch (error) {
console.error('Error naming cluster', clusterId, error);
query.details.clusterNames[clusterId] = `Cluster ${clusterId}`;
}
}
// Trigger digest cycle to update UI
$scope.$apply();
};
// Start naming clusters asynchronously
nameClusters();
// --- Render AmCharts historical chart ---
// ... (existing AmCharts logic remains unchanged) ...
setTimeout(function () {
if (!window.AmCharts) return;
var chartData = [];
var providers = $scope.allProviders || [];
var dateMap = {};
(query.history || []).forEach(function (entry) {
if (!entry.date) return;
var dateStr = entry.date.substr(0, 10).replace(/-/g, '');
dateMap[dateStr] = dateMap[dateStr] || { date: dateStr };
var rank = null;
if (entry.urls && entry.urls.length) {
for (var i = 0; i < entry.urls.length; ++i) {
var urlDomain = entry.urls[i].replace(/^https?:\/\//, '').split('/')[0].replace(/^www\./, '');
if (urlDomain === userDomain) {
rank = i + 1;
break;
}
}
}
dateMap[dateStr][entry.provider] = rank;
});
for (var d in dateMap) chartData.push(dateMap[d]);
chartData.sort(function (a, b) { return a.date.localeCompare(b.date); });
var graphs = (providers || []).map(function (provider, idx) {
return {
balloonText: provider + ': [[value]] ',
title: provider,
valueField: provider,
bullet: 'round',
bulletBorderAlpha: 1,
bulletColor: '#FFFFFF',
useLineColorForBulletBorder: true,
fillAlphas: 0,
lineThickness: 2,
lineAlpha: 1,
bulletSize: 7,
lineColor: h.colors ? h.colors()[idx % 6] : undefined
};
});
AmCharts.makeChart('llm-history-chart-' + query._id, {
type: 'serial', theme: 'light', marginLeft: 0, marginRight: 0, marginTop: 10, categoryField: 'date', rotate: false, dataDateFormat: 'YYYYMMDD',
categoryAxis: { gridPosition: 'start', parseDates: true, labelRotation: 45 },
valueAxes: [{ reversed: true, position: 'left', axisAlpha: 0, labelsEnabled: true, minimum: 1 }],
graphs: graphs, dataProvider: chartData, legend: { position: 'bottom', enabled: true, useGraphSettings: true }
});
}, 0);
}
};
// --- Credit & Budgeting ---
$scope.providers = ['google', 'openai', 'perplexity'];
$scope.frequencies = {
'daily': { name: 'Daily', cost: 31 },
'weekly': { name: 'Weekly', cost: 4 },
'monthly': { name: 'Monthly', cost: 1 }
};
// Initialize new query options
$scope.page.newQueryProviders = { google: true, openai: true, perplexity: true };
$scope.page.newQueryFrequency = 'weekly';
$scope.page.newQueryCost = 0;
$scope.page.isOverBudget = false;
$scope.page.currentUsage = 0;
$scope.page.totalProjectedCost = 0;
// Function to calculate cost
$scope.calculateCost = function() {
// Count the number of actual queries entered in the textarea
var queryCount = 0;
if ($scope.page.newQuery && $scope.page.newQuery.trim()) {
// Split by comma or newline and filter out any empty strings
queryCount = $scope.page.newQuery.split(/[\,\n]+/).filter(q => q.trim()).length;
}
// When editing, we are always dealing with a single query, so force count to 1
if ($scope.page.editing) {
queryCount = 1;
}
// 1. Calculate cost of the new/edited query batch
var newQueryProvidersCount = Object.values($scope.page.newQueryProviders).filter(Boolean).length;
var newQueryFrequencyCost = $scope.frequencies[$scope.page.newQueryFrequency] ? $scope.frequencies[$scope.page.newQueryFrequency].cost : 0;
var newQueryCost = queryCount * newQueryProvidersCount * newQueryFrequencyCost;
$scope.page.newQueryCost = newQueryCost;
// 2. Calculate current usage from all existing queries
var currentUsage = 0;
($scope.queries || []).forEach(function(q) {
// If we are editing this query, its cost will be replaced by the new cost, so skip it here.
if ($scope.page.editing && $scope.page.editing === q._id) {
return;
}
var providerCount = (q.providers && q.providers.length) ? q.providers.length : $scope.allProviders.length;
var frequencyCost = $scope.frequencies[q.frequency] ? $scope.frequencies[q.frequency].cost : $scope.frequencies['weekly'].cost;
currentUsage += providerCount * frequencyCost;
});
$scope.page.currentUsage = currentUsage;
// 3. Calculate total projected cost
var totalProjectedCost = currentUsage + newQueryCost;
$scope.page.totalProjectedCost = totalProjectedCost;
// 4. Check against budget
var availableCredits = ($rootScope.user && $rootScope.user.credits && $rootScope.user.credits.llm) ? $rootScope.user.credits.llm : 0;
$scope.page.isOverBudget = totalProjectedCost > availableCredits;
};
// Watch for changes to recalculate cost
$scope.$watch('page.newQueryProviders', $scope.calculateCost, true);
$scope.$watch('page.newQueryFrequency', $scope.calculateCost);
$scope.$watch('queries', $scope.calculateCost, true); // Recalculate if queries are added/deleted
// Initial calculation
$scope.calculateCost();
// Initial load
$scope.getQueries();
},
]);
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 + 10% voor
// servers en onvoorzien; dat mag de white label zien, zonder namen van bronnen. 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/PROMPT_STEPS
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_PROMPTS = [10, 25, 50, 100, 200];
// 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, aiPrompts: AI_PROMPTS,
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', prompts: 10 };
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/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 = {};
var o = self.overview;
if (o) {
var money = function (x) { return self.money(x, o.currency); };
var addons = o.serp.gross + o.ai.gross;
v.kpis = [
{ label: t('whitelabel.overview.kpi_sites'), value: f.num(o.sites), note: t('whitelabel.overview.this_month') },
{ label: t('whitelabel.overview.kpi_unit'), value: money(o.unit_price), note: t('whitelabel.overview.per_month_incl') },
{ label: t('whitelabel.overview.kpi_addons'), value: addons ? f.euro(addons) : '–', note: t('whitelabel.overview.addon_sites', { n: o.serp.sites + o.ai.sites }) },
{ label: t('whitelabel.overview.kpi_total'), value: money(o.total), note: t('whitelabel.overview.estimate') }
];
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) },
{ what: t('whitelabel.overview.line_serp'), how: o.serp.sites ? t('whitelabel.overview.line_sites', { n: o.serp.sites }) : t('whitelabel.overview.line_none'), amount: o.serp.gross ? f.euro(o.serp.gross) : '–' },
{ what: t('whitelabel.overview.line_ai'), how: o.ai.sites ? t('whitelabel.overview.line_sites', { n: o.ai.sites }) : t('whitelabel.overview.line_none'), amount: o.ai.gross ? 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.prompts }) },
{ t: t('whitelabel.overview.inc_brand'), d: t('whitelabel.overview.inc_brand_d') }
];
var pct = Math.round(o.markup * 100);
var unit = function (u, digits) { return { cost: self.eur(u.cost, digits), markup: self.eur(u.markup, digits), price: self.eur(u.price, digits) }; };
v.addons = [
angular.extend({ name: t('menu.serp'), per: t('whitelabel.addons.per_serp') }, unit(o.unit_costs.serp, 6)),
angular.extend({ name: t('aiinsights.title'), per: t('whitelabel.addons.per_ai') }, unit(o.unit_costs.ai, 4))
];
v.markupText = t('whitelabel.addons.markup', { pct: pct });
}
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;
},
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}';
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' }); },
// 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;
}
// series [{values:[getal|null]}] -> svg-paden met losse stukken waar data ontbreekt, en de punten
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); }
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 || isNaN(v)) { if (cur.length) { 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); }
return { d: segs.map(smooth).join(' '), color: s.color, width: s.width || 2, dash: s.dash ? '4 4' : '', dots: dots, label: s.label };
});
}
return { niceMax: niceMax, bands: bands, lines: lines };
});
// --- 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}}
' +
'
' +
'
'
};
});
// --- verschil: ▲ groen of ▼ rood; invert voor posities (lager is beter); unit '%' (standaard, ook leeg), 'pt' of 'none'
app.directive('mtDelta', ['mtFmt', function (mtFmt) {
return {
restrict: 'E',
scope: { value: '<', invert: '', unit: '@', digits: '@' },
template:
'' +
' {{txt}} ',
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',
scope: { label: '@', value: '@', delta: '', invert: '', unit: '@', note: '@', href: '@', warn: '', loading: '' },
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}} ' +
'
' +
'{{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
app.directive('mtState', function () {
return {
restrict: 'E', transclude: true,
scope: { kind: '@', title: '@', text: '@', step: '@', progress: '', action: '@', onAction: '&', href: '@' },
template:
'
' +
'' +
'
' +
'
' +
'
{{title}} ' +
'
{{text}}
' +
'
' +
'
' +
'
{{action}} ' +
'
{{action}} ' +
'
'
};
});
// --- 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?}]}; invert: hoog = klein getal (posities)
// min/max: vaste as (anders 0 tot een rond maximum)
app.directive('mtLine', ['mtChart', 'mtTip', 'mtFmt', function (mtChart, mtTip, mtFmt) {
var W = 640;
return {
restrict: 'E',
scope: { data: '<', height: '@', invert: '', min: '', max: '', fmt: '' },
template:
'{{axis[0]}} {{axis[1]}} {{axis[2]}}
' +
'
' +
'
' +
' ' +
' ' +
'
' +
'
' +
'
' +
'
{{l}}
' +
'' +
'{{s.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.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); } }); });
var min = scope.min !== undefined ? scope.min : (scope.invert ? 1 : 0);
var max = scope.max !== undefined ? scope.max : mtChart.niceMax(Math.max.apply(null, all.concat([1])));
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);
});
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);
}
};
}]);
// --- 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: ''
};
});
// --- 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'
};
return {
restrict: 'E',
scope: { kind: '@' },
template: '{{\'ui.badge.\' + kind | translate}} ',
link: function (scope) { scope.c = C; }
};
});
// --- 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; }
};
}
};
});
// 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', '$translate', 'h', function ($scope, $http, $translate, h) {
var s = $scope.gsciSettings = { country: '', loaded: false, saved: false };
$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;
});
// ontkoppelen (gscinsights/pullcustomer)
s.disconnect = function () {
if (!window.confirm($translate.instant('gscinsights.settings.disconnect_confirm'))) { return; }
$http.get('/rest/gscinsights/pullcustomer', { params: { websiteid: h.siteId() } }).then(function () {
s.connected = false;
s.property = null;
gsciState.cache = {};
});
};
s.save = function () {
$http.post('/rest/gscinsights/settings', { websiteid: h.siteId(), country: s.country }).then(function () {
s.saved = true;
gsciState.cache = {};
});
};
}]);
app.controller('gscinsights', ['$scope', '$rootScope', '$location', '$routeParams', '$http', '$translate', '$timeout', 'h', 'mtFmt', function ($scope, $rootScope, $location, $routeParams, $http, $translate, $timeout, h, mtFmt) {
// 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: {},
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;
if (s.gsc === 'no_property') { 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) {
self.properties = (res.data.data || []).map(function (p) { return p.siteUrl; });
});
},
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) {
var ch = function (a, b) { return b ? 100 * (a - b) / b : null; };
var out = [
{ label: t('gscinsights.kpi.clicks'), value: f.num(now.clicks), delta: before ? ch(now.clicks, before.clicks) : null },
{ label: t('gscinsights.kpi.impressions'), value: f.num(now.impressions), delta: before ? ch(now.impressions, before.impressions) : null },
{ label: t('gscinsights.kpi.ctr'), value: f.pct(now.ctr), delta: before ? now.ctr - before.ctr : null, unit: 'pt', digits: 1 },
{ 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({ 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({ 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 = this.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'; },
// ------------------------------------------------------------------ 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) { angular.extend(v, self.viewOverviewChart(p.chart)); }
if (p.why) {
var w = p.why.why;
v.whyDown = w.diff < 0;
v.whyText = t(w.diff < 0 ? 'gscinsights.overview.why_down' : 'gscinsights.overview.why_up', { n: f.num(Math.abs(w.diff)) });
v.why = w.rows.map(function (r) { return { q: r.query, diff: f.num(r.diff), why: self.causeText(r), href: self.exploreHref(r.query) }; });
}
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;
},
viewOverviewChart: function (d) {
var months = d.months || [];
return {
chart: {
labels: months.map(function (m) { return f.month(m.month); }),
titles: months.map(function (m) { return f.monthLong(m.month) + (m.partial ? ' · ' + t('gscinsights.overview.partial') : ''); }),
series: d.brand.length ? [
{ label: t('gscinsights.overview.brand'), color: '#1e40af', values: months.map(function (m) { return m.brand; }) },
{ label: t('gscinsights.overview.nonbrand'), color: '#60a5fa', values: months.map(function (m) { return m.clicks === null ? null : m.clicks - m.brand; }) }
] : [{ label: t('gscinsights.kpi.clicks'), color: '#3b82f6', values: months.map(function (m) { return m.clicks; }) }],
line: { label: t('gscinsights.overview.last_year'), color: '#9ca3af', values: months.map(function (m) { return m.last_year; }) },
faded: months.map(function (m) { return m.partial; }),
extra: months.map(function (m) { return m.revenue !== null && m.revenue !== undefined ? [{ label: t('gscinsights.overview.revenue'), value: f.euro(m.revenue), color: '#047857' }] : []; })
},
brandNote: d.brand.length ? t('gscinsights.overview.brand_terms', { terms: d.brand.join(', ') }) : ''
};
},
// ------------------------------------------------------------------ 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 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.healthColor = k.health === null ? '#e5e7eb' : (k.health >= 67 ? '#16a34a' : (k.health >= 40 ? '#f59e0b' : '#dc2626'));
k.measuredText = k.measured ? (k.measured.pos ? String(k.measured.pos) : t('gscinsights.tracked.not_top20')) : '–';
});
});
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) {
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 };
}),
groupNames: (d.groups || []).map(function (g) { return g.name; }).filter(Boolean)
};
},
// één zoekwoord openklappen: grafiek (klikken, vertoningen, positie, CTR; per dag of per maand), pagina's,
// landen en apparaten (gscinsights/tracked_detail)
kwOpen: null,
// vast (geen array-literal in ng-repeat: die maakt elke digest nieuwe objecten)
kwBlocks: [{ k: 'pages', t: 'gscinsights.tracked.pages' }, { k: 'countries', t: 'gscinsights.tracked.countries' }, { k: 'devices', t: 'gscinsights.tracked.devices' }],
toggleKeyword: function (k) {
var self = this;
if (self.kwOpen && self.kwOpen.q === k.query) { self.kwOpen = null; return; }
self.kwOpen = { q: k.query, metric: 'clicks', per: 'month', d: null };
self.get('tracked_detail', { keyword: k.query }).then(function (d) {
if (!self.kwOpen || self.kwOpen.q !== k.query) { return; }
self.kwOpen.d = d;
d.countries.forEach(function (c) { c.label = self.countryName(c.cc) || c.country; });
d.devices.forEach(function (c) { c.label = t('gscinsights.explore.device_name.' + c.device); });
d.pages.forEach(function (p) { p.label = p.page.replace(/^https?:\/\/[^\/]+/, '') || '/'; });
self.kwChart();
});
},
kwSet: function (key, val) { this.kwOpen[key] = val; this.kwChart(); },
// nieuw data-object voor mt-line bij elke keuze
kwChart: function () {
var o = this.kwOpen, d = o && o.d;
if (!d) { return; }
var rows = o.per === 'day' ? d.daily : d.monthly;
var lbl = function (r) { return o.per === 'day' ? f.day(r.date) : f.month(r.month); };
var labels = rows.map(lbl);
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: [{ label: t('gscinsights.explore.col.' + o.metric), color: '#2563eb', values: rows.map(function (r) { return r[o.metric]; }) }]
};
o.fmt = o.metric === 'ctr' ? function (v) { return f.pct(v); } : (o.metric === 'position' ? function (v) { return f.pos(v); } : null);
},
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,
exOpen: null,
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.exOpen = 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; }
},
exToggle: function (r) {
var self = this;
if (self.ex.dim !== 'query' && self.ex.dim !== 'page') { return; }
if (self.exOpen && self.exOpen.r === r) { self.exOpen = null; return; }
self.exOpen = { r: r, rows: null };
var p = self.exploreParams();
delete p.dim;
p[self.ex.dim] = r[self.ex.dim];
self.get('detail', p).then(function (d) {
if (self.exOpen && self.exOpen.r === r) {
d.rows.forEach(function (x) { x.label = d.dim === 'page' ? (x.page.replace(/^https?:\/\/[^\/]+/, '') || '/') : x.query; });
self.exOpen.rows = d.rows;
self.exOpen.dim = d.dim;
}
});
},
// klik op een pagina in de details: 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: {} };
app.controller('aiinsights', ['$scope', '$rootScope', '$location', '$routeParams', '$http', '$translate', '$timeout', '$sce', 'h', 'mtFmt', function ($scope, $rootScope, $location, $routeParams, $http, $translate, $timeout, $sce, h, mtFmt) {
// 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 = { chatgpt: '#1e3a8a', perplexity: '#3b82f6', gemini: '#93c5fd', other: '#cbd5e1' };
var TRAFFIC_OF = { openai: 'chatgpt', google: 'gemini', perplexity: 'perplexity' };
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: '' };
// 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 (zelfde lijsten als ai::PLAN_FREQ en ai::PROMPT_STEPS in rest/include/ai.php)
var FREQS = ['monthly', 'biweekly', 'weekly'];
var PROMPT_STEPS = [10, 25, 50, 100, 200];
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,
promptSteps: PROMPT_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 NAMES[e] || 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 BASE + '/antwoorden/' + encodeURIComponent(id); },
// ------------------------------------------------------------------ overzicht
viewOverview: function (p) {
var self = this, v = {};
var k = p.kpis;
if (!k) {
v.kpis = ['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;
v.kpis = [
{ 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) {
v.kpis.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; };
v.kpis.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) }) : '' });
v.kpis.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') });
}
}
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) {
var months = p.chart.months || [];
v.noGa4 = !p.chart.ga4;
v.chart = p.chart.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;
}
if (p.actions) {
v.actions = p.actions.actions.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 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,
planPct: Math.min(100, 100 * d.prompts.length / d.plan.prompts),
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) {
this.edit = {
show: true, busy: false, error: '', id: p ? p.id : null, q: p ? p.q : (q || ''),
providers: { openai: !p || p.providers.indexOf('openai') > -1, google: !p || p.providers.indexOf('google') > -1, perplexity: !p || p.providers.indexOf('perplexity') > -1 },
brands: p ? p.brands.join(', ') : ''
};
},
saveEdit: function () {
var self = this, e = self.edit;
e.busy = true;
e.error = '';
$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)),
prompts: Math.max(0, PROMPT_STEPS.filter(function (n) { return n <= p.prompts; }).length - 1)
};
});
},
planOf: function () { return { freq: FREQS[+this.settings.freq], prompts: PROMPT_STEPS[+this.settings.prompts] }; },
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;
var pages = d.pages.filter(function (p) {
if (!flt) { return true; }
return flt === 'other' ? Object.keys(p.engines).some(function (e) { return main.indexOf(e) === -1; }) : !!p.engines[flt];
}).map(function (p) {
var bar = main.concat(['other']).map(function (e) {
var s = e === 'other' ? Object.keys(p.engines).filter(function (x) { return main.indexOf(x) === -1; }).reduce(function (t, x) { return t + p.engines[x]; }, 0) : (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,
sessions: f.num(p.sessions), delta: 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('contentcheckpreview', ['$scope', '$location', 'h', function ($scope, $location, h) {
// Prototype van de contentcheck "wat mist dit artikel om te ranken" (2026-10-27): alleen demodata, geen aanroepen
// naar de server. Verstopt: geen menu-item, alleen zichtbaar bij de site van brabbels. Het artikel is echt, de top 10
// en alle cijfers zijn verzonnen.
if (h.siteId() !== '6ac8ae10c1b85117944adbe2') {
$location.path('/');
return;
}
// onderwerp, dekking per plek 1-10 in de top 10, aantal op plek 11-20, jij: ja / nee / half
var TOPICS = [
['Strippen door de verloskundige', '1111111111', 9, 'ja'],
['Ananas, tonic en ander eten', '1111011111', 8, 'ja'],
['Medisch inleiden in het ziekenhuis', '1101111011', 3, 'nee'],
['Tepelstimulatie', '1110111101', 6, 'ja'],
['Vrijen', '1111101101', 7, 'ja'],
['Vanaf welke week mag je het proberen', '1110101101', 4, 'nee'],
['Wandelen, fietsen, traplopen', '1011110110', 6, 'ja'],
['Dadels eten', '0101011010', 2, 'nee'],
['Wonderolie', '1100110110', 5, 'ja'],
['Frambozenbladthee en kruiden', '0110101101', 5, 'ja'],
['Acupressuur en acupunctuur', '1010110011', 4, 'ja'],
['Oefeningen op de bevallingsbal', '1001101010', 2, 'half'],
['Wanneer bel je de verloskundige', '1100100011', 3, 'ja'],
['Ervaringen van andere moeders', '0010110001', 2, 'half'],
['Homeopathie', '0000100000', 2, 'ja'],
['Kinine in tonic en bitter lemon', '1000000100', 1, 'ja']
];
$scope.oCheck = {
keyword: 'bevalling opwekken',
url: 'https://www.brabbels.com/bevalling/bevalling-opwekken',
position: 14,
busy: false,
positions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
missing: [
{
title: 'Medisch inleiden in het ziekenhuis', top10: 8, rest: 3, state: 'nee',
why: 'Wie zoekt op "bevalling opwekken" wil ook weten wat er gebeurt als het niet vanzelf gaat: de ballonkatheter, de gel of tablet om de baarmoedermond rijp te maken en het infuus met weeënopwekkers. Jij verwijst alleen naar je pagina over inleiden. Dit is het onderwerp dat de top 10 het duidelijkst onderscheidt van plek 11-20.',
todo: 'Voeg een kop toe', example: 'Wat als je bevalling ingeleid wordt?', todoAfter: 'met de drie manieren in het kort en een link naar je pagina over inleiden.',
how: [['#1', 'legt de ballonkatheter, prostaglandinen en het oxytocine-infuus stap voor stap uit'], ['#2', 'zet natuurlijk opwekken en medisch inleiden naast elkaar in een tabel'],
['#4', 'noemt wanneer de verloskundige je doorverwijst naar het ziekenhuis'], ['#5, #6, #8, #9, #10', 'hebben er een eigen kop over']]
},
{
title: 'Vanaf welke week mag je het proberen', top10: 7, rest: 4, state: 'nee',
why: 'Je noemt dat bevallen tussen 37 en 42 weken normaal is, maar zegt niet vanaf wanneer het verstandig is om zelf iets te proberen. De top 10 beantwoordt dat bijna altijd vroeg in de tekst, vaak in de eerste alinea.',
todo: 'Voeg één alinea bovenaan toe, bijvoorbeeld', example: 'Probeer het pas als je 40 weken zwanger bent, en eerder alleen na overleg', todoAfter: '',
how: [['#1', 'zet het antwoord in de inleiding'], ['#3', 'heeft er een kop met een vraag over'], ['#2, #5, #7, #8, #10', 'noemen het bij de afzonderlijke middelen']]
},
{
title: 'Dadels eten in de laatste weken', top10: 6, rest: 2, state: 'nee',
why: 'Bij eten en drinken bespreek je ananas, tonic en pittig eten. Dadels noemt de top 10 vaker dan ananas, omdat daar onderzoek naar is gedaan. Mensen verwachten het in dit rijtje, ook al zoeken ze er niet apart op.',
todo: 'Voeg een paar zinnen over dadels toe onder de kop', example: 'Eten en drinken', todoAfter: '',
how: [['#2 en #6', 'noemen hoeveel dadels per dag en vanaf welke week'], ['#1, #4, #7, #9', 'noemen het in een opsomming']]
},
{
title: 'Oefeningen op de bevallingsbal', top10: 5, rest: 2, state: 'half',
why: 'Je kop over beweging gaat over fietsen, wandelen en traplopen. De top 10 geeft daarnaast concrete oefeningen: wiegen op de bal, je bekken rondjes laten draaien, hurken.',
todo: 'Breid uit met twee of drie oefeningen onder', example: 'Beweging', todoAfter: '', how: []
},
{
title: 'Ervaringen van andere moeders', top10: 4, rest: 2, state: 'half',
why: 'Vier artikelen in de top 10 laten moeders vertellen wat bij hen werkte. Jij hebt een forum met precies die verhalen, maar het artikel verwijst er niet naar.',
todo: 'Voeg een verwijzing naar het forum over de bevalling toe, of twee korte citaten.', example: '', todoAfter: '', how: []
}
],
extra: [
{ title: 'Homeopathie', top10: 1, note: 'Een korte zin is genoeg. Meer tekst hierover maakt je artikel minder gericht op wat de zoeker wil weten.' },
{ title: 'Kinine in tonic en bitter lemon', top10: 2, note: 'Je uitleg is goed, maar staat in verhouding lang. Overweeg hem in te korten tot de waarschuwing.' }
],
// factor, jouw percentiel t.o.v. de top 20, gemiddelde top 10, oordeel, toelichting
factors: [
{ name: 'Dekking van de zoekvraag', me: 72, top: 70, state: 'ok', note: 'Je gebruikt "bevalling opwekken" en de varianten ervan net zo vaak als de top 10.' },
{ name: 'Lijkt op de top 10', me: 48, top: 66, state: 'part', note: 'Onder de top 10, vooral door de ontbrekende onderwerpen hierboven.' },
{ name: 'Eigen zijpaden', me: 61, top: 40, state: 'part', note: 'Iets meer eigen materiaal dan de top 10 (homeopathie, kinine). Kort dat in.' },
{ name: 'Samenhang tussen zinnen', me: 70, top: 62, state: 'ok', note: 'De tekst loopt goed; elke kop blijft bij zijn onderwerp.' },
{ name: 'Direct antwoord bovenaan', me: 45, top: 64, state: 'part', note: 'Het antwoord ("een beetje, maar minder dan de verhalen") staat pas in de vierde zin. Zet het in de eerste twee.' },
{ name: 'Vraagkoppen', me: 20, top: 55, state: 'miss', note: '1 van je 9 koppen is een vraag, in de top 10 gemiddeld 4. Bijvoorbeeld "Werkt strippen?"' },
{ name: 'Aanspreekvorm', me: 85, top: 80, state: 'ok', note: 'Je spreekt de lezer aan met "je", net als de top 10.' },
{ name: 'Actualiteit', me: 90, top: 75, state: 'ok', note: 'Bijgewerkt in oktober 2026.' }
],
topics: TOPICS.map(function (t) {
var cells = t[1].split('').map(function (c) { return c === '1'; });
return { name: t[0], cells: cells, n: cells.filter(Boolean).length, rest: t[2], me: t[3] };
}),
fnCount: function (state) {
return this.missing.filter(function (m) { return m.state === state; }).length;
},
fnDots: function (n) {
var a = [];
for (var i = 0; i < 10; i++) a.push(i < n);
return a;
},
fnStateLabel: function (state) {
return { ok: 'goed', part: 'kan beter', miss: 'aandacht' }[state];
},
// de knop doet niets echt: even "bezig" en dezelfde demodata
fnAnalyse: function () {
var self = this;
self.busy = true;
setTimeout(function () { $scope.$apply(function () { self.busy = false; }); }, 1200);
}
};
}]);