diff --git a/.gitignore b/.gitignore
index 723ef36f4e4f32c4560383aa5987c575a30c6535..8bf1f37fd91c583a7a19c09774d7ad1fa5e35d76 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
-.idea
\ No newline at end of file
+.idea
+vendor
+modules
+composer.lock
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 515eff65bbd9b0601bc51167d96b3dd75f54d2b2..2215e0979c519a5f928153f322376c1022702d02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file.
 
 ## [Unreleased]
 
+## [v4.0.0]
+#### Added
+- aggregated statistics (logins and unique users)
+
+#### Changed
+- new database tables
+- config options for table names replaced with one (optional) option `tableNames`
+- option 'config' for auth proc filter made optional
+- auth proc filter renamed to Statistics (PascalCase)
+- major refactoring
+
+#### Removed
+- `detailedDays` config option
+- compatibility for deprecated database config options
+- duplicate code
+
 ## [v3.2.1]
 #### Fixed
 - Fixed the bug in using double '$'
diff --git a/README.md b/README.md
index 40fec49e1bcdc22c9e6636dd7bedc8a932f5ba80..14b49834bb53358070d39212b83f61d3821fbfbd 100644
--- a/README.md
+++ b/README.md
@@ -15,51 +15,38 @@ Once you have installed SimpleSAMLphp, installing this module is very simple. Fi
 ## Configuration
 1. Install MySQL Database and create database for statistics and user. 
 2. For this database run script to create tables. Script is available in config-templates/tables.sql.
-3. Copy config-templates/module_statisticsproxy.php to your folder vith config and fill it.
+3. Copy config-templates/module_proxystatistics.php to your config folder and fill it.
 4. Configure, according to mode
-* for mode PROXY, configure IdPAttribute filter from Perun module to get sourceIdPName from IdP metadata:  
+* for PROXY mode, configure IdPAttribute filter from Perun module to get sourceIdPName from IdP metadata:
 ```
-    XX => [
-            'class' => 'perun:IdPAttribute',
-            'attrMap' => [
-                    'name:en' => 'sourceIdPName',
-            ],
+    50 => [
+        'class' => 'perun:IdPAttribute',
+        'attrMap' => [
+            'name:en' => 'sourceIdPName',
+        ],
     ],
-    // where XX is priority (for example 30, must not be used for other modules)
+    // where 50 is priority (for example, must not be used for other modules)
 ```
-* for mode IDP, configure `idpEntityId` and `idpName` in `module_statisticsproxy.php`
+* for IDP mode, configure entity ID and name in `module_proxystatistics.php`
 ```
-    /*
-     * EntityId of IdP
-     * REQUIRED FOR IDP MODE
-     */
-    'idpEntityId' => '',
-    /*
-     * Name of IdP
-     * REQUIRED FOR IDP MODE
-     */
-    'idpName' => '',
+    'IDP' => [
+        'id' => '',
+        'name' => '',
+    ],
 ```
-* for mode SP, configure `spEntityId` and `spName` in `module_statisticsproxy.php`
+* for SP mode, configure entity ID and name in `module_proxystatistics.php`
 ```
-    /*
-     * EntityId of SP
-     * REQUIRED FOR SP MODE
-     */
-    'spEntityId' => '',
-    /*
-     * Name of SP
-     * REQUIRED FOR SP MODE
-     */
-    'spName' => '',
+    'SP' => [
+        'id' => '',
+        'name' => '',
+    ],
 ```
-5. Configure proxystatistic filter
+5. Configure proxystatistics filter
 ```
-    XX => array(
-            'class' => 'proxystatistics:statistics',
-            'config' => [],
-    ),                
-    // where XX is priority (for example 50, must not be used for other modules)
+    50 => [
+        'class' => 'proxystatistics:Statistics',
+    ],
+    // where 50 is priority (for example, must not be used for other modules)
 ```
 6. Add to `config.php`:
 ```
diff --git a/composer.json b/composer.json
index 4cf95370deea2ace83dd00699caba2f680177480..6a2ce1a39da3d21f9ad3a38b9ff72eaf59ff3b65 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,6 @@
 {
   "name": "cesnet/simplesamlphp-module-proxystatistics",
   "description": "A SimpleSAMLPHP module for statistics",
-  "version": "3.3.0-dev",
   "type": "simplesamlphp-module",
   "keywords": ["statistics","simplesamlphp"],
   "license": "BSD-2-Clause",
@@ -12,10 +11,14 @@
     }
   ],
   "require": {
-    "php": ">=5.4.0",
-    "cesnet/simplesamlphp-module-perun" : "~3.0",
+    "php": ">=7.1.0",
+    "cesnet/simplesamlphp-module-perun" : "^3.0",
     "simplesamlphp/simplesamlphp": "~1.17",
     "ext-mysqli": "*",
     "ext-json": "*"
+  },
+  "require-dev": {
+    "squizlabs/php_codesniffer": "*",
+    "symplify/easy-coding-standard": "^7.2"
   }
 }
diff --git a/config-templates/module_proxystatistics.php b/config-templates/module_proxystatistics.php
new file mode 100644
index 0000000000000000000000000000000000000000..118e7ba66b7d7510e71dfc2a401191605d0a1721
--- /dev/null
+++ b/config-templates/module_proxystatistics.php
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * This is an example configuration of SimpleSAMLphp Perun interface and additional features.
+ * Copy this file to default config directory and edit the properties.
+ *
+ * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
+$config = [
+
+    /*
+     * Choose one of the following modes: PROXY, IDP, SP
+     */
+    'mode' => 'PROXY',
+
+    /*
+     * EntityId and name of IdP
+     * REQUIRED FOR IDP MODE
+     */
+    //'IDP' => [
+    //    'id' => '',
+    //    'name' => '',
+    //],
+
+    /*
+     * EntityId and name of SP
+     * REQUIRED FOR SP MODE
+     */
+    //'SP' => [
+    //    'id' => '',
+    //    'name' => '',
+    //],
+
+    /*
+     * Config for SimpleSAML\Database.
+     * If not set, the global config is used.
+     * @see SimpleSAML\Database
+     */
+    'store' => [
+        'database.dsn' => 'mysql:host=localhost;port=3306;dbname=STATS;charset=utf8',
+        'database.username' => 'stats',
+        'database.password' => 'stats',
+
+        /**
+         * Configuration for SSL
+         * If you want to use SSL, fill these values and uncomment the block of code
+         */
+        //'database.driver_options' => [
+        //    PDO::MYSQL_ATTR_SSL_KEY => '', // Path for the ssl key file
+        //    PDO::MYSQL_ATTR_SSL_CERT => '', // Path for the ssl cert file
+        //    PDO::MYSQL_ATTR_SSL_CA => '', // Path for the ssl ca file
+        //    PDO::MYSQL_ATTR_SSL_CAPATH => '', // Path for the ssl ca dir
+        //],
+    ],
+
+    /**
+     * Which attribute should be used as user ID.
+     * @default uid
+     */
+    //'userIdAttribute' => 'uid',
+
+    /**
+     * Database table names.
+     * Default is to keep the name (as in `tables.sql`)
+     */
+    'tableNames' => [
+        //'statistics_sums' => 'statistics_sums',
+        //'statistics_per_user' => 'statistics_per_user',
+        //'statistics_idp' => 'statistics_idp',
+        //'statistics_sp' => 'statistics_sp',
+    ],
+
+    /**
+     * Authentication source name if authentication should be required.
+     * Defaults to empty string.
+     */
+    //'requireAuth.source' => 'default-sp',
+
+    /**
+     * For how many days should the detailed statistics be kept. Minimum is 31.
+     */
+    //'keepPerUser' => 62,
+];
diff --git a/config-templates/module_statisticsproxy.php b/config-templates/module_statisticsproxy.php
deleted file mode 100644
index b21bb41457c30e96e3a54151dfd2f4eaaabe7a36..0000000000000000000000000000000000000000
--- a/config-templates/module_statisticsproxy.php
+++ /dev/null
@@ -1,106 +0,0 @@
-<?php
-/**
- * This is an example configuration of SimpleSAMLphp Perun interface and additional features.
- * Copy this file to default config directory and edit the properties.
- *
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- */
-
-$config = [
-
-    /*
-     * Choose one of the following modes: PROXY, IDP, SP
-     */
-    'mode' => '',
-
-    /*
-     * EntityId of IdP
-     * REQUIRED FOR IDP MODE
-     */
-    'idpEntityId' => '',
-
-    /*
-     * Name of IdP
-     * REQUIRED FOR IDP MODE
-     */
-    'idpName' => '',
-
-    /*
-     * EntityId of SP
-     * REQUIRED FOR SP MODE
-     */
-    'spEntityId' => '',
-
-    /*
-     * Name of SP
-     * REQUIRED FOR SP MODE
-     */
-    'spName' => '',
-
-    /*
-     * Config for SimpleSAML\Database.
-     * If not set, the global config is used.
-     * @see SimpleSAML\Database
-     */
-    'store' => [
-        'database.dsn' => 'mysql:host=localhost;port=3306;dbname=STATS;charset=utf8',
-        'database.username' => 'stats',
-        'database.password' => 'stats',
-
-        /**
-         * Configuration for SSL
-         * If you want to use SSL, fill these values and uncomment the block of code
-         */
-        /*
-        'database.driver_options' => [
-            // Path for the ssl key file
-            PDO::MYSQL_ATTR_SSL_KEY => '',
-            // Path for the ssl cert file
-            PDO::MYSQL_ATTR_SSL_CERT => '',
-            // Path for the ssl ca file
-            PDO::MYSQL_ATTR_SSL_CA => '',
-            // Path for the ssl ca dir
-            PDO::MYSQL_ATTR_SSL_CAPATH => '',
-        ],
-        */
-    ],
-
-    /*
-     * For how many days should detailed statistics (per user) be kept.
-     * @default 0
-     */
-    'detailedDays' => 0,
-
-    /**
-     * Which attribute should be used as user ID.
-     * @default uid
-     */
-    'userIdAttribute' => 'uid',
-
-    /*
-     * Table name for statistics
-     */
-    'statisticsTableName' => 'statisticsTableName',
-
-    /*
-     * Table name for detailed statistics
-     * @default
-     */
-    'detailedStatisticsTableName' => 'statistics_detail',
-
-    /*
-     * Table name for identityProvidersMap
-     */
-    'identityProvidersMapTableName' => 'identityProvidersMap',
-
-    /*
-     * Table name for serviceProviders
-     */
-    'serviceProvidersMapTableName' => 'serviceProvidersMap',
-
-    /**
-     * Authentication source name if authentication should be required.
-     * Defaults to empty string.
-     */
-    //'requireAuth.source' => 'default-sp',
-];
diff --git a/config-templates/tables.sql b/config-templates/tables.sql
index 3f0a1b1a34eb388e3b56e7aa2ad13d18e15b1818..03a3962ab3317eca559ffd70833461cfee72f3c9 100644
--- a/config-templates/tables.sql
+++ b/config-templates/tables.sql
@@ -1,38 +1,55 @@
---Statistics for IdPs
-CREATE TABLE statistics (
-    year INT NOT NULL,
-    month INT NOT NULL,
-    day INT NOT NULL,
-    sourceIdp VARCHAR(255) NOT NULL,
-    service VARCHAR(255) NOT NULL,
-    count INT,
-    INDEX (sourceIdp),
-    INDEX (service),
-    PRIMARY KEY (year, month, day, sourceIdp, service)
+-- daily and monthly logins and unique users for all combinations of idp+sp
+-- -> can be reduced depending on the mode (IdP mode does not need the combinations with IdP)
+-- (could also include yearly numbers if statistics_per_user are kept for a year)
+CREATE TABLE `statistics_sums` (
+  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `year` YEAR NOT NULL,
+  `month` TINYINT UNSIGNED DEFAULT NULL,
+  `day` TINYINT UNSIGNED DEFAULT NULL,
+  `idpId` INT UNSIGNED NOT NULL DEFAULT 0,
+  `spId` INT UNSIGNED NOT NULL DEFAULT 0,
+  `logins` INT UNSIGNED DEFAULT NULL,
+  `users` INT UNSIGNED DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `year` (`year`,`month`,`day`,`idpId`,`spId`),
+  KEY `idpId` (`idpId`),
+  KEY `spId` (`spId`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+-- ROWS
+-- each row contains daily users = COUNT(1) and daily logins = SUM(logins) from statistics_per_user
+-- year, month, day,  idp,  sp    | daily per idp+sp
+-- year, month, day,  NULL, sp    | daily per sp
+-- year, month, day,  idp,  NULL  | daily per idp
+-- year, month, day,  NULL, NULL  | daily (total)
+-- year, month, NULL, -||-        | monthly -||-
 
-CREATE TABLE statistics_detail (
-    year INT NOT NULL,
-    month INT NOT NULL,
-    day INT NOT NULL,
-    sourceIdp VARCHAR(255) NOT NULL,
-    service VARCHAR(255) NOT NULL,
-    user VARCHAR(255) NOT NULL,
-    count INT,
-    INDEX (sourceIdp),
-    INDEX (service),
-    PRIMARY KEY (year, month, day, sourceIdp, service, user)
+-- daily logins per IdP+SP+user combination
+-- data is being kept for ~1 month
+CREATE TABLE `statistics_per_user` (
+  `day` date NOT NULL,
+  `idpId` INT UNSIGNED NOT NULL,
+  `spId` INT UNSIGNED NOT NULL,
+  `user` VARCHAR(255) NOT NULL,
+  `logins` INT UNSIGNED DEFAULT '1',
+  PRIMARY KEY (`day`,`idpId`,`spId`,`user`),
+  KEY `idpId` (`idpId`),
+  KEY `spId` (`spId`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
---Tables for mapping identifier to name
-CREATE TABLE identityProvidersMap(
-	entityId VARCHAR(255) NOT NULL,
-	name VARCHAR(255) NOT NULL,
-	PRIMARY KEY (entityId)
+-- identity providers
+CREATE TABLE `statistics_idp` (
+  `idpId` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `identifier` VARCHAR(255) NOT NULL,
+  `name` VARCHAR(255) NOT NULL,
+  PRIMARY KEY (`idpId`),
+  UNIQUE KEY `identifier` (`identifier`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
-CREATE TABLE serviceProvidersMap(
-	identifier VARCHAR(255) NOT NULL,
-	name VARCHAR(255) NOT NULL,
-	PRIMARY KEY (identifier)
+-- services
+CREATE TABLE `statistics_sp` (
+  `spId` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `identifier` VARCHAR(255) NOT NULL,
+  `name` VARCHAR(255) NOT NULL,
+  PRIMARY KEY (`spId`),
+  UNIQUE KEY `identifier` (`identifier`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/dictionaries/Proxystatistics.definition.json b/dictionaries/Proxystatistics.definition.json
deleted file mode 100644
index 2db8eb6537e37f8ae66d6ac586234588954a9e45..0000000000000000000000000000000000000000
--- a/dictionaries/Proxystatistics.definition.json
+++ /dev/null
@@ -1,186 +0,0 @@
-{
-  "summary": {
-    "en": "Summary",
-    "cs": "Shrnutí"
-  },
-  "templates/statistics-tpl_idpsDetail": {
-    "en": "Identity providers detail",
-    "cs": "Detail poskytovatelů identity"
-  },
-  "templates/statistics-tpl_spsDetail": {
-    "en": "Service providers detail",
-    "cs": "Detail služeb"
-  },
-  "templates_time_range": {
-    "en": "Select the time range:",
-    "cs": "Vyberte časový interval:"
-  },
-  "templates/statistics-tpl_all": {
-    "en": "All",
-    "cs": "Vše"
-  },
-  "templates/statistics-tpl_week": {
-    "en": "Last 7 days",
-    "cs": "Posledních 7 dnů"
-  },
-  "templates/statistics-tpl_month": {
-    "en": "Last 30 days",
-    "cs": "Posledních 30 dnů"
-  },
-  "templates/statistics-tpl_year": {
-    "en": "Last year",
-    "cs": "Poslední rok"
-  },
-  "templates/summary_overall_logins": {
-    "en": "Overall number of logins",
-    "cs": "Celkový počet přihlášení"
-  },
-  "templates/summary_accessed_providers": {
-    "en": "Total number of accessed service providers",
-    "cs": "Celkový počet používaných služeb"
-  },
-  "templates/summary_used_identity_providers": {
-    "en": "Total number of used identity providers",
-    "cs": "Celkový počet použitých poskytovatelů identit"
-  },
-  "templates/summary_logins_today": {
-    "en": "Number of logins for today",
-    "cs": "Počet přihlášení během dnešního dne"
-  },
-  "templates/summary_average_logins": {
-    "en": "Average number of logins per day",
-    "cs": "Průměrný počet přihlášení za den"
-  },
-  "templates/summary_max_logins": {
-    "en": "Maximal number of logins per day",
-    "cs": "Maximální počet přihlášení za den"
-  },
-  "templates/summary_name": {
-    "en": "Name",
-    "cs": "Jméno"
-  },
-  "templates/count": {
-    "en": "Count",
-    "cs": "Počet"
-  },
-  "templates/other": {
-    "en": "other",
-    "cs": "ostatní"
-  },
-  "templates/graphs_logins": {
-    "en": "Number of logins",
-    "cs": "Počet přihlášení"
-  },
-  "templates/graphs_id_providers": {
-    "en": "Identity providers",
-    "cs": "Poskytovatelé identit"
-  },
-  "templates/graphs_service_providers": {
-    "en": "Service providers",
-    "cs": "Poskytovatelé služeb"
-  },
-  "templates/logins_day": {
-    "en": "Number of logins per day",
-    "cs": "Počet přihlášení za den"
-  },
-  "templates/logins_identity": {
-    "en": "Overall logins per identity provider",
-    "cs": "Počet přihlášení podle poskytovatele identity"
-  },
-  "templates/logins_service": {
-    "en": "Overall logins to service providers",
-    "cs": "Počet přihlášení ke službám"
-  },
-  "templates/tables_date": {
-    "en": "Date",
-    "cs": "Datum"
-  },
-  "templates/tables_month": {
-    "en": "Month",
-    "cs": "Měsíc"
-  },
-  "templates/tables_identity_provider": {
-    "en": "Identity provider",
-    "cs": "Poskytovatel identity"
-  },
-  "templates/tables_service_provider": {
-    "en": "Service provider",
-    "cs": "Služba"
-  },
-  "templates/statistics_header": {
-    "en": "AAI Statistics",
-    "cs": "AAI Statistiky"
-  },
-  "templates/identityProviders_legend": {
-    "en": "The chart and the table show number of logins from each identity provider in selected time range. Click a specific identity provider to view detailed statistics for that identity provider.",
-    "cs": "Graf s tabulkou ukazují počet přihlášení od každého poskytovatele identity ve zvoleném časovém rozmezí. Kliknutím na konkrétního poskytovatele identit zobrazíte detailní statistiky pro daného poskytovatele identit."
-  },
-  "templates/serviceProviders_legend": {
-    "en": "The chart and the table show number of logins to each service provider in selected time range. Only first access to the service is counted, following single sign-on accesses are not counted, because they are not going through the Proxy. Click a specific service to view detailed statistics for that service.",
-    "cs": "Graf s tabulkou ukazují přihlášení ke každé službě ve zvoleném časovém rozmezí. Počítá se pouze první přístup ke službě, protože další již nejdou přes Proxy. Kliknutím na konkrétní službu zobrazíte detailní statistiky pro danou službu."
-  },
-  "templates/summary_logins_info": {
-    "en": "The chart shows overall number of logins from identity providers for each day.",
-    "cs": "Graf zobrazuje počet přihlášení za každý den."
-  },
-  "templates/summary_idps_info": {
-    "en": "The chart shows number of logins from each identity provider in selected time range. Click a specific identity provider to view detailed statistics for that identity provider.",
-    "cs": "Graf zobrazuje počet přihlášení od každého poskytovatele identity ve zvoleném časovém rozmezí. Kliknutím na konkrétního poskytovatele identit zobrazíte detailní statistiky pro daného poskytovatele identit."
-  },
-  "templates/summary_sps_info": {
-    "en": "The chart shows number of logins to each service provider in selected time range. Only first access to the service is counted, following single sign-on accesses are not counted, because they are not going through the Proxy. Click a specific service to view detailed statistics for that service.",
-    "cs": "Graf zobrazuje počet přihlášení ke každé službě ve zvoleném časovém rozmezí. Počítá se pouze první přístup ke službě, následující přístupy neprocházejí přes Proxy. Kliknutím na konkrétní službu zobrazíte detailní statistiky pro danou službu."
-  },
-  "templates/spDetail_header_name": {
-    "en": "Detail statistics for Service Provider: ",
-    "cs": "Podrobné statistiky služby: "
-  },
-  "templates/spDetail_header_identifier": {
-    "en": "Detail statistics for Service Provider with identifier: ",
-    "cs": "Podrobné statistiky služby s identifikátorem: "
-  },
-  "templates/spDetail_dashboard_header": {
-    "en": "Number of logins",
-    "cs": "Počet přihlášení"
-  },
-  "templates/spDetail_dashboard_legend": {
-    "en": "The chart shows number of logins to this service for each day.",
-    "cs": "Graf zobrazuje počet přihlášení k dané službě za každý den."
-  },
-  "templates/spDetail_graph_header": {
-    "en": "Used identity providers",
-    "cs": "Použití poskytovatelé identit"
-  },
-  "templates/spDetail_graph_legend": {
-    "en": "The chart and the table shows used identity providers to login to this service provider in selected time range. Only first access to the service is counted, following single sign-on accesses are not counted, because they are not going through the Proxy.",
-    "cs": "Graf s tabulkou ukazují použité poskytovatele identit pro přihlášení k této službě ve zvoleném časovém rozmezí. Počítá se pouze první přístup ke službě, protože další již nejdou přes Proxy."
-  },
-  "templates/idpDetail_header_name": {
-    "en": "Detail usage of Identity Provider: ",
-    "cs": "Detail využití poskytovatele identit: "
-  },
-  "templates/idpDetail_header_entityId": {
-    "en": "Detail usage for Service Provider with entityId: ",
-    "cs": "Detail využití poskytovatele identit s entityId: "
-  },
-  "templates/idpDetail_dashboard_header": {
-    "en": "Number of usage of identity provider",
-    "cs": "Počet přístupů skrze poskytovatele identit"
-  },
-  "templates/idpDetail_dashboard_legend": {
-    "en": "The chart shows number of logins using this identity provider for each day.",
-    "cs": "Graf zobrazuje počet přihlášení tímto poskytovatelem identit za každý den."
-  },
-  "templates/idpDetail_graph_header": {
-    "en": "Accessed service providers",
-    "cs": "Služby, ke kterým bylo přistupováno"
-  },
-  "templates/idpDetail_graph_legend": {
-    "en": "The chart and the table shows accessed service providers by this identity provider in selected time range. Only first access to the service is counted, following single sign-on accesses are not counted, because they are not going through the Proxy.",
-    "cs": "Graf s tabulkou ukazují služby, ke kterým bylo přistoupeno daným poskytovatelem identit ve zvoleném časovém rozmezí. Počítá se pouze první přístup ke službě, protože další již nejdou přes Proxy."
-  },
-  "btn_label_back_to_stats": {
-    "en": "Back to overall statistics",
-    "cs": "Zpět na přehled celkových statistik"
-  }
-}
diff --git a/dictionaries/stats.definition.json b/dictionaries/stats.definition.json
new file mode 100644
index 0000000000000000000000000000000000000000..bc26b1cc893bd582315e347d06330c7e78200699
--- /dev/null
+++ b/dictionaries/stats.definition.json
@@ -0,0 +1,202 @@
+{
+  "summary": {
+    "en": "Summary",
+    "cs": "Shrnutí"
+  },
+  "sideIDPDetail": {
+    "en": "Identity providers detail",
+    "cs": "Detail poskytovatelů identity"
+  },
+  "sideSPDetail": {
+    "en": "Service providers detail",
+    "cs": "Detail služeb"
+  },
+  "select_time_range": {
+    "en": "Select the time range:",
+    "cs": "Vyberte časový interval:"
+  },
+  "time_range_all": {
+    "en": "All",
+    "cs": "Vše"
+  },
+  "time_range_week": {
+    "en": "Last 7 days",
+    "cs": "Posledních 7 dnů"
+  },
+  "time_range_month": {
+    "en": "Last 30 days",
+    "cs": "Posledních 30 dnů"
+  },
+  "time_range_year": {
+    "en": "Last year",
+    "cs": "Poslední rok"
+  },
+  "summary_overall_logins": {
+    "en": "Overall number of logins",
+    "cs": "Celkový počet přihlášení"
+  },
+  "summary_accessed_providers": {
+    "en": "Total number of accessed service providers",
+    "cs": "Celkový počet používaných služeb"
+  },
+  "summary_used_identity_providers": {
+    "en": "Total number of used identity providers",
+    "cs": "Celkový počet použitých poskytovatelů identit"
+  },
+  "summary_logins_today": {
+    "en": "Number of logins for today",
+    "cs": "Počet přihlášení během dnešního dne"
+  },
+  "summary_average_logins": {
+    "en": "Average number of logins per day",
+    "cs": "Průměrný počet přihlášení za den"
+  },
+  "summary_max_logins": {
+    "en": "Maximal number of logins per day",
+    "cs": "Maximální počet přihlášení za den"
+  },
+  "summary_name": {
+    "en": "Name",
+    "cs": "Jméno"
+  },
+  "count": {
+    "en": "Count",
+    "cs": "Počet"
+  },
+  "other": {
+    "en": "other",
+    "cs": "ostatní"
+  },
+  "graphs_logins": {
+    "en": "Number of logins",
+    "cs": "Počet přihlášení"
+  },
+  "side_IDPs": {
+    "en": "Identity providers",
+    "cs": "Poskytovatelé identit"
+  },
+  "side_IDP": {
+    "en": "Identity provider",
+    "cs": "Poskytovatel identity"
+  },
+  "side_SPs": {
+    "en": "Service providers",
+    "cs": "Poskytovatelé služeb"
+  },
+  "side_SP": {
+    "en": "Service provider",
+    "cs": "Služba"
+  },
+  "logins_day": {
+    "en": "Number of logins per day",
+    "cs": "Počet přihlášení za den"
+  },
+  "logins_IDP": {
+    "en": "Overall logins per identity provider",
+    "cs": "Počet přihlášení podle poskytovatele identity"
+  },
+  "logins_SP": {
+    "en": "Overall logins to service providers",
+    "cs": "Počet přihlášení ke službám"
+  },
+  "tables_date": {
+    "en": "Date",
+    "cs": "Datum"
+  },
+  "tables_month": {
+    "en": "Month",
+    "cs": "Měsíc"
+  },
+  "statistics_header": {
+    "en": "AAI Statistics",
+    "cs": "AAI Statistiky"
+  },
+  "chart_legend": {
+    "en": "The chart shows the number of logins !side_of in the selected time range. Click !side_on to view detailed statistics.",
+    "cs": "Graf ukazuje počet přihlášení !side_of ve zvoleném časovém rozmezí. Kliknutím na !side_on zobrazíte detailní statistiky."
+  },
+  "chart_legend_side_of_IDP": {
+    "en": "from each identity provider",
+    "cs": "od každého poskytovatele identity"
+  },
+  "chart_legend_side_of_SP": {
+    "en": "to each service provider",
+    "cs": "ke každé službě"
+  },
+  "chart_legend_side_on_IDP": {
+    "en": "an identity provider",
+    "cs": "poskytovatele identit"
+  },
+  "chart_legend_side_on_SP": {
+    "en": "a service",
+    "cs": "službu"
+  },
+  "first_access_only": {
+    "en": "Only first access to a service is counted, because following logins are not going through the !through_mode.",
+    "cs": "Počítá se pouze první přístup ke službě, protože další již nejdou přes !through_mode."
+  },
+  "through_mode_PROXY": {
+    "en": "proxy",
+    "cs": "proxy"
+  },
+  "through_mode_IDP": {
+    "en": "identity provider",
+    "cs": "poskytovatele identity"
+  },
+  "summary_logins_info": {
+    "en": "The chart shows overall number of logins from identity providers for each day.",
+    "cs": "Graf zobrazuje počet přihlášení za každý den."
+  },
+  "SPDetail_header_name": {
+    "en": "Detail statistics for Service Provider ",
+    "cs": "Podrobné statistiky služby "
+  },
+  "SPDetail_dashboard_header": {
+    "en": "Number of logins",
+    "cs": "Počet přihlášení"
+  },
+  "SPDetail_dashboard_legend": {
+    "en": "The chart shows number of logins to this service for each day.",
+    "cs": "Graf zobrazuje počet přihlášení k dané službě za každý den."
+  },
+  "SPDetail_graph_header": {
+    "en": "Used identity providers",
+    "cs": "Použití poskytovatelé identit"
+  },
+  "SPDetail_graph_legend": {
+    "en": "The chart and the table shows used identity providers to login to this service provider in selected time range. Only first access to the service is counted, following single sign-on accesses are not counted, because they are not going through the Proxy.",
+    "cs": "Graf s tabulkou ukazují použité poskytovatele identit pro přihlášení k této službě ve zvoleném časovém rozmezí. Počítá se pouze první přístup ke službě, protože další již nejdou přes Proxy."
+  },
+  "IDPDetail_header_name": {
+    "en": "Detail usage of Identity Provider ",
+    "cs": "Detail využití poskytovatele identit "
+  },
+  "IDPDetail_dashboard_header": {
+    "en": "Number of usage of identity provider",
+    "cs": "Počet přístupů skrze poskytovatele identit"
+  },
+  "IDPDetail_dashboard_legend": {
+    "en": "The chart shows number of logins using this identity provider for each day.",
+    "cs": "Graf zobrazuje počet přihlášení tímto poskytovatelem identit za každý den."
+  },
+  "IDPDetail_graph_header": {
+    "en": "Accessed service providers",
+    "cs": "Služby, ke kterým bylo přistupováno"
+  },
+  "IDPDetail_graph_legend": {
+    "en": "The chart and the table shows accessed service providers by this identity provider in selected time range. Only first access to the service is counted, following single sign-on accesses are not counted, because they are not going through the Proxy.",
+    "cs": "Graf s tabulkou ukazují služby, ke kterým bylo přistoupeno daným poskytovatelem identit ve zvoleném časovém rozmezí. Počítá se pouze první přístup ke službě, protože další již nejdou přes Proxy."
+  },
+  "back_to_stats": {
+    "en": "Back to overall statistics",
+    "cs": "Zpět na přehled celkových statistik"
+  },
+  "of_logins": {
+    "en": "logins",
+    "cs": "přihlášení"
+  },
+  "of_users": {
+    "en": "users",
+    "cs": "uživatelů"
+  }
+}
diff --git a/ecs.yaml b/ecs.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b11760b71fcf6c45fb27b12bc2d95b930ba0995a
--- /dev/null
+++ b/ecs.yaml
@@ -0,0 +1,9 @@
+imports:
+    - { resource: 'vendor/symplify/easy-coding-standard/config/set/clean-code.yaml' }
+    - { resource: 'vendor/symplify/easy-coding-standard/config/set/common.yaml' }
+parameters:
+    skip:
+        PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer: ~
+        SlevomatCodingStandard\Sniffs\Variables\UnusedVariableSniff.UnusedVariable:
+          - 'config-templates/module_proxystatistics.php'
+services:
diff --git a/hooks/hook_cron.php b/hooks/hook_cron.php
index d2c2d3b41228cb75877dcbd5584347397ce78cc0..b50e4b9547b2177847beda460890190671f82829 100644
--- a/hooks/hook_cron.php
+++ b/hooks/hook_cron.php
@@ -1,24 +1,27 @@
 <?php
 
+use SimpleSAML\Logger;
+use SimpleSAML\Module\proxystatistics\DatabaseCommand;
+
 /**
  * Hook to run a cron job.
  *
  * @param array &$croninfo  Output
- * @return void
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
  */
 function proxystatistics_hook_cron(&$croninfo)
 {
     if ($croninfo['tag'] !== 'daily') {
-        \SimpleSAML\Logger::debug('cron [proxystatistics]: Skipping cron in cron tag ['.$croninfo['tag'].'] ');
+        Logger::debug('cron [proxystatistics]: Skipping cron in cron tag [' . $croninfo['tag'] . '] ');
         return;
     }
 
-    \SimpleSAML\Logger::info('cron [proxystatistics]: Running cron in cron tag ['.$croninfo['tag'].'] ');
+    Logger::info('cron [proxystatistics]: Running cron in cron tag [' . $croninfo['tag'] . '] ');
 
     try {
-        $dbCmd = new \SimpleSAML\Module\proxystatistics\Auth\Process\DatabaseCommand();
-        $dbCmd->deleteOldDetailedStatistics();
+        $dbCmd = new DatabaseCommand();
+        $dbCmd->aggregate();
     } catch (\Exception $e) {
-        $croninfo['summary'][] = 'Error during deleting old detailed statistics: '.$e->getMessage();
+        $croninfo['summary'][] = 'Error during statistics aggregation: ' . $e->getMessage();
     }
 }
diff --git a/lib/Auth/Process/DatabaseCommand.php b/lib/Auth/Process/DatabaseCommand.php
deleted file mode 100644
index f97fab8a006f787a7341bec48c51bf2101e738e3..0000000000000000000000000000000000000000
--- a/lib/Auth/Process/DatabaseCommand.php
+++ /dev/null
@@ -1,254 +0,0 @@
-<?php
-
-namespace SimpleSAML\Module\proxystatistics\Auth\Process;
-
-use SimpleSAML\Error\Exception;
-use SimpleSAML\Logger;
-use PDO;
-
-/**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- */
-class DatabaseCommand
-{
-    private $databaseConnector;
-    private $conn;
-    private $statisticsTableName;
-    private $detailedStatisticsTableName;
-    private $identityProvidersMapTableName;
-    private $serviceProvidersMapTableName;
-
-    public function __construct()
-    {
-        $this->databaseConnector = new DatabaseConnector();
-        $this->conn = $this->databaseConnector->getConnection();
-        assert($this->conn !== null);
-        $this->statisticsTableName = $this->databaseConnector->getStatisticsTableName();
-        $this->detailedStatisticsTableName = $this->databaseConnector->getDetailedStatisticsTableName();
-        $this->identityProvidersMapTableName = $this->databaseConnector->getIdentityProvidersMapTableName();
-        $this->serviceProvidersMapTableName = $this->databaseConnector->getServiceProvidersMapTableName();
-    }
-
-    private function writeLogin($year, $month, $day, $sourceIdp, $service, $user = null)
-    {
-        $params = [
-            'year' => $year,
-            'month' => $month,
-            'day' => $day,
-            'sourceIdp' => $sourceIdp,
-            'service' => $service,
-            'count' => 1,
-        ];
-        $table = $this->statisticsTableName;
-        if ($user && $this->databaseConnector->getDetailedDays() > 0) {
-            // write also into aggregated statistics
-            self::writeLogin($year, $month, $day, $sourceIdp, $service);
-            $params['user'] = $user;
-            $table = $this->detailedStatisticsTableName;
-        }
-        $fields = array_keys($params);
-        $placeholders = array_map(function ($field) {
-            return ':' . $field;
-
-        }, $fields);
-        $query = "INSERT INTO " . $table . " (" . implode(', ', $fields) . ")" .
-                 " VALUES (" . implode(', ', $placeholders) . ") ON DUPLICATE KEY UPDATE count = count + 1";
-
-        return $this->conn->write($query, $params);
-    }
-
-    public function insertLogin(&$request, &$date)
-    {
-        if (!in_array($this->databaseConnector->getMode(), ['PROXY', 'IDP', 'SP'])) {
-            throw new Exception('Unknown mode is set. Mode has to be one of the following: PROXY, IDP, SP.');
-        }
-        if ($this->databaseConnector->getMode() !== 'IDP') {
-            $idpName = $request['Attributes']['sourceIdPName'][0];
-            $idpEntityID = $request['saml:sp:IdP'];
-        }
-        if ($this->databaseConnector->getMode() !== 'SP') {
-            $spEntityId = $request['Destination']['entityid'];
-            $spName = isset($request['Destination']['name']) ? $request['Destination']['name']['en'] : '';
-        }
-
-        if ($this->databaseConnector->getMode() === 'IDP') {
-            $idpName = $this->databaseConnector->getIdpName();
-            $idpEntityID = $this->databaseConnector->getIdpEntityId();
-        } elseif ($this->databaseConnector->getMode() === 'SP') {
-            $spEntityId = $this->databaseConnector->getSpEntityId();
-            $spName = $this->databaseConnector->getSpName();
-        }
-
-        $year = $date->format('Y');
-        $month = $date->format('m');
-        $day = $date->format('d');
-
-        if (empty($idpEntityID) || empty($spEntityId)) {
-            Logger::error(
-                "'idpEntityId' or 'spEntityId'" .
-                " is empty and login log wasn't inserted into the database."
-            );
-        } else {
-            $idAttribute = $this->databaseConnector->getUserIdAttribute();
-            $userId = isset($request['Attributes'][$idAttribute]) ? $request['Attributes'][$idAttribute][0] : null;
-            if ($this->writeLogin($year, $month, $day, $idpEntityID, $spEntityId, $userId) === false) {
-                Logger::error("The login log wasn't inserted into table: " . $this->statisticsTableName . ".");
-            }
-
-            if (!empty($idpName)) {
-                $this->conn->write(
-                    "INSERT INTO " . $this->identityProvidersMapTableName .
-                    "(entityId, name) VALUES (:idp, :name1) ON DUPLICATE KEY UPDATE name = :name2",
-                    ['idp'=>$idpEntityID, 'name1'=>$idpName, 'name2'=>$idpName]
-                );
-            }
-
-            if (!empty($spName)) {
-                $this->conn->write(
-                    "INSERT INTO " . $this->serviceProvidersMapTableName .
-                    "(identifier, name) VALUES (:sp, :name1) ON DUPLICATE KEY UPDATE name = :name2",
-                    ['sp'=>$spEntityId, 'name1'=>$spName, 'name2'=>$spName]
-                );
-            }
-        }
-
-    }
-
-    public function getSpNameBySpIdentifier($identifier)
-    {
-        return $this->conn->read(
-            "SELECT name " .
-            "FROM " . $this->serviceProvidersMapTableName . " " .
-            "WHERE identifier=:sp",
-            ['sp'=>$identifier]
-        )->fetchColumn();
-    }
-
-    public function getIdPNameByEntityId($idpEntityId)
-    {
-        return $this->conn->read(
-            "SELECT name " .
-            "FROM " . $this->identityProvidersMapTableName . " " .
-            "WHERE entityId=:idp",
-            ['idp'=>$idpEntityId]
-        )->fetchColumn();
-    }
-
-    public function getLoginCountPerDay($days)
-    {
-        $query = "SELECT year, month, day, SUM(count) AS count " .
-                 "FROM " . $this->statisticsTableName . " " .
-                 "WHERE service != '' ";
-        $params = [];
-        self::addDaysRange($days, $query, $params);
-        $query .= "GROUP BY year,month,day " .
-                  "ORDER BY year ASC,month ASC,day ASC";
-
-        return $this->conn->read($query, $params)->fetchAll(PDO::FETCH_ASSOC);
-    }
-
-    public function getLoginCountPerDayForService($days, $spIdentifier)
-    {
-        $query = "SELECT year, month, day, SUM(count) AS count " .
-                 "FROM " . $this->statisticsTableName . " " .
-                 "WHERE service=:service ";
-        $params = ['service' => $spIdentifier];
-        self::addDaysRange($days, $query, $params);
-        $query .= "GROUP BY year,month,day " .
-                  "ORDER BY year ASC,month ASC,day ASC";
-
-        return $this->conn->read($query, $params)->fetchAll(PDO::FETCH_ASSOC);
-    }
-
-    public function getLoginCountPerDayForIdp($days, $idpIdentifier)
-    {
-        $query = "SELECT year, month, day, SUM(count) AS count " .
-                 "FROM " . $this->statisticsTableName . " " .
-                 "WHERE sourceIdP=:sourceIdP ";
-        $params = ['sourceIdP'=>$idpIdentifier];
-        self::addDaysRange($days, $query, $params);
-        $query .= "GROUP BY year,month,day " .
-                  "ORDER BY year ASC,month ASC,day ASC";
-
-        return $this->conn->read($query, $params)->fetchAll(PDO::FETCH_ASSOC);
-    }
-
-    public function getAccessCountPerService($days)
-    {
-        $query = "SELECT IFNULL(name,service) AS spName, service, SUM(count) AS count " .
-                 "FROM " . $this->serviceProvidersMapTableName . " " .
-                 "LEFT OUTER JOIN " . $this->statisticsTableName . " ON service = identifier ";
-        $params = [];
-        self::addDaysRange($days, $query, $params);
-        $query .= "GROUP BY service HAVING service != '' " .
-                  "ORDER BY count DESC";
-
-        return $this->conn->read($query, $params)->fetchAll(PDO::FETCH_NUM);
-    }
-
-    public function getAccessCountForServicePerIdentityProviders($days, $spIdentifier)
-    {
-        $query = "SELECT IFNULL(name,sourceIdp) AS idpName, SUM(count) AS count " .
-                 "FROM " . $this->identityProvidersMapTableName . " " .
-                 "LEFT OUTER JOIN " . $this->statisticsTableName . " ON sourceIdp = entityId ";
-        $params = ['service' => $spIdentifier];
-        self::addDaysRange($days, $query, $params);
-        $query .= "GROUP BY sourceIdp, service HAVING sourceIdp != '' AND service=:service " .
-                  "ORDER BY count DESC";
-
-        return $this->conn->read($query, $params)->fetchAll(PDO::FETCH_NUM);
-    }
-
-    public function getAccessCountForIdentityProviderPerServiceProviders($days, $idpEntityId)
-    {
-        $query = "SELECT IFNULL(name,service) AS spName, SUM(count) AS count " .
-                 "FROM " . $this->serviceProvidersMapTableName . " " .
-                 "LEFT OUTER JOIN " . $this->statisticsTableName . " ON service = identifier ";
-        $params = ['sourceIdp'=>$idpEntityId];
-        self::addDaysRange($days, $query, $params);
-        $query .= "GROUP BY sourceIdp, service HAVING service != '' AND sourceIdp=:sourceIdp " .
-                  "ORDER BY count DESC";
-
-        return $this->conn->read($query, $params)->fetchAll(PDO::FETCH_NUM);
-    }
-
-    public function getLoginCountPerIdp($days)
-    {
-        $query = "SELECT IFNULL(name,sourceIdp) AS idpName, sourceIdp, SUM(count) AS count " .
-                 "FROM " . $this->identityProvidersMapTableName . " " .
-                 "LEFT OUTER JOIN " . $this->statisticsTableName . " ON sourceIdp = entityId ";
-        $params = [];
-        self::addDaysRange($days, $query, $params);
-        $query .= "GROUP BY sourceIdp HAVING sourceIdp != '' " .
-                  "ORDER BY count DESC";
-
-        return $this->conn->read($query, $params)->fetchAll(PDO::FETCH_NUM);
-    }
-
-    private static function addDaysRange($days, &$query, &$params, $not = false)
-    {
-        if ($days != 0) {    // 0 = all time
-            if (stripos($query, "WHERE") === false) {
-                $query .= "WHERE";
-            } else {
-                $query .= "AND";
-            }
-            $query .= " CONCAT(year,'-',LPAD(month,2,'00'),'-',LPAD(day,2,'00')) ";
-            if ($not) {
-                $query .= "NOT ";
-            }
-            $query .= "BETWEEN CURDATE() - INTERVAL :days DAY AND CURDATE() ";
-            $params['days'] = $days;
-        }
-    }
-
-    public function deleteOldDetailedStatistics()
-    {
-        if ($this->databaseConnector->getDetailedDays() > 0) {
-            $query = "DELETE FROM " . $this->detailedStatisticsTableName . " ";
-            $params = [];
-            self::addDaysRange($this->databaseConnector->getDetailedDays(), $query, $params, true);
-            return $this->conn->write($query, $params);
-        }
-    }
-}
diff --git a/lib/Auth/Process/DatabaseConnector.php b/lib/Auth/Process/DatabaseConnector.php
deleted file mode 100644
index 34ada8b76c28b13c762f7df174d56f73882fee96..0000000000000000000000000000000000000000
--- a/lib/Auth/Process/DatabaseConnector.php
+++ /dev/null
@@ -1,167 +0,0 @@
-<?php
-
-namespace SimpleSAML\Module\proxystatistics\Auth\Process;
-
-use SimpleSAML\Configuration;
-use SimpleSAML\Database;
-use SimpleSAML\Logger;
-use PDO;
-
-/**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- */
-
-class DatabaseConnector
-{
-    private $statisticsTableName;
-    private $detailedStatisticsTableName;
-    private $identityProvidersMapTableName;
-    private $serviceProvidersMapTableName;
-    private $mode;
-    private $idpEntityId;
-    private $idpName;
-    private $spEntityId;
-    private $spName;
-    private $detailedDays;
-    private $userIdAttribute;
-    private $conn = null;
-
-    const CONFIG_FILE_NAME = 'module_statisticsproxy.php';
-    /** @deprecated */
-    const SERVER = 'serverName';
-    /** @deprecated */
-    const PORT = 'port';
-    /** @deprecated */
-    const USER = 'userName';
-    /** @deprecated */
-    const PASSWORD = 'password';
-    /** @deprecated */
-    const DATABASE = 'databaseName';
-    const STATS_TABLE_NAME = 'statisticsTableName';
-    const DETAILED_STATS_TABLE_NAME = 'detailedStatisticsTableName';
-    const IDP_MAP_TABLE_NAME = 'identityProvidersMapTableName';
-    const SP_MAP_TABLE_NAME = 'serviceProvidersMapTableName';
-    /** @deprecated */
-    const ENCRYPTION = 'encryption';
-    const STORE = 'store';
-    /** @deprecated */
-    const SSL_CA = 'ssl_ca';
-    /** @deprecated */
-    const SSL_CERT = 'ssl_cert_path';
-    /** @deprecated */
-    const SSL_KEY = 'ssl_key_path';
-    /** @deprecated */
-    const SSL_CA_PATH = 'ssl_ca_path';
-    const MODE = 'mode';
-    const IDP_ENTITY_ID = 'idpEntityId';
-    const IDP_NAME = 'idpName';
-    const SP_ENTITY_ID = 'spEntityId';
-    const SP_NAME = 'spName';
-    const DETAILED_DAYS = 'detailedDays';
-    const USER_ID_ATTRIBUTE = 'userIdAttribute';
-
-    public function __construct()
-    {
-        $conf = Configuration::getConfig(self::CONFIG_FILE_NAME);
-        $this->storeConfig = $conf->getArray(self::STORE, null);
-
-        // TODO: remove
-        if (empty($this->storeConfig) && $conf->getString(self::DATABASE, false)) {
-            $this->storeConfig = [
-                'database.dsn' => sprintf(
-                    'mysql:host=%s;port=%d;dbname=%s;charset=utf8',
-                    $conf->getString(self::SERVER, 'localhost'),
-                    $conf->getInteger(self::PORT, 3306),
-                    $conf->getString(self::DATABASE)
-                ),
-                'database.username' => $conf->getString(self::USER),
-                'database.password' => $conf->getString(self::PASSWORD),
-            ];
-            if ($conf->getBoolean(self::ENCRYPTION, false)) {
-                Logger::debug("Getting connection with encryption.");
-                $this->storeConfig['database.driver_options'] = [
-                    PDO::MYSQL_ATTR_SSL_KEY => $conf->getString(self::SSL_KEY, ''),
-                    PDO::MYSQL_ATTR_SSL_CERT => $conf->getString(self::SSL_CERT, ''),
-                    PDO::MYSQL_ATTR_SSL_CA => $conf->getString(self::SSL_CA, ''),
-                    PDO::MYSQL_ATTR_SSL_CAPATH => $conf->getString(self::SSL_CA_PATH, ''),
-                ];
-            }
-
-            Logger::debug("Deprecated option(s) used for proxystatistics. Please use the store option.");
-        }
-
-        $this->storeConfig = Configuration::loadFromArray($this->storeConfig);
-
-        $this->statisticsTableName = $conf->getString(self::STATS_TABLE_NAME);
-        $this->detailedStatisticsTableName = $conf->getString(self::DETAILED_STATS_TABLE_NAME, 'statistics_detail');
-        $this->identityProvidersMapTableName = $conf->getString(self::IDP_MAP_TABLE_NAME);
-        $this->serviceProvidersMapTableName = $conf->getString(self::SP_MAP_TABLE_NAME);
-        $this->mode = $conf->getString(self::MODE, 'PROXY');
-        $this->idpEntityId = $conf->getString(self::IDP_ENTITY_ID, '');
-        $this->idpName = $conf->getString(self::IDP_NAME, '');
-        $this->spEntityId = $conf->getString(self::SP_ENTITY_ID, '');
-        $this->spName = $conf->getString(self::SP_NAME, '');
-        $this->detailedDays = $conf->getInteger(self::DETAILED_DAYS, 0);
-        $this->userIdAttribute = $conf->getString(self::USER_ID_ATTRIBUTE, 'uid');
-    }
-
-    public function getConnection()
-    {
-        return Database::getInstance($this->storeConfig);
-    }
-
-    public function getStatisticsTableName()
-    {
-        return $this->statisticsTableName;
-    }
-
-    public function getDetailedStatisticsTableName()
-    {
-        return $this->detailedStatisticsTableName;
-    }
-
-    public function getIdentityProvidersMapTableName()
-    {
-        return $this->identityProvidersMapTableName;
-    }
-
-    public function getServiceProvidersMapTableName()
-    {
-        return $this->serviceProvidersMapTableName;
-    }
-
-    public function getMode()
-    {
-        return $this->mode;
-    }
-
-    public function getIdpEntityId()
-    {
-        return $this->idpEntityId;
-    }
-
-    public function getIdpName()
-    {
-        return $this->idpName;
-    }
-
-    public function getSpEntityId()
-    {
-        return $this->spEntityId;
-    }
-
-    public function getSpName()
-    {
-        return $this->spName;
-    }
-
-    public function getDetailedDays()
-    {
-        return $this->detailedDays;
-    }
-
-    public function getUserIdAttribute()
-    {
-        return $this->userIdAttribute;
-    }
-}
diff --git a/lib/Auth/Process/statistics.php b/lib/Auth/Process/Statistics.php
similarity index 77%
rename from lib/Auth/Process/statistics.php
rename to lib/Auth/Process/Statistics.php
index f9838938125ae1f7cdf4ce3dadb73afa67f65fee..ace9c133b96eae909415732a2b48785b723ce7a7 100644
--- a/lib/Auth/Process/statistics.php
+++ b/lib/Auth/Process/Statistics.php
@@ -1,30 +1,22 @@
 <?php
 
+/**
+ * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
 namespace SimpleSAML\Module\proxystatistics\Auth\Process;
 
 use DateTime;
 use SimpleSAML\Auth\ProcessingFilter;
-use SimpleSAML\Error\Exception;
 use SimpleSAML\Logger;
+use SimpleSAML\Module\proxystatistics\DatabaseCommand;
 
-/**
- *
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- */
 class Statistics extends ProcessingFilter
 {
-    private $config;
-    private $reserved;
-
     public function __construct($config, $reserved)
     {
         parent::__construct($config, $reserved);
-
-        if (!isset($config['config'])) {
-            throw new Exception("missing mandatory configuration option 'config'");
-        }
-        $this->config = (array)$config['config'];
-        $this->reserved = (array)$reserved;
     }
 
     public function process(&$request)
@@ -50,13 +42,11 @@ class Statistics extends ProcessingFilter
 
         if (isset($request['perun']['user'])) {
             $user = $request['perun']['user'];
-            Logger::notice('UserId: ' . $user->getId() . ', identity: ' .  $eduPersonUniqueId . ', service: '
+            Logger::notice('UserId: ' . $user->getId() . ', identity: ' . $eduPersonUniqueId . ', service: '
                 . $spEntityId . ', external identity: ' . $sourceIdPEppn . ' from ' . $sourceIdPEntityId);
         } else {
-            Logger::notice('User identity: ' .  $eduPersonUniqueId . ', service: ' . $spEntityId .
+            Logger::notice('User identity: ' . $eduPersonUniqueId . ', service: ' . $spEntityId .
                 ', external identity: ' . $sourceIdPEppn . ' from ' . $sourceIdPEntityId);
         }
-
     }
-
 }
diff --git a/lib/Config.php b/lib/Config.php
new file mode 100644
index 0000000000000000000000000000000000000000..7e133907e98ca20842956672744906d863042c91
--- /dev/null
+++ b/lib/Config.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
+namespace SimpleSAML\Module\proxystatistics;
+
+use SimpleSAML\Configuration;
+
+class Config
+{
+    public const CONFIG_FILE_NAME = 'module_proxystatistics.php';
+
+    public const MODE_IDP = 'IDP';
+
+    public const MODE_SP = 'SP';
+
+    public const SIDES = [self::MODE_IDP, self::MODE_SP];
+
+    public const MODE_PROXY = 'PROXY';
+
+    private const STORE = 'store';
+
+    private const MODE = 'mode';
+
+    private const USER_ID_ATTRIBUTE = 'userIdAttribute';
+
+    private const REQUIRE_AUTH_SOURCE = 'requireAuth.source';
+
+    private const KEEP_PER_USER = 'keepPerUser';
+
+    private $config;
+
+    private $store;
+
+    private $mode;
+
+    private static $instance = null;
+
+    private function __construct()
+    {
+        $this->config = Configuration::getConfig(self::CONFIG_FILE_NAME);
+        $this->store = $this->config->getConfigItem(self::STORE, null);
+        $this->tables = $this->config->getArray('tables', []);
+        $this->mode = $this->config->getValueValidate(self::MODE, ['PROXY', 'IDP', 'SP'], 'PROXY');
+    }
+
+    private function __clone()
+    {
+    }
+
+    public static function getInstance()
+    {
+        if (self::$instance === null) {
+            self::$instance = new self();
+        }
+        return self::$instance;
+    }
+
+    public function getMode()
+    {
+        return $this->mode;
+    }
+
+    public function getTables()
+    {
+        return $this->tables;
+    }
+
+    public function getStore()
+    {
+        return $this->store;
+    }
+
+    public function getIdAttribute()
+    {
+        return $this->config->getString(self::USER_ID_ATTRIBUTE, 'uid');
+    }
+
+    public function getSideInfo($side)
+    {
+        assert(in_array($side, [self::SIDES], true));
+        return array_merge(['name' => '', 'id' => ''], $this->config->getArray($side, []));
+    }
+
+    public function getRequiredAuthSource()
+    {
+        return $this->config->getString(self::REQUIRE_AUTH_SOURCE, '');
+    }
+
+    public function getKeepPerUser()
+    {
+        return $this->config->getIntegerRange(self::KEEP_PER_USER, 31, 1827, 31);
+    }
+}
diff --git a/lib/DatabaseCommand.php b/lib/DatabaseCommand.php
new file mode 100644
index 0000000000000000000000000000000000000000..a4406e345d67b317cd41ec5d08e13df1e7182d4f
--- /dev/null
+++ b/lib/DatabaseCommand.php
@@ -0,0 +1,281 @@
+<?php
+
+/**
+ * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
+namespace SimpleSAML\Module\proxystatistics;
+
+use PDO;
+use SimpleSAML\Database;
+use SimpleSAML\Logger;
+
+class DatabaseCommand
+{
+    public const TABLE_SUM = 'statistics_sums';
+
+    private const TABLE_PER_USER = 'statistics_per_user';
+
+    private const TABLE_IDP = 'statistics_idp';
+
+    private const TABLE_SP = 'statistics_sp';
+
+    private const TABLE_SIDES = [
+        Config::MODE_IDP => self::TABLE_IDP,
+        Config::MODE_SP => self::TABLE_SP,
+    ];
+
+    private const TABLE_IDS = [
+        self::TABLE_IDP => 'idpId',
+        self::TABLE_SP => 'spId',
+    ];
+
+    private $tables = [
+        self::TABLE_SUM => self::TABLE_SUM,
+        self::TABLE_PER_USER => self::TABLE_PER_USER,
+        self::TABLE_IDP => self::TABLE_IDP,
+        self::TABLE_SP => self::TABLE_SP,
+    ];
+
+    private $config;
+
+    private $conn = null;
+
+    private $mode;
+
+    public function __construct()
+    {
+        $this->config = Config::getInstance();
+        $this->conn = Database::getInstance($this->config->getStore());
+        assert($this->conn !== null);
+        $this->tables = array_merge($this->tables, $this->config->getTables());
+        $this->mode = $this->config->getMode();
+    }
+
+    public static function prependColon($str)
+    {
+        return ':' . $str;
+    }
+
+    public function insertLogin(&$request, &$date)
+    {
+        $entities = $this->getEntities($request);
+
+        foreach (Config::SIDES as $side) {
+            if (empty($entities[$side]['id'])) {
+                Logger::error('idpEntityId or spEntityId is empty and login log was not inserted into the database.');
+                return;
+            }
+        }
+
+        $idAttribute = $this->config->getIdAttribute();
+        $userId = isset($request['Attributes'][$idAttribute]) ? $request['Attributes'][$idAttribute][0] : '';
+
+        $ids = [];
+        foreach (self::TABLE_SIDES as $side => $table) {
+            $tableId = self::TABLE_IDS[$table];
+            $ids[$tableId] = $this->getIdFromIdentifier($table, $entities[$side], $tableId);
+        }
+
+        if ($this->writeLogin($date, $ids, $userId) === false) {
+            Logger::error('The login log was not inserted.');
+        }
+    }
+
+    public function getNameById($side, $id)
+    {
+        $table = self::TABLE_SIDES[$side];
+        return $this->read(
+            'SELECT IFNULL(name, identifier) ' .
+            'FROM ' . $this->tables[$table] . ' ' .
+            'WHERE ' . self::TABLE_IDS[$table] . '=:id',
+            ['id' => $id]
+        )->fetchColumn();
+    }
+
+    public function getLoginCountPerDay($days, $where = [])
+    {
+        $params = [];
+        $query = 'SELECT UNIX_TIMESTAMP(STR_TO_DATE(CONCAT(year,"-",month,"-",day), "%Y-%m-%d")) AS day, ' .
+                 'logins AS count, users ' .
+                 'FROM ' . $this->tables[self::TABLE_SUM] . ' ' .
+                 'WHERE ';
+        $where = array_merge([Config::MODE_SP => null, Config::MODE_IDP => null], $where);
+        self::addWhereId($where, $query, $params);
+        self::addDaysRange($days, $query, $params);
+        $query .= //'GROUP BY day ' .
+                  'ORDER BY day ASC';
+
+        return $this->read($query, $params)->fetchAll(PDO::FETCH_ASSOC);
+    }
+
+    public function getAccessCount($side, $days, $where = [])
+    {
+        $table = self::TABLE_SIDES[$side];
+        $params = [];
+        $query = 'SELECT IFNULL(name,identifier) AS name, ' . self::TABLE_IDS[$table] . ', SUM(logins) AS count ' .
+                 'FROM ' . $this->tables[$table] . ' ' .
+                 'LEFT OUTER JOIN ' . $this->tables[self::TABLE_SUM] . ' ' .
+                 'USING (' . self::TABLE_IDS[$table] . ') ' .
+                 'WHERE ';
+        self::addWhereId($where, $query, $params);
+        self::addDaysRange($days, $query, $params);
+        $query .= 'GROUP BY ' . self::TABLE_IDS[$table] . ' ';
+        $query .= 'ORDER BY SUM(logins) DESC';
+
+        return $this->read($query, $params)->fetchAll(PDO::FETCH_NUM);
+    }
+
+    public function aggregate()
+    {
+        foreach ([self::TABLE_IDS[self::TABLE_IDP], null] as $idpId) {
+            foreach ([self::TABLE_IDS[self::TABLE_SP], null] as $spId) {
+                $ids = [$idpId, $spId];
+                $msg = 'Aggregating daily statistics per ' . implode(' and ', array_filter($ids));
+                Logger::info($msg);
+                $query = 'INSERT INTO ' . $this->tables[self::TABLE_SUM] . ' '
+                    . 'SELECT NULL, YEAR(`day`), MONTH(`day`), DAY(`day`), ';
+                foreach ($ids as $id) {
+                    $query .= ($id === null ? '0' : $id) . ',';
+                }
+                $query .= 'SUM(logins), COUNT(DISTINCT user) '
+                    . 'FROM ' . $this->tables[self::TABLE_PER_USER] . ' '
+                    . 'WHERE day<DATE(NOW()) '
+                    . 'GROUP BY ' . self::getAggregateGroupBy($ids) . ' '
+                    . 'ON DUPLICATE KEY UPDATE id=id;';
+                // do nothing if row already exists
+                if (!$this->conn->write($query)) {
+                    Logger::warning($msg . ' failed');
+                }
+            }
+        }
+
+        $keepPerUserDays = $this->config->getKeepPerUser();
+
+        $msg = 'Deleting detailed statistics';
+        Logger::info($msg);
+        // INNER JOIN ensures that only aggregated stats are deleted
+        if (
+            !$this->conn->write(
+                'DELETE u FROM ' . $this->tables[self::TABLE_PER_USER] . ' AS u '
+                . 'INNER JOIN ' . $this->tables[self::TABLE_SUM] . ' AS s '
+                . 'ON YEAR(u.`day`)=s.`year` AND MONTH(u.`day`)=s.`month` AND DAY(u.`day`)=s.`day`'
+                . 'WHERE u.`day` < CURDATE() - INTERVAL :days DAY',
+                ['days' => $keepPerUserDays]
+            )
+        ) {
+            Logger::warning($msg . ' failed');
+        }
+    }
+
+    private function read($query, $params)
+    {
+        return $this->conn->read($query, $params);
+    }
+
+    private static function addWhereId($where, &$query, &$params)
+    {
+        assert(count(array_filter($where)) <= 1); //placeholder would be overwritten
+        $parts = [];
+        foreach ($where as $side => $value) {
+            $table = self::TABLE_SIDES[$side];
+            $column = self::TABLE_IDS[$table];
+            $part = $column;
+            if ($value === null) {
+                $part .= '=0';
+            } else {
+                $part .= '=:id';
+                $params['id'] = $value;
+            }
+            $parts[] = $part;
+        }
+        if (empty($parts)) {
+            $parts[] = '1=1';
+        }
+        $query .= implode(' AND ', $parts);
+        $query .= ' ';
+    }
+
+    private function writeLogin($date, $ids, $user)
+    {
+        $params = array_merge($ids, [
+            'day' => $date->format('Y-m-d'),
+            'logins' => 1,
+            'user' => $user,
+        ]);
+        $fields = array_keys($params);
+        $placeholders = array_map(['self', 'prependColon'], $fields);
+        $query = 'INSERT INTO ' . $this->tables[self::TABLE_PER_USER] . ' (' . implode(', ', $fields) . ')' .
+                 ' VALUES (' . implode(', ', $placeholders) . ') ON DUPLICATE KEY UPDATE logins = logins + 1';
+
+        return $this->conn->write($query, $params);
+    }
+
+    private function getEntities($request)
+    {
+        $entities = [
+            Config::MODE_IDP => [],
+            Config::MODE_SP => [],
+        ];
+        if ($this->mode !== Config::MODE_IDP) {
+            $entities[Config::MODE_IDP]['id'] = $request['saml:sp:IdP'];
+            $entities[Config::MODE_IDP]['name'] = $request['Attributes']['sourceIdPName'][0];
+        }
+        if ($this->mode !== Config::MODE_SP) {
+            $entities[Config::MODE_SP]['id'] = $request['Destination']['entityid'];
+            $entities[Config::MODE_SP]['name'] = $request['Destination']['name']['en'] ?? '';
+        }
+
+        if ($this->mode !== Config::MODE_PROXY) {
+            $entities[$this->mode] = $this->config->getSideInfo($this->mode);
+            if (empty($entities[$this->mode]['id']) || empty($entities[$this->mode]['name'])) {
+                Logger::error('Invalid configuration (id, name) for ' . $this->mode);
+            }
+        }
+
+        return $entities;
+    }
+
+    private function getIdFromIdentifier($table, $entity, $idColumn)
+    {
+        $identifier = $entity['id'];
+        $name = $entity['name'];
+        $this->conn->write(
+            'INSERT INTO ' . $this->tables[$table]
+            . '(identifier, name) VALUES (:identifier, :name1) ON DUPLICATE KEY UPDATE name = :name2',
+            ['identifier' => $identifier, 'name1' => $name, 'name2' => $name]
+        );
+        return $this->read('SELECT ' . $idColumn . ' FROM ' . $this->tables[$table]
+            . ' WHERE identifier=:identifier', ['identifier' => $identifier])
+            ->fetchColumn();
+    }
+
+    private static function addDaysRange($days, &$query, &$params, $not = false)
+    {
+        if ($days !== 0) {    // 0 = all time
+            if (stripos($query, 'WHERE') === false) {
+                $query .= 'WHERE';
+            } else {
+                $query .= 'AND';
+            }
+            $query .= ' CONCAT(year,"-",LPAD(month,2,"00"),"-",LPAD(day,2,"00")) ';
+            if ($not) {
+                $query .= 'NOT ';
+            }
+            $query .= 'BETWEEN CURDATE() - INTERVAL :days DAY AND CURDATE() ';
+            $params['days'] = $days;
+        }
+    }
+
+    private static function getAggregateGroupBy($ids)
+    {
+        $columns = ['day'];
+        foreach ($ids as $id) {
+            if ($id !== null) {
+                $columns[] = $id;
+            }
+        }
+        return '`' . implode('`,`', $columns) . '`';
+    }
+}
diff --git a/lib/Templates.php b/lib/Templates.php
new file mode 100644
index 0000000000000000000000000000000000000000..308b981992565aebe322a6794e9859b2aeb828ca
--- /dev/null
+++ b/lib/Templates.php
@@ -0,0 +1,256 @@
+<?php
+
+/**
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
+ */
+
+namespace SimpleSAML\Module\proxystatistics;
+
+use SimpleSAML\Configuration;
+use SimpleSAML\Logger;
+use SimpleSAML\Module;
+use SimpleSAML\XHTML\Template;
+
+class Templates
+{
+    private const INSTANCE_NAME = 'instance_name';
+
+    public static function showProviders($side, $tab)
+    {
+        assert(in_array($side, ['identity', 'service'], true));
+
+        $t = new Template(Configuration::getInstance(), 'proxystatistics:providers-tpl.php');
+        $t->data['side'] = $side;
+        $t->data['tab'] = $tab;
+        $t->show();
+    }
+
+    public static function pieChart($id)
+    {
+        ?>
+        <div class="pie-chart-container row">
+            <div class="canvas-container col-md-7">
+                <canvas id="<?php echo $id; ?>" class="pieChart chart-<?php echo $id; ?>"></canvas>
+            </div>
+            <div class="legend-container col-md-5"></div>
+        </div>
+        <?php
+    }
+
+    public static function timeRange($vars = [])
+    {
+        $t = new Template(Configuration::getInstance(), 'proxystatistics:timeRange-tpl.php');
+        $t->data['lastDays'] = self::getSelectedTimeRange();
+        foreach ($vars as $var => $value) {
+            $t->data[$var] = $value;
+        }
+
+        $t->show();
+    }
+
+    public static function loginsDashboard()
+    {
+        $t = new Template(Configuration::getInstance(), 'proxystatistics:loginsDashboard-tpl.php');
+        $t->show();
+    }
+
+    public static function showDetail($side)
+    {
+        $t = new Template(Configuration::getInstance(), 'proxystatistics:detail-tpl.php');
+
+        $lastDays = self::getSelectedTimeRange();
+        $id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]);
+        $t->data['id'] = $id;
+
+        $t->data['detailGraphClass'] = '';
+        if (Config::getInstance()->getMode() === Utils::theOther(Config::SIDES, $side)) {
+            $t->data['detailGraphClass'] = 'hidden';
+        }
+
+        self::headIncludes($t);
+
+        $dbCmd = new DatabaseCommand();
+        $t->data['head'] .= Utils::metaData(
+            'loginCountPerDay',
+            $dbCmd->getLoginCountPerDay($lastDays, [$side => $id])
+        );
+        $t->data['head'] .= Utils::metaData(
+            'accessCounts',
+            $dbCmd->getAccessCount(Utils::theOther(Config::SIDES, $side), $lastDays, [$side => $id])
+        );
+
+        $translations = [
+            'count' => $t->t('{proxystatistics:stats:count}'),
+        ];
+        foreach (Config::SIDES as $s) {
+            $translations['tables_' . $s] = $t->t('{proxystatistics:stats:side_' . $s . '}');
+        }
+        $t->data['head'] .= Utils::metaData('translations', $translations);
+
+        $name = $dbCmd->getNameById($side, $id);
+        $t->data['header'] = $t->t('{proxystatistics:stats:' . $side . 'Detail_header_name}') . $name;
+
+        $t->data['htmlinject']['htmlContentPost'][]
+            = '<script type="text/javascript" src="' . Module::getModuleUrl('proxystatistics/index.js') . '"></script>';
+
+        $t->data['side'] = $side;
+        $t->data['other_side'] = Utils::theOther(Config::SIDES, $side);
+
+        $t->show();
+    }
+
+    public static function showIndex()
+    {
+        $config = Config::getInstance();
+
+        $authSource = $config->getRequiredAuthSource();
+        if ($authSource) {
+            $as = new \SimpleSAML\Auth\Simple($authSource);
+            $as->requireAuth();
+        }
+
+        $t = new Template(Configuration::getInstance(), 'proxystatistics:index-tpl.php');
+        $lastDays = self::getSelectedTimeRange();
+
+        $t->data['tab'] = filter_input(
+            INPUT_GET,
+            'tab',
+            FILTER_VALIDATE_INT,
+            ['options' => ['default' => 0, 'min_range' => 0, 'max_range' => 2]]
+        ); // indexed from 0
+
+        $t->data['tabsAttributes'] = [
+            'PROXY' => 'id="tab-1" href="summary.php?lastDays=' . $lastDays . '"',
+            'IDP' => 'id="tab-2" href="identityProviders.php?lastDays=' . $lastDays . '"',
+            'SP' => 'id="tab-3" href="serviceProviders.php?lastDays=' . $lastDays . '"',
+        ];
+        $mode = $config->getMode();
+        if ($mode !== Config::MODE_PROXY) {
+            $t->data['tabsAttributes'][$mode] = 'class="hidden" ' . $t->data['tabsAttributes'][$mode];
+        }
+
+        $t->data['header'] = $t->t('{proxystatistics:stats:statistics_header}');
+        $instanceName = Configuration::getInstance()->getString(self::INSTANCE_NAME, null);
+        if ($instanceName !== null) {
+            $t->data['header'] = $instanceName . ' ' . $t->data['header'];
+        } else {
+            Logger::warning('Missing configuration: config.php - instance_name is not set.');
+        }
+
+        self::headIncludes($t);
+
+        $dbCmd = new DatabaseCommand();
+        $t->data['head'] .= Utils::metaData(
+            'loginCountPerDay',
+            $dbCmd->getLoginCountPerDay($lastDays)
+        );
+
+        $translations = [
+            'count' => $t->t('{proxystatistics:stats:count}'),
+            'other' => $t->t('{proxystatistics:stats:other}'),
+            'of_logins' => $t->t('{proxystatistics:stats:of_logins}'),
+            'of_users' => $t->t('{proxystatistics:stats:of_users}'),
+        ];
+        foreach (Config::SIDES as $side) {
+            $otherSide = Utils::theOther(Config::SIDES, $side);
+            $t->data['head'] .= Utils::metaData(
+                'loginCountPer' . $side,
+                $dbCmd->getAccessCount($side, $lastDays, [$otherSide => null])
+            );
+            $translations['tables_' . $side] = $t->t('{proxystatistics:stats:side_' . $side . '}');
+        }
+
+        $t->data['head'] .= Utils::metaData('translations', $translations);
+
+        $t->show();
+    }
+
+    public static function showLegend($t, $side)
+    {
+        $mode = Config::getInstance()->getMode();
+        echo $t->t(
+            '{proxystatistics:stats:chart_legend}',
+            [
+                '!side_of' => $t->t('{proxystatistics:stats:chart_legend_side_of_' . $side . '}'),
+                '!side_on' => $t->t('{proxystatistics:stats:chart_legend_side_on_' . $side . '}'),
+            ]
+        );
+        if ($side === Config::MODE_SP && $mode !== Config::MODE_SP) {
+            echo ' ';
+            echo $t->t(
+                '{proxystatistics:stats:first_access_only}',
+                [
+                    '!through_mode' => $t->t('{proxystatistics:stats:through_mode_' . $mode . '}'),
+                ]
+            );
+        }
+    }
+
+    public static function showSummary()
+    {
+        $t = new Template(Configuration::getInstance(), 'proxystatistics:summary-tpl.php');
+        $t->data['tab'] = 0;
+
+        $mode = Config::getInstance()->getMode();
+        $t->data['mode'] = $mode;
+        $t->data['summaryGraphs'] = [];
+        if ($mode === Config::MODE_PROXY) {
+            foreach (Config::SIDES as $side) {
+                $t->data['summaryGraphs'][$side] = [];
+                $t->data['summaryGraphs'][$side]['Providers'] = 'col-md-6 graph';
+                $t->data['summaryGraphs'][$side]['ProvidersLegend'] = 'col-md-12';
+                $t->data['summaryGraphs'][$side]['ProvidersGraph'] = 'col-md-12';
+            }
+        } else {
+            $side = $mode;
+            $t->data['summaryGraphs'][$side] = [];
+            $t->data['summaryGraphs'][$side]['Providers'] = 'hidden';
+            $t->data['summaryGraphs'][$side]['ProvidersLegend'] = '';
+            $t->data['summaryGraphs'][$side]['ProvidersGraph'] = '';
+            $otherSide = Utils::theOther(Config::SIDES, $side);
+            $t->data['summaryGraphs'][$otherSide] = [];
+            $t->data['summaryGraphs'][$otherSide]['Providers'] = 'col-md-12 graph';
+            $t->data['summaryGraphs'][$otherSide]['ProvidersLegend'] = 'col-md-6';
+            $t->data['summaryGraphs'][$otherSide]['ProvidersGraph'] = 'col-md-6 col-md-offset-3';
+        }
+
+        $t->show();
+    }
+
+    private static function getSelectedTimeRange()
+    {
+        return filter_input(
+            INPUT_GET,
+            'lastDays',
+            FILTER_VALIDATE_INT,
+            ['options' => ['default' => 0, 'min_range' => 0]]
+        );
+    }
+
+    private static function headIncludes($t)
+    {
+        $t->data['jquery'] = ['core' => true, 'ui' => true, 'css' => true];
+        $t->data['head'] = '';
+        $t->data['head'] .= '<link rel="stylesheet"  media="screen" type="text/css" href="' .
+            Module::getModuleUrl('proxystatistics/assets/css/bootstrap.min.css') . '" />';
+        $t->data['head'] .= '<link rel="stylesheet"  media="screen" type="text/css" href="' .
+            Module::getModuleUrl('proxystatistics/assets/css/statisticsproxy.css') . '" />';
+        $t->data['head'] .= '<link rel="stylesheet" type="text/css" href="' .
+            Module::getModuleUrl('proxystatistics/assets/css/Chart.min.css') . '">';
+        $t->data['head'] .= '<script type="text/javascript" src="' .
+            Module::getModuleUrl('proxystatistics/assets/js/moment.min.js') . '"></script>';
+        if ($t->getLanguage() === 'cs') {
+            $t->data['head'] .= '<script type="text/javascript" src="' .
+                Module::getModuleUrl('proxystatistics/assets/js/moment.cs.min.js') . '"></script>';
+        }
+        $t->data['head'] .= '<script type="text/javascript" src="' .
+            Module::getModuleUrl('proxystatistics/assets/js/Chart.min.js') . '"></script>';
+        $t->data['head'] .= '<script type="text/javascript" src="' .
+            Module::getModuleUrl('proxystatistics/assets/js/hammer.min.js') . '"></script>';
+        $t->data['head'] .= '<script type="text/javascript" src="' .
+            Module::getModuleUrl('proxystatistics/assets/js/chartjs-plugin-zoom.min.js') . '"></script>';
+        $t->data['head'] .= '<script type="text/javascript" src="' .
+            Module::getModuleUrl('proxystatistics/assets/js/index.js') . '"></script>';
+    }
+}
diff --git a/lib/Utils.php b/lib/Utils.php
new file mode 100644
index 0000000000000000000000000000000000000000..b833ccd7e66546a1d7828efc488099ba3b45ede3
--- /dev/null
+++ b/lib/Utils.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
+namespace SimpleSAML\Module\proxystatistics;
+
+class Utils
+{
+    public static function theOther($arr, $val)
+    {
+        return current(array_diff($arr, [$val]));
+    }
+
+    public static function metaData($id, $data)
+    {
+        return '<meta name="' . $id . '" id="' . $id . '" ' .
+        'content="' . htmlspecialchars(json_encode($data, JSON_NUMERIC_CHECK)) . '">';
+    }
+}
diff --git a/scripts/migrate_2.0.php b/scripts/migrate_2.0.php
index 7f3473b246b89fa36cba4889ab35eccc6d7e607d..982c4656201b978dbb6a72a761008f3885cea76f 100644
--- a/scripts/migrate_2.0.php
+++ b/scripts/migrate_2.0.php
@@ -1,6 +1,5 @@
 <?php
 /**
- *
  * Script for migrate statistics data from version < 1.6.x to version > 2.0.0
  *
  * You need firstly export the tables identityProviders and serviceProviders into two separate CSV files.
@@ -23,7 +22,7 @@ $serviceProvidersFileName = '';
 $resultFileName = '';
 
 if (empty($identityProvidersFileName) || empty($serviceProvidersFileName) || empty($resultFileName)) {
-    exit("One of required attributes is empty." . PHP_EOL);
+    exit('One of required attributes is empty.' . PHP_EOL);
 }
 
 $tableName = 'statistics';
@@ -32,11 +31,11 @@ $result = '';
 $line = null;
 
 // Identity providers part
-$file = fopen($identityProvidersFileName, "r");
+$file = fopen($identityProvidersFileName, 'r');
 
 while (!feof($file)) {
-    $line = (fgetcsv($file));
-    if ($line != null) {
+    $line = fgetcsv($file);
+    if ($line !== null) {
         $lineInsert = 'INSERT INTO ' . $tableName . '(year, month, day, sourceIdp, service, count) ' .
             'VALUES(' . $line[0] . ', ' . $line[1] . ', ' . $line[2] . ', "' . $line[3] . '","" , ' . $line[4] . ');' .
             PHP_EOL;
@@ -47,11 +46,11 @@ while (!feof($file)) {
 fclose($file);
 
 // Service providers part
-$file = fopen($serviceProvidersFileName, "r");
+$file = fopen($serviceProvidersFileName, 'r');
 
 while (!feof($file)) {
-    $line = (fgetcsv($file));
-    if ($line != null) {
+    $line = fgetcsv($file);
+    if ($line !== null) {
         $lineInsert = 'INSERT INTO ' . $tableName . '(year, month, day, sourceIdp, service, count) ' .
             'VALUES(' . $line[0] . ', ' . $line[1] . ', ' . $line[2] . ', "", "' . $line[3] . '", ' . $line[4] . ');' .
             PHP_EOL;
diff --git a/scripts/migrate_4.0.sql b/scripts/migrate_4.0.sql
new file mode 100644
index 0000000000000000000000000000000000000000..dbfbbc8b5909e94a0b06b77781400be5b607e840
--- /dev/null
+++ b/scripts/migrate_4.0.sql
@@ -0,0 +1,221 @@
+# import
+INSERT INTO statistics_idp (`identifier`, `name`)
+SELECT
+  `entityID`,
+  `name`
+FROM
+  identityProvidersMap;
+
+INSERT INTO statistics_idp (`identifier`, `name`)
+SELECT
+  DISTINCT `sourceIdp`,
+  `sourceIdp`
+FROM
+  statistics_detail
+WHERE
+  `sourceIdp` NOT IN (
+    SELECT
+      `identifier`
+    FROM
+      statistics_idp
+  );
+
+INSERT INTO statistics_sp (`identifier`, `name`)
+SELECT
+  `identifier`,
+  `name`
+FROM
+  serviceProvidersMap;
+
+INSERT INTO statistics_sp (`identifier`, `name`)
+SELECT
+  DISTINCT `service`,
+  `service`
+FROM
+  statistics_detail
+WHERE
+  `service` NOT IN (
+    SELECT
+      `identifier`
+    FROM
+      statistics_sp
+  );
+
+INSERT INTO statistics_per_user (
+  `day`, `idpId`, `spId`, `user`, `logins`
+)
+SELECT
+  STR_TO_DATE(
+    CONCAT(`year`, '-', `month`, '-', `day`),
+    '%Y-%m-%d'
+  ),
+  `idpId`,
+  `spId`,
+  `user`,
+  `count`
+FROM
+  statistics_detail
+  JOIN statistics_idp ON statistics_detail.sourceIdp = statistics_idp.identifier
+  JOIN statistics_sp ON statistics_detail.service = statistics_sp.identifier
+GROUP BY
+  `year`,
+  `month`,
+  `day`,
+  `sourceIdp`,
+  `service`,
+  `user`;
+
+# aggregation
+INSERT INTO statistics_sums
+SELECT
+  NULL,
+  YEAR(`day`),
+  MONTH(`day`),
+  DAY(`day`),
+  idpId,
+  spId,
+  SUM(logins),
+  COUNT(DISTINCT user) AS users
+FROM
+  statistics_per_user
+GROUP BY
+  `day`,
+  idpId,
+  spId
+HAVING day < DATE(NOW());
+
+INSERT INTO statistics_sums
+SELECT
+  NULL,
+  YEAR(`day`),
+  MONTH(`day`),
+  DAY(`day`),
+  0,
+  spId,
+  SUM(logins),
+  COUNT(DISTINCT user) AS users
+FROM
+  statistics_per_user
+GROUP BY
+  `day`,
+  spId
+HAVING day < DATE(NOW());
+
+INSERT INTO statistics_sums
+SELECT
+  NULL,
+  YEAR(`day`),
+  MONTH(`day`),
+  DAY(`day`),
+  idpId,
+  0,
+  SUM(logins),
+  COUNT(DISTINCT user) AS users
+FROM
+  statistics_per_user
+GROUP BY
+  `day`,
+  idpId
+HAVING day < DATE(NOW());
+
+INSERT INTO statistics_sums
+SELECT
+  NULL,
+  YEAR(`day`),
+  MONTH(`day`),
+  DAY(`day`),
+  0,
+  0,
+  SUM(logins),
+  COUNT(DISTINCT user) AS users
+FROM
+  statistics_per_user
+GROUP BY
+  `day`
+HAVING day < DATE(NOW());
+
+
+# add older stats
+INSERT INTO statistics_sums (`year`, `month`, `day`, `idpId`, `spId`, `logins`, `users`)
+SELECT
+  `year`,
+  `month`,
+  `day`,
+  `idpId`,
+  `spId`,
+  `count`,
+  NULL
+FROM
+  statistics
+  JOIN statistics_idp ON statistics.sourceIdp = statistics_idp.identifier
+  JOIN statistics_sp ON statistics.service = statistics_sp.identifier
+GROUP BY
+  `year`,
+  `month`,
+  `day`,
+  `sourceIdp`,
+  `service`
+ON DUPLICATE KEY UPDATE id=id;
+# or if you want to merge, ON DUPLICATE KEY UPDATE logins=logins+VALUES(logins)
+
+INSERT INTO statistics_sums (`year`, `month`, `day`, `idpId`, `spId`, `logins`, `users`)
+SELECT
+  `year`,
+  `month`,
+  `day`,
+  0,
+  `spId`,
+  SUM(`count`),
+  NULL
+FROM
+  statistics
+  JOIN statistics_idp ON statistics.sourceIdp = statistics_idp.identifier
+  JOIN statistics_sp ON statistics.service = statistics_sp.identifier
+GROUP BY
+  `year`,
+  `month`,
+  `day`,
+  `service`
+ON DUPLICATE KEY UPDATE id=id;
+# or if you want to merge, ON DUPLICATE KEY UPDATE logins=logins+VALUES(logins)
+
+INSERT INTO statistics_sums (`year`, `month`, `day`, `idpId`, `spId`, `logins`, `users`)
+SELECT
+  `year`,
+  `month`,
+  `day`,
+  `idpId`,
+  0,
+  SUM(`count`),
+  NULL
+FROM
+  statistics
+  JOIN statistics_idp ON statistics.sourceIdp = statistics_idp.identifier
+  JOIN statistics_sp ON statistics.service = statistics_sp.identifier
+GROUP BY
+  `year`,
+  `month`,
+  `day`,
+  `sourceIdp`
+ON DUPLICATE KEY UPDATE id=id;
+# or if you want to merge, ON DUPLICATE KEY UPDATE logins=logins+VALUES(logins)
+
+INSERT INTO statistics_sums (`year`, `month`, `day`, `idpId`, `spId`, `logins`, `users`)
+SELECT
+  `year`,
+  `month`,
+  `day`,
+  0,
+  0,
+  SUM(`count`),
+  NULL
+FROM
+  statistics
+  JOIN statistics_idp ON statistics.sourceIdp = statistics_idp.identifier
+  JOIN statistics_sp ON statistics.service = statistics_sp.identifier
+GROUP BY
+  `year`,
+  `month`,
+  `day`
+ON DUPLICATE KEY UPDATE id=id;
+# or if you want to merge, ON DUPLICATE KEY UPDATE logins=logins+VALUES(logins)
diff --git a/templates/charts.include.php b/templates/charts.include.php
deleted file mode 100644
index 91c401b7026bfb02082adf325bbaf66f5702fa19..0000000000000000000000000000000000000000
--- a/templates/charts.include.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-use SimpleSAML\Module;
-
-$this->data['jquery'] = ['core' => true, 'ui' => true, 'css' => true];
-$this->data['head'] = '';
-$this->data['head'] .= '<link rel="stylesheet"  media="screen" type="text/css" href="' .
-    Module::getModuleUrl('proxystatistics/bootstrap.min.css') . '" />';
-$this->data['head'] .= '<link rel="stylesheet"  media="screen" type="text/css" href="' .
-    Module::getModuleUrl('proxystatistics/statisticsproxy.css') . '" />';
-$this->data['head'] .= '<link rel="stylesheet" type="text/css" href="' .
-    Module::getModuleUrl('proxystatistics/Chart.min.css') . '">';
-$this->data['head'] .= '<script type="text/javascript" src="' .
-    Module::getModuleUrl('proxystatistics/moment.min.js').'"></script>';
-if ($this->getLanguage() === 'cs') {
-    $this->data['head'] .= '<script type="text/javascript" src="' .
-        Module::getModuleUrl('proxystatistics/moment.cs.min.js').'"></script>';
-}
-$this->data['head'] .= '<script type="text/javascript" src="' .
-    Module::getModuleUrl('proxystatistics/Chart.min.js').'"></script>';
-$this->data['head'] .= '<script type="text/javascript" src="' .
-    Module::getModuleUrl('proxystatistics/hammer.min.js').'"></script>';
-$this->data['head'] .= '<script type="text/javascript" src="' .
-    Module::getModuleUrl('proxystatistics/chartjs-plugin-zoom.min.js').'"></script>';
diff --git a/templates/detail-tpl.php b/templates/detail-tpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..665beee089abd917b6a733ebb4ba1c7bf20ac694
--- /dev/null
+++ b/templates/detail-tpl.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
+use SimpleSAML\Module\proxystatistics\Templates;
+
+$this->includeAtTemplateBase('includes/header.php');
+?>
+    </head>
+    <body>
+        <div class="go-to-stats-btn">
+            <a href="./" class="btn btn-md btn-default">
+                <span class="glyphicon glyphicon-home"></span>
+                <?php echo $this->t('{proxystatistics:stats:back_to_stats}'); ?>
+            </a>
+        </div>
+        <?php Templates::timeRange(['side' => $this->data['side'], 'id' => $this->data['id']]); ?>
+        <h3><?php echo $this->t('{proxystatistics:stats:' . $this->data['side'] . 'Detail_dashboard_header}'); ?></h3>
+        <div class="legend">
+            <div>
+                <?php echo $this->t('{proxystatistics:stats:' . $this->data['side'] . 'Detail_dashboard_legend}'); ?>
+            </div>
+        </div>
+        <?php Templates::loginsDashboard(); ?>
+        <div class="<?php echo $this->data['detailGraphClass'] ?>">
+            <h3><?php echo $this->t('{proxystatistics:stats:' . $this->data['side'] . 'Detail_graph_header}'); ?></h3>
+            <div class="legend">
+                <div>
+                    <?php echo $this->t('{proxystatistics:stats:' . $this->data['side'] . 'Detail_graph_legend}'); ?>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-8">
+                    <?php Templates::pieChart('detail' . $this->data['other_side'] . 'Chart'); ?>
+                </div>
+                <div class="col-md-4">
+                    <div id="detail<?php echo $this->data['other_side']; ?>Table" class="table-container"></div>
+                </div>
+            </div>
+        </div>
+    </body>
+<?php
+$this->includeAtTemplateBase('includes/footer.php');
diff --git a/templates/functions.include.php b/templates/functions.include.php
deleted file mode 100644
index 741628a77069a408dff3cc80d672b8cdaf1ec12a..0000000000000000000000000000000000000000
--- a/templates/functions.include.php
+++ /dev/null
@@ -1,12 +0,0 @@
-<?php
-function pieChart($id)
-{
-    ?>
-    <div class="pie-chart-container row">
-        <div class="canvas-container col-md-7">
-            <canvas id="<?php echo $id;?>" class="pieChart chart-<?php echo $id;?>"></canvas>
-        </div>
-        <div class="legend-container col-md-5"></div>
-    </div>
-    <?php
-}
diff --git a/templates/identityProviders-tpl.php b/templates/identityProviders-tpl.php
deleted file mode 100644
index f822fdb0e5511173ea788505ad7a639651d8cacf..0000000000000000000000000000000000000000
--- a/templates/identityProviders-tpl.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-use SimpleSAML\Module;
-
-/**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- * @author Dominik Baránek <0Baranek.dominik0@gmail.com>
- */
-
-?>
-
-<?php
-require_once 'timeRange.include.php';
-require_once 'functions.include.php';
-?>
-
-<h2><?php echo $this->t('{proxystatistics:Proxystatistics:templates/graphs_id_providers}'); ?></h2>
-<div class="legend">
-    <div><?php echo $this->t('{proxystatistics:Proxystatistics:templates/identityProviders_legend}'); ?></div>
-</div>
-<div class="row">
-    <div class="col-md-8">
-        <?php pieChart('idpsChart'); ?>
-    </div>
-    <div class="col-md-4">
-        <div id="idpsTable" class="table-container"></div>
-    </div>
-</div>
diff --git a/templates/idpDetail-tpl.php b/templates/idpDetail-tpl.php
deleted file mode 100644
index 3c2f1c748f70fde317ca378342df80acd1c16c5d..0000000000000000000000000000000000000000
--- a/templates/idpDetail-tpl.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-
-use SimpleSAML\Module\proxystatistics\Auth\Process\DatabaseCommand;
-use SimpleSAML\Module;
-
-/**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- */
-
-const CONFIG_FILE_NAME = 'config.php';
-const INSTANCE_NAME = 'instance_name';
-
-$lastDays = $this->data['lastDays'];
-$idpEntityId = $this->data['entityId'];
-
-require_once 'charts.include.php';
-require_once 'functions.include.php';
-
-$dbCmd = new DatabaseCommand();
-$this->data['head'] .= '<meta name="loginCountPerDay" id="loginCountPerDay" content="' .
-    htmlspecialchars(json_encode($dbCmd->getLoginCountPerDayForIdp($lastDays, $idpEntityId), JSON_NUMERIC_CHECK))
-    . '">';
-$this->data['head'] .=
-    '<meta name="accessCountForIdentityProviderPerServiceProviders" ' .
-    'id="accessCountForIdentityProviderPerServiceProviders" content="' .
-    htmlspecialchars(json_encode(
-        $dbCmd->getAccessCountForIdentityProviderPerServiceProviders($lastDays, $idpEntityId),
-        JSON_NUMERIC_CHECK
-    )).'">';
-$this->data['head'] .= '<meta name="translations" id="translations" content="'.htmlspecialchars(json_encode([
-    'tables_identity_provider' => $this->t('{proxystatistics:Proxystatistics:templates/tables_identity_provider}'),
-    'tables_service_provider' => $this->t('{proxystatistics:Proxystatistics:templates/tables_service_provider}'),
-    'count' => $this->t('{proxystatistics:Proxystatistics:templates/count}'),
-])).'">';
-
-$idpName = $dbCmd->getIdPNameByEntityId($idpEntityId);
-
-if (!empty($idpName)) {
-    $this->data['header'] = $this->t('{proxystatistics:Proxystatistics:templates/idpDetail_header_name}') . $idpName;
-} else {
-    $this->data['header'] = $this->t('{proxystatistics:Proxystatistics:templates/idpDetail_header_entityId}') .
-        $idpEntityId;
-}
-
-$this->includeAtTemplateBase('includes/header.php');
-
-?>
-    </head>
-    <body>
-    <div class="go-to-stats-btn">
-        <a href="./" class="btn btn-md btn-default">
-            <span class="glyphicon glyphicon-home"></span>
-            <?php echo $this->t('{proxystatistics:Proxystatistics:btn_label_back_to_stats}'); ?>
-        </a>
-    </div>
-
-    <?php
-    require 'timeRange.include.php';
-    ?>
-
-    <h3><?php echo $this->t('{proxystatistics:Proxystatistics:templates/idpDetail_dashboard_header}'); ?></h3>
-
-    <div class="legend">
-        <div><?php echo $this->t('{proxystatistics:Proxystatistics:templates/idpDetail_dashboard_legend}'); ?></div>
-    </div>
-
-    <?php require_once 'loginsDashboard.include.php'; ?>
-
-    <div class="<?php echo $this->data['idpDetailGraphClass'] ?>">
-        <h3><?php echo $this->t('{proxystatistics:Proxystatistics:templates/idpDetail_graph_header}'); ?></h3>
-        <div class="legend">
-            <div><?php echo $this->t('{proxystatistics:Proxystatistics:templates/idpDetail_graph_legend}'); ?></div>
-        </div>
-        <div class="row">
-            <div class="col-md-8">
-                <?php pieChart('accessedSpsChartDetail'); ?>
-            </div>
-            <div class="col-md-4">
-                <div id="accessedSpsTable" class="table-container"></div>
-            </div>
-        </div>
-    </div>
-    </body>
-<?php
-$this->data['htmlinject']['htmlContentPost'][]
-    = '<script type="text/javascript" src="' . Module::getMOduleUrl('proxystatistics/index.js') . '"></script>';
-$this->includeAtTemplateBase('includes/footer.php');
diff --git a/templates/index-tpl.php b/templates/index-tpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb62bf70ada9bd37ecd03b7b9bd6577ae9f0a095
--- /dev/null
+++ b/templates/index-tpl.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
+use SimpleSAML\Module\proxystatistics\Config;
+
+$this->includeAtTemplateBase('includes/header.php');
+?>
+
+<div id="tabdiv" data-activetab="<?php echo htmlspecialchars($this->data['tab']); ?>">
+    <ul class="tabset_tabs" width="100px">
+        <li>
+            <a <?php echo $this->data['tabsAttributes']['PROXY'] ?>>
+                <?php echo $this->t('{proxystatistics:stats:summary}'); ?>
+            </a>
+        </li>
+        <?php foreach (Config::SIDES as $side) : ?>
+        <li>
+            <a <?php echo $this->data['tabsAttributes'][$side] ?>>
+                <?php echo $this->t('{proxystatistics:stats:side' . $side . 'Detail}'); ?>
+            </a>
+        </li>
+        <?php endforeach; ?>
+    </ul>
+</div>
+
+<?php
+$this->includeAtTemplateBase('includes/footer.php');
+?>
diff --git a/templates/loginsDashboard.include.php b/templates/loginsDashboard-tpl.php
similarity index 54%
rename from templates/loginsDashboard.include.php
rename to templates/loginsDashboard-tpl.php
index 4f80b5025c39507410c2de81d7db87c868b639a4..23dcc3b20ac941217921d3a557d7742438361ec9 100644
--- a/templates/loginsDashboard.include.php
+++ b/templates/loginsDashboard-tpl.php
@@ -1,4 +1,12 @@
+<?php
+
+/**
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
+?>
+
 <div class="canvas-container">
-    <canvas id="loginsDashboard"<?php echo $this->getLanguage() === 'cs' ? ' data-locale="cs"' : '';?>
+    <canvas id="loginsDashboard"<?php echo $this->getLanguage() === 'cs' ? ' data-locale="cs"' : ''; ?>
         height="250"></canvas>
 </div>
diff --git a/templates/providers-tpl.php b/templates/providers-tpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..63dd9e832feabcdce141f2c788fef917f996ee1a
--- /dev/null
+++ b/templates/providers-tpl.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
+ * @author Dominik Baránek <0Baranek.dominik0@gmail.com>
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
+use SimpleSAML\Module\proxystatistics\Templates;
+
+?>
+
+<?php Templates::timeRange(['tab' => $this->data['tab']]); ?>
+<h2>
+    <?php
+    echo $this->t('{proxystatistics:stats:side_' . $this->data['side'] . 's}');
+    ?>
+</h2>
+<div class="legend">
+    <div>
+        <?php Templates::showLegend($this, $this->data['side']); ?>
+    </div>
+</div>
+<div class="row">
+    <div class="col-md-8">
+        <?php
+        Templates::pieChart($this->data['side'] . 'Chart');
+        ?>
+    </div>
+    <div class="col-md-4">
+        <div id="<?php echo $this->data['side']; ?>Table" class="table-container"></div>
+    </div>
+</div>
diff --git a/templates/serviceProviders-tpl.php b/templates/serviceProviders-tpl.php
deleted file mode 100644
index 877abff128dded5f78fd5f3f5497b94216395983..0000000000000000000000000000000000000000
--- a/templates/serviceProviders-tpl.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-
-/**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- * @author Dominik Baránek <0Baranek.dominik0@gmail.com>
- */
-
-?>
-
-<?php
-require_once 'timeRange.include.php';
-require_once 'functions.include.php';
-?>
-
-<h2><?php echo $this->t('{proxystatistics:Proxystatistics:templates/graphs_service_providers}'); ?></h2>
-<div class="legend">
-    <div><?php echo $this->t('{proxystatistics:Proxystatistics:templates/serviceProviders_legend}'); ?></div>
-</div>
-<div class="row">
-    <div class="col-md-8">
-        <?php pieChart('spsChart'); ?>
-    </div>
-    <div class="col-md-4">
-        <div id="spsTable" class="table-container"></div>
-    </div>
-</div>
diff --git a/templates/spDetail-tpl.php b/templates/spDetail-tpl.php
deleted file mode 100644
index 058341911ef54bda530f880cb7a94e59adf0f4fa..0000000000000000000000000000000000000000
--- a/templates/spDetail-tpl.php
+++ /dev/null
@@ -1,85 +0,0 @@
-<?php
-
-use SimpleSAML\Module\proxystatistics\Auth\Process\DatabaseCommand;
-use SimpleSAML\Module;
-
-/**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- */
-
-const CONFIG_FILE_NAME = 'config.php';
-const INSTANCE_NAME = 'instance_name';
-
-$lastDays = $this->data['lastDays'];
-$spIdentifier = $this->data['identifier'];
-
-require_once 'charts.include.php';
-require_once 'functions.include.php';
-
-$dbCmd = new DatabaseCommand();
-$this->data['head'] .= '<meta name="loginCountPerDay" id="loginCountPerDay" content="' .
-    htmlspecialchars(json_encode($dbCmd->getLoginCountPerDayForService($lastDays, $spIdentifier), JSON_NUMERIC_CHECK))
-    . '">';
-$this->data['head'] .=
-    '<meta name="accessCountForServicePerIdentityProviders" id="accessCountForServicePerIdentityProviders" content="' .
-    htmlspecialchars(json_encode(
-        $dbCmd->getAccessCountForServicePerIdentityProviders($lastDays, $spIdentifier),
-        JSON_NUMERIC_CHECK
-    )) . '">';
-$this->data['head'] .= '<meta name="translations" id="translations" content="'.htmlspecialchars(json_encode([
-    'tables_identity_provider' => $this->t('{proxystatistics:Proxystatistics:templates/tables_identity_provider}'),
-    'tables_service_provider' => $this->t('{proxystatistics:Proxystatistics:templates/tables_service_provider}'),
-    'count' => $this->t('{proxystatistics:Proxystatistics:templates/count}'),
-])).'">';
-
-$spName = $dbCmd->getSpNameBySpIdentifier($spIdentifier);
-
-if (!empty($spName)) {
-    $this->data['header'] = $this->t('{proxystatistics:Proxystatistics:templates/spDetail_header_name}') .
-        $spName;
-} else {
-    $this->data['header'] = $this->t('{proxystatistics:Proxystatistics:templates/spDetail_header_identifier}') .
-        $spIdentifier;
-}
-
-$this->includeAtTemplateBase('includes/header.php');
-
-?>
-
-    </head>
-    <body>
-    <div class="go-to-stats-btn">
-        <a href="./" class="btn btn-md btn-default"><span class="glyphicon glyphicon-home"></span>
-            <?php echo $this->t('{proxystatistics:Proxystatistics:btn_label_back_to_stats}'); ?>
-        </a>
-    </div>
-
-    <?php require_once 'timeRange.include.php'; ?>
-
-    <h3><?php echo $this->t('{proxystatistics:Proxystatistics:templates/spDetail_dashboard_header}'); ?></h3>
-
-    <div class="legend">
-        <div><?php echo $this->t('{proxystatistics:Proxystatistics:templates/spDetail_dashboard_legend}'); ?></div>
-    </div>
-
-    <?php require_once 'loginsDashboard.include.php'; ?>
-
-    <div class="<?php echo $this->data['spDetailGraphClass'] ?>">
-        <h3><?php echo $this->t('{proxystatistics:Proxystatistics:templates/spDetail_graph_header}'); ?></h3>
-        <div class="legend">
-            <div><?php echo $this->t('{proxystatistics:Proxystatistics:templates/spDetail_graph_legend}'); ?></div>
-        </div>
-        <div class="row">
-            <div class="col-md-8">
-                <?php pieChart('usedIdPsChartDetail'); ?>
-            </div>
-            <div class="col-md-4">
-                <div id="usedIdPsTable" class="table-container"></div>
-            </div>
-        </div>
-    </div>
-    </body>
-<?php
-$this->data['htmlinject']['htmlContentPost'][]
-    = '<script type="text/javascript" src="' . Module::getMOduleUrl('proxystatistics/index.js') . '"></script>';
-$this->includeAtTemplateBase('includes/footer.php');
diff --git a/templates/statistics-tpl.php b/templates/statistics-tpl.php
deleted file mode 100644
index 4bd398b5749da80f48d144134df2668e67c3983d..0000000000000000000000000000000000000000
--- a/templates/statistics-tpl.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-use SimpleSAML\Configuration;
-use SimpleSAML\Module;
-use SimpleSAML\Logger;
-use SimpleSAML\Module\proxystatistics\Auth\Process\DatabaseCommand;
-
-/**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- * @author Dominik Baránek <0Baranek.dominik0@gmail.com>
- */
-
-const CONFIG_FILE_NAME = 'config.php';
-const INSTANCE_NAME = 'instance_name';
-
-$config = Configuration::getConfig(CONFIG_FILE_NAME);
-$instanceName = $config->getString(INSTANCE_NAME, null);
-if (!is_null($instanceName)) {
-    $this->data['header'] = $instanceName . ' ' .
-        $this->t('{proxystatistics:Proxystatistics:templates/statistics_header}');
-} else {
-    $this->data['header'] = $this->t('{proxystatistics:Proxystatistics:templates/statistics_header}');
-    Logger::warning('Missing configuration: config.php - instance_name is not set.');
-}
-
-require_once 'charts.include.php';
-
-if (!isset($this->data['lastDays'])) {
-    $this->data['lastDays'] = 0;
-}
-
-if (!isset($this->data['tab'])) {
-    $this->data['tab'] = 1;
-}
-$dbCmd = new DatabaseCommand();
-$this->data['head'] .= '<meta name="loginCountPerDay" id="loginCountPerDay" content="' .
-    htmlspecialchars(json_encode($dbCmd->getLoginCountPerDay($this->data['lastDays']), JSON_NUMERIC_CHECK))
-    . '">';
-$this->data['head'] .= '<meta name="loginCountPerIdp" id="loginCountPerIdp" content="' .
-    htmlspecialchars(json_encode($dbCmd->getLoginCountPerIdp($this->data['lastDays']), JSON_NUMERIC_CHECK))
-    . '">';
-$this->data['head'] .= '<meta name="accessCountPerService" id="accessCountPerService" content="' .
-    htmlspecialchars(json_encode($dbCmd->getAccessCountPerService($this->data['lastDays']), JSON_NUMERIC_CHECK))
-    . '">';
-$this->data['head'] .= '<meta name="translations" id="translations" content="'.htmlspecialchars(json_encode([
-    'tables_identity_provider' => $this->t('{proxystatistics:Proxystatistics:templates/tables_identity_provider}'),
-    'tables_service_provider' => $this->t('{proxystatistics:Proxystatistics:templates/tables_service_provider}'),
-    'count' => $this->t('{proxystatistics:Proxystatistics:templates/count}'),
-    'other' => $this->t('{proxystatistics:Proxystatistics:templates/other}'),
-])).'">';
-$this->includeAtTemplateBase('includes/header.php');
-?>
-
-<div id="tabdiv" data-activetab="<?php echo htmlspecialchars($this->data['tab']);?>">
-    <ul class="tabset_tabs" width="100px">
-        <li>
-            <a <?php echo $this->data['tabsAttributes']['PROXY'] ?>>
-                <?php echo $this->t('{proxystatistics:Proxystatistics:summary}'); ?>
-            </a>
-        </li>
-        <li>
-            <a <?php echo $this->data['tabsAttributes']['IDP'] ?>>
-                <?php echo $this->t('{proxystatistics:Proxystatistics:templates/statistics-tpl_idpsDetail}'); ?>
-            </a>
-        </li>
-        <li>
-            <a <?php echo $this->data['tabsAttributes']['SP'] ?>>
-                <?php echo $this->t('{proxystatistics:Proxystatistics:templates/statistics-tpl_spsDetail}'); ?>
-            </a>
-        </li>
-    </ul>
-</div>
-
-<?php
-$this->data['htmlinject']['htmlContentPost'][]
-    = '<script type="text/javascript" src="' . Module::getMOduleUrl('proxystatistics/index.js') . '"></script>';
-$this->includeAtTemplateBase('includes/footer.php');
-?>
diff --git a/templates/summary-tpl.php b/templates/summary-tpl.php
index 5a5948648a71ef2a96cf487e784ebdcfda78ca54..8b4bb0721d8d144bc9fa023a9e93928e76463cbf 100644
--- a/templates/summary-tpl.php
+++ b/templates/summary-tpl.php
@@ -1,58 +1,46 @@
 <?php
 
-use SimpleSAML\Module;
-
 /**
  * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
  * @author Dominik Baránek <0Baranek.dominik0@gmail.com>
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
  */
 
+use SimpleSAML\Module\proxystatistics\Config;
+use SimpleSAML\Module\proxystatistics\Templates;
 
-require_once 'timeRange.include.php';
-require_once 'functions.include.php';
 ?>
 
-<h2><?php echo $this->t('{proxystatistics:Proxystatistics:templates/graphs_logins}'); ?></h2>
+<?php Templates::timeRange(['tab' => $this->data['tab']]); ?>
+
+<h2><?php echo $this->t('{proxystatistics:stats:graphs_logins}'); ?></h2>
 <div class="legend-logins">
     <div>
-        <?php echo $this->t('{proxystatistics:Proxystatistics:templates/summary_logins_info}'); ?>
+        <?php echo $this->t('{proxystatistics:stats:summary_logins_info}'); ?>
     </div>
-    <?php require_once 'loginsDashboard.include.php'; ?>
+    <?php Templates::loginsDashboard(); ?>
 </div>
 
 <div class="row tableMaxHeight">
-    <div class="<?php echo $this->data['summaryGraphs']['identityProviders'] ?>">
-        <h2><?php echo $this->t('{proxystatistics:Proxystatistics:templates/graphs_id_providers}'); ?></h2>
-        <div class="row">
-            <div class="<?php echo $this->data['summaryGraphs']['identityProvidersLegend'] ?>">
-                <div class="legend">
-                    <div id="summaryIdp">
-                        <?php echo $this->t('{proxystatistics:Proxystatistics:templates/summary_idps_info}'); ?>
+    <?php foreach (Config::SIDES as $side) : ?>
+        <div class="<?php echo $this->data['summaryGraphs'][$side]['Providers'] ?>">
+            <h2>
+                <?php echo $this->t('{proxystatistics:stats:side_' . $side . 's}'); ?>
+            </h2>
+            <div class="row">
+                <div class="<?php echo $this->data['summaryGraphs'][$side]['ProvidersLegend'] ?>">
+                    <div class="legend">
+                        <div id="summary<?php echo $side; ?>">
+                            <?php Templates::showLegend($this, $side); ?>
+                        </div>
                     </div>
                 </div>
             </div>
-        </div>
-        <div class="row">
-            <div class="<?php echo $this->data['summaryGraphs']['identityProvidersGraph'] ?>">
-                <?php pieChart('idpsChart'); ?>
-            </div>
-        </div>
-    </div>
-    <div class="<?php echo $this->data['summaryGraphs']['serviceProviders'] ?>">
-        <h2><?php echo $this->t('{proxystatistics:Proxystatistics:templates/graphs_service_providers}'); ?></h2>
-        <div class="row">
-            <div class="<?php echo $this->data['summaryGraphs']['serviceProvidersLegend'] ?>">
-                <div class="legend">
-                    <div>
-                        <?php echo $this->t('{proxystatistics:Proxystatistics:templates/summary_sps_info}'); ?>
-                    </div>
+            <div class="row">
+                <div class="<?php echo $this->data['summaryGraphs'][$side]['ProvidersGraph'] ?>">
+                    <?php Templates::pieChart($side . 'Chart'); ?>
                 </div>
             </div>
         </div>
-        <div class="row">
-            <div class="<?php echo $this->data['summaryGraphs']['serviceProvidersGraph'] ?>">
-                <?php pieChart('spsChart'); ?>
-            </div>
-        </div>
-    </div>
+    <?php endforeach; ?>
 </div>
diff --git a/templates/timeRange-tpl.php b/templates/timeRange-tpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..0f7f98c8fe234e033a4c67e8e98f765dbb618a2f
--- /dev/null
+++ b/templates/timeRange-tpl.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
+?>
+<div class="timeRange">
+    <h4><?php echo $this->t('{proxystatistics:stats:select_time_range}'); ?></h4>
+    <form id="dateSelector" method="GET">
+        <?php
+        foreach (['tab', 'side', 'id'] as $var) {
+            if (isset($this->data[$var])) {
+                ?>
+                <input name="<?php echo $var; ?>" type="hidden"
+                    value="<?php echo htmlspecialchars($this->data[$var]); ?>">
+                <?php
+            }
+        }
+        ?>
+        <?php
+        $values = [0 => 'all', 7 => 'week', 30 => 'month', 365 => 'year'];
+        $i = 0;
+        ?>
+        <?php foreach ($values as $value => $str) : ?>
+            <label>
+                <input id="<?php echo $i; ?>" type="radio" name="lastDays" value="<?php echo $value; ?>"
+                        <?php echo $this->data['lastDays'] === $value ? 'checked=true' : '' ?>>
+                <?php echo $this->t('{proxystatistics:stats:time_range_' . $str . '}'); ?>
+            </label>
+            <?php $i++; ?>
+        <?php endforeach; ?>
+    </form>
+</div>
diff --git a/templates/timeRange.include.php b/templates/timeRange.include.php
deleted file mode 100644
index 5fafc5003f70a1106f498b351b22e047b0744820..0000000000000000000000000000000000000000
--- a/templates/timeRange.include.php
+++ /dev/null
@@ -1,20 +0,0 @@
-<div class="timeRange">
-    <h4><?php echo $this->t('{proxystatistics:Proxystatistics:templates_time_range}'); ?></h4>
-    <form id="dateSelector" method="post">
-        <?php if (isset($this->data['tab'])) : ?>
-            <input name="tab" value="<?php echo $this->data['tab'];?>" type="hidden">
-        <?php endif; ?>
-        <?php
-        $values = [0=>'all', 7=>'week', 30=>'month', 365=>'year'];
-        $i = 0;
-        ?>
-        <?php foreach ($values as $value => $str) : ?>
-            <label>
-                <input id="<?php echo $i;?>" type="radio" name="lastDays" value="<?php echo $value;?>"
-                        <?php echo ($this->data['lastDays'] == $value) ? "checked=true" : "" ?>>
-                <?php echo $this->t('{proxystatistics:Proxystatistics:templates/statistics-tpl_'.$str.'}'); ?>
-            </label>
-            <?php $i++; ?>
-        <?php endforeach; ?>
-    </form>
-</div>
diff --git a/www/bootstrap.config.json b/www/assets/bootstrap.config.json
similarity index 100%
rename from www/bootstrap.config.json
rename to www/assets/bootstrap.config.json
diff --git a/www/Chart.min.css b/www/assets/css/Chart.min.css
similarity index 100%
rename from www/Chart.min.css
rename to www/assets/css/Chart.min.css
diff --git a/www/bootstrap.min.css b/www/assets/css/bootstrap.min.css
similarity index 100%
rename from www/bootstrap.min.css
rename to www/assets/css/bootstrap.min.css
diff --git a/www/statisticsproxy.css b/www/assets/css/statisticsproxy.css
similarity index 99%
rename from www/statisticsproxy.css
rename to www/assets/css/statisticsproxy.css
index dee2b43fcc89841d33f8f6dc903cf76698d96ec9..cf481231cd14abb1828e02d07cb7e4fca7e9e913 100644
--- a/www/statisticsproxy.css
+++ b/www/assets/css/statisticsproxy.css
@@ -31,7 +31,7 @@ h1, h2, h3 {
     font-weight: normal;
 }
 
-#summaryIdp {
+#summaryIDP {
     padding-bottom: 20px;
 }
 
diff --git a/www/Chart.min.js b/www/assets/js/Chart.min.js
similarity index 100%
rename from www/Chart.min.js
rename to www/assets/js/Chart.min.js
diff --git a/www/chartjs-plugin-zoom.min.js b/www/assets/js/chartjs-plugin-zoom.min.js
similarity index 100%
rename from www/chartjs-plugin-zoom.min.js
rename to www/assets/js/chartjs-plugin-zoom.min.js
diff --git a/www/hammer.min.js b/www/assets/js/hammer.min.js
similarity index 100%
rename from www/hammer.min.js
rename to www/assets/js/hammer.min.js
diff --git a/www/index.js b/www/assets/js/index.js
similarity index 82%
rename from www/index.js
rename to www/assets/js/index.js
index 251ccf9922de7cfca335628862a0ce76f9e6d5b3..5885f2e9a02bbb30966bbc645cc655bb9efbaa08 100644
--- a/www/index.js
+++ b/www/assets/js/index.js
@@ -6,11 +6,11 @@ function getStatisticsData(name) {
   return $.parseJSON($('#' + name).attr('content'));
 }
 
-function getStatisticsDataYMDC(name) {
+function getStatisticsDataYMDC(name, field) {
   return getStatisticsData(name).map(function mapItemToDate(item) {
     return {
-      t: new Date(item.year, item.month - 1, item.day),
-      y: item.count
+      t: new Date(item.day * 1000),
+      y: item[field]
     };
   });
 }
@@ -19,17 +19,7 @@ function getTranslation(str) {
   return $.parseJSON($('#translations').attr('content'))[str];
 }
 
-function drawLoginsChart(getEl) {
-  var el = getEl();
-  if (!el) return;
-
-  var ctx = el.getContext('2d');
-
-  var data = getStatisticsDataYMDC('loginCountPerDay');
-
-  var minX = data[0].t;
-  var maxX = data[data.length - 1].t;
-
+function extendData(data, minX, maxX) {
   var i = 0;
   var extendedData = [];
   for (var d = new Date(minX); d <= maxX; d.setDate(d.getDate() + 1)) {
@@ -42,7 +32,23 @@ function drawLoginsChart(getEl) {
       throw new Error("Data is not sorted");
     }
   }
-  data = extendedData;
+  return extendedData;
+}
+
+function drawLoginsChart(getEl) {
+  var el = getEl();
+  if (!el) return;
+
+  var ctx = el.getContext('2d');
+
+  var data = getStatisticsDataYMDC('loginCountPerDay', 'count');
+  var data2 = getStatisticsDataYMDC('loginCountPerDay', 'users');
+
+  var minX = Math.min(data[0].t, data2[0].t);
+  var maxX = Math.max(data[data.length - 1].t, data2[data2.length - 1].t);
+
+  data = extendData(data, minX, maxX);
+  data2 = extendData(data2, minX, maxX);
 
   new Chart(ctx, { // eslint-disable-line no-new
     type: 'bar',
@@ -109,15 +115,26 @@ function drawLoginsChart(getEl) {
     "data": {
         "datasets": [
             {
-                "label": "",
-                "data": data,
+                label: getTranslation('of_users'),
+                data: data2,
                 type: 'line',
                 pointRadius: 0,
                 fill: false,
                 lineTension: 0,
                 borderWidth: 2,
-                backgroundColor: '#00F',
-                borderColor: '#00F'
+                backgroundColor: '#3b3eac',
+                borderColor: '#3b3eac'
+            },
+            {
+                label: getTranslation('of_logins'),
+                data: data,
+                type: 'line',
+                pointRadius: 0,
+                fill: false,
+                lineTension: 0,
+                borderWidth: 2,
+                backgroundColor: '#f90',
+                borderColor: '#f90'
             }
         ]
     }
@@ -167,7 +184,7 @@ function processDataForPieChart(data, viewCols) {
   return { data: processedData, other: othersFraction > 0, total: total };
 }
 
-function drawPieChart(colNames, dataName, viewCols, url, getEl) {
+function drawPieChart(dataName, viewCols, url, getEl) {
   var el = getEl();
   if (!el) return;
 
@@ -260,11 +277,9 @@ function drawPieChart(colNames, dataName, viewCols, url, getEl) {
   legendContainer.innerHTML = chart.generateLegend();
 }
 
-var drawIdpsChart = drawPieChart.bind(null, ['sourceIdpName', 'sourceIdPEntityId', 'Count'], 'loginCountPerIdp',
-  [0, 2], 'idpDetail.php?entityId=');
-
-var drawSpsChart = drawPieChart.bind(null, ['service', 'serviceIdentifier', 'Count'], 'accessCountPerService',
-  [0, 2], 'spDetail.php?identifier=');
+function getDrawChart(side) {
+  return drawPieChart.bind(null, 'loginCountPer' + side, [0, 2], 'detail.php?side=' + side + '&id=');
+}
 
 function drawCountTable(cols, dataCol, countCol, dataName, allowHTML, url, getEl) {
   var el = getEl();
@@ -322,23 +337,14 @@ function drawCountTable(cols, dataCol, countCol, dataName, allowHTML, url, getEl
   });
 }
 
-var drawIdpsTable = drawCountTable.bind(null, ['tables_identity_provider', 'count'], 0, 2,
-  'loginCountPerIdp', false, 'idpDetail.php?entityId=');
-
-var drawAccessedSpsChart = drawPieChart.bind(null, ['service', 'Count'],
-  'accessCountForIdentityProviderPerServiceProviders', null, null);
-
-var drawAccessedSpsTable = drawCountTable.bind(null, ['tables_service_provider', 'count'], 0, 1,
-  'accessCountForIdentityProviderPerServiceProviders', true, null);
-
-var drawSpsTable = drawCountTable.bind(null, ['tables_service_provider', 'count'], 0, 2,
-  'accessCountPerService', true, 'spDetail.php?identifier=');
-
-var drawUsedIdpsChart = drawPieChart.bind(null, ['service', 'Count'],
-  'accessCountForServicePerIdentityProviders', null, null);
+function getDrawTable(side) {
+  return drawCountTable.bind(null, ['tables_' + side, 'count'], 0, 2, 'loginCountPer' + side, false,
+    'detail.php?side=' + side + '&id=');
+}
 
-var drawUsedIdpsTable = drawCountTable.bind(null, ['tables_identity_provider', 'count'], 0, 1,
-  'accessCountForServicePerIdentityProviders', true, null);
+function getDrawCountTable(side) {
+  return drawCountTable.bind(null, ['tables_' + side, 'count'], 0, 2, 'accessCounts', true, null);
+}
 
 function getterLoadCallback(getEl, callback) {
   callback(getEl);
@@ -354,14 +360,13 @@ function idLoadCallback(id, callback) {
 
 function chartInit() {
   idLoadCallback('loginsDashboard', drawLoginsChart);
-  classLoadCallback('chart-idpsChart', drawIdpsChart);
-  classLoadCallback('chart-spsChart', drawSpsChart);
-  idLoadCallback('idpsTable', drawIdpsTable);
-  idLoadCallback('accessedSpsChartDetail', drawAccessedSpsChart);
-  idLoadCallback('accessedSpsTable', drawAccessedSpsTable);
-  idLoadCallback('spsTable', drawSpsTable);
-  idLoadCallback('usedIdPsChartDetail', drawUsedIdpsChart);
-  idLoadCallback('usedIdPsTable', drawUsedIdpsTable);
+  ['IDP', 'SP'].forEach(function callbacksForSide(side) {
+    classLoadCallback('chart-' + side + 'Chart', getDrawChart(side));
+    idLoadCallback(side + 'Table', getDrawTable(side));
+    idLoadCallback('detail' + side + 'Chart', drawPieChart.bind(null, 'accessCounts', [0, 2], null));
+    idLoadCallback('detail' + side + 'Table', getDrawCountTable(side));
+  });
+
   $('#dateSelector input[name=lastDays]').on('click', function submitForm() {
     this.form.submit();
   });
diff --git a/www/moment.cs.min.js b/www/assets/js/moment.cs.min.js
similarity index 100%
rename from www/moment.cs.min.js
rename to www/assets/js/moment.cs.min.js
diff --git a/www/moment.min.js b/www/assets/js/moment.min.js
similarity index 100%
rename from www/moment.min.js
rename to www/assets/js/moment.min.js
diff --git a/www/detail.php b/www/detail.php
new file mode 100644
index 0000000000000000000000000000000000000000..86f6236fbe4a9b7c6a43fb05b63d4c5b792fde97
--- /dev/null
+++ b/www/detail.php
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
+ */
+
+use SimpleSAML\Module\proxystatistics\Config;
+use SimpleSAML\Module\proxystatistics\Templates;
+
+if (empty($_GET['side']) || !in_array($_GET['side'], Config::SIDES, true)) {
+    throw new \Exception('Invalid argument');
+}
+Templates::showDetail($_GET['side']);
diff --git a/www/identityProviders.php b/www/identityProviders.php
index 5185d21316cee22c0a21809e1431f3f89a6692cb..f8ff896ce7770c422766132c262f7b73c0387c2f 100644
--- a/www/identityProviders.php
+++ b/www/identityProviders.php
@@ -1,22 +1,10 @@
 <?php
 
-use SimpleSAML\Configuration;
-use SimpleSAML\Session;
-use SimpleSAML\XHTML\Template;
-
 /**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
  */
 
-$config = Configuration::getInstance();
-$session = Session::getSessionFromRequest();
+use SimpleSAML\Module\proxystatistics\Config;
+use SimpleSAML\Module\proxystatistics\Templates;
 
-$t = new Template($config, 'proxystatistics:identityProviders-tpl.php');
-$t->data['lastDays'] = filter_input(
-    INPUT_GET,
-    'lastDays',
-    FILTER_VALIDATE_INT,
-    ['options'=>['default'=>0,'min_range'=>0]]
-);
-$t->data['tab'] = 1;
-$t->show();
+Templates::showProviders(Config::MODE_IDP, 1);
diff --git a/www/idpDetail.php b/www/idpDetail.php
deleted file mode 100644
index dba5dfce64b9f6b331d4d00bb9c24f2d80e4e858..0000000000000000000000000000000000000000
--- a/www/idpDetail.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-use SimpleSAML\Configuration;
-use SimpleSAML\Session;
-use SimpleSAML\XHTML\Template;
-
-/**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- */
-
-const CONFIG_FILE_NAME_STATISTICSPROXY = 'module_statisticsproxy.php';
-const MODE = 'mode';
-
-$config = Configuration::getInstance();
-$session = Session::getSessionFromRequest();
-
-$configStatisticsproxy = Configuration::getConfig(CONFIG_FILE_NAME_STATISTICSPROXY);
-$mode = $configStatisticsproxy->getString(MODE, 'PROXY');
-
-$t = new Template($config, 'proxystatistics:idpDetail-tpl.php');
-
-$t->data['lastDays'] = filter_input(
-    INPUT_POST,
-    'lastDays',
-    FILTER_VALIDATE_INT,
-    ['options'=>['default'=>0,'min_range'=>0]]
-);
-$t->data['entityId'] = filter_input(INPUT_GET, 'entityId', FILTER_SANITIZE_STRING);
-
-if ($mode === 'SP') {
-    $t->data['idpDetailGraphClass'] = 'hidden';
-} else {
-    $t->data['idpDetailGraphClass'] = '';
-}
-
-$t->show();
diff --git a/www/index.php b/www/index.php
index fb31a14027ca08e19c1fe5334048cffb58d9039c..8360b158036109cfbc21ed4b31fdd1871f51c11c 100644
--- a/www/index.php
+++ b/www/index.php
@@ -1,65 +1,9 @@
 <?php
 
-use SimpleSAML\Configuration;
-use SimpleSAML\Session;
-use SimpleSAML\XHTML\Template;
-
 /**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
  */
 
-const CONFIG_FILE_NAME_STATISTICSPROXY = 'module_statisticsproxy.php';
-const MODE = 'mode';
-
-$config = Configuration::getInstance();
-$session = Session::getSessionFromRequest();
-
-$configStatisticsproxy = Configuration::getConfig(CONFIG_FILE_NAME_STATISTICSPROXY);
-
-$authSource = $configStatisticsproxy->getString('requireAuth.source', '');
-if ($authSource) {
-    $as = new \SimpleSAML\Auth\Simple($authSource);
-    $as->requireAuth();
-}
-
-$mode = $configStatisticsproxy->getString(MODE, 'PROXY');
-
-$t = new Template($config, 'proxystatistics:statistics-tpl.php');
-
-$lastDays = filter_input(
-    INPUT_POST,
-    'lastDays',
-    FILTER_VALIDATE_INT,
-    ['options'=>['default'=>0,'min_range'=>0]]
-);
-
-$t->data['lastDays'] = $lastDays;
-
-$t->data['tab'] = filter_input(
-    INPUT_POST,
-    'tab',
-    FILTER_VALIDATE_INT,
-    ['options'=>['default'=>0,'min_range'=>1]]
-);
-
-if ($mode === 'IDP') {
-    $t->data['tabsAttributes'] = [
-        'PROXY' => 'id="tab-1" href="summary.php?lastDays=' . $lastDays . '"',
-        'IDP' => 'class="hidden" id="tab-2" href="identityProviders.php?lastDays=' . $lastDays . '"',
-        'SP' => 'id="tab-3" href="serviceProviders.php?lastDays=' . $lastDays . '"',
-    ];
-} elseif ($mode === 'SP') {
-    $t->data['tabsAttributes'] = [
-        'PROXY' => 'id="tab-1" href="summary.php?lastDays=' . $lastDays . '"',
-        'IDP' => 'id="tab-2" href="identityProviders.php?lastDays=' . $lastDays . '"',
-        'SP' => 'class="hidden" id="tab-3" href="serviceProviders.php?lastDays=' . $lastDays . '"',
-    ];
-} elseif ($mode === 'PROXY') {
-    $t->data['tabsAttributes'] = [
-        'PROXY' => 'id="tab-1" href="summary.php?lastDays=' . $lastDays . '"',
-        'IDP' => 'id="tab-2" href="identityProviders.php?lastDays=' . $lastDays . '"',
-        'SP' => 'id="tab-3" href="serviceProviders.php?lastDays=' . $lastDays . '"',
-    ];
-}
+use SimpleSAML\Module\proxystatistics\Templates;
 
-$t->show();
+Templates::showIndex();
diff --git a/www/serviceProviders.php b/www/serviceProviders.php
index 81949fa9c4b92885d2001f5defdd33f6fd5f9342..dc7104ed61ee0e524417f8a313fc695325862d3d 100644
--- a/www/serviceProviders.php
+++ b/www/serviceProviders.php
@@ -1,22 +1,10 @@
 <?php
 
-use SimpleSAML\Configuration;
-use SimpleSAML\Session;
-use SimpleSAML\XHTML\Template;
-
 /**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
  */
 
-$config = Configuration::getInstance();
-$session = Session::getSessionFromRequest();
+use SimpleSAML\Module\proxystatistics\Config;
+use SimpleSAML\Module\proxystatistics\Templates;
 
-$t = new Template($config, 'proxystatistics:serviceProviders-tpl.php');
-$t->data['lastDays'] = filter_input(
-    INPUT_GET,
-    'lastDays',
-    FILTER_VALIDATE_INT,
-    ['options'=>['default'=>0,'min_range'=>0]]
-);
-$t->data['tab'] = 2;
-$t->show();
+Templates::showProviders(Config::MODE_SP, 2);
diff --git a/www/spDetail.php b/www/spDetail.php
deleted file mode 100644
index dec913d85c8f8b85d03a6ebeba619e3b4e57c12d..0000000000000000000000000000000000000000
--- a/www/spDetail.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-use SimpleSAML\Configuration;
-use SimpleSAML\Session;
-use SimpleSAML\XHTML\Template;
-
-/**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
- */
-
-const CONFIG_FILE_NAME_STATISTICSPROXY = 'module_statisticsproxy.php';
-const MODE = 'mode';
-
-$config = Configuration::getInstance();
-$session = Session::getSessionFromRequest();
-
-$configStatisticsproxy = Configuration::getConfig(CONFIG_FILE_NAME_STATISTICSPROXY);
-$mode = $configStatisticsproxy->getString(MODE, 'PROXY');
-
-$t = new Template($config, 'proxystatistics:spDetail-tpl.php');
-
-$t->data['lastDays'] = filter_input(
-    INPUT_POST,
-    'lastDays',
-    FILTER_VALIDATE_INT,
-    ['options'=>['default'=>0,'min_range'=>0]]
-);
-$t->data['identifier'] = filter_input(INPUT_GET, 'identifier', FILTER_SANITIZE_STRING);
-
-if ($mode === 'IDP') {
-    $t->data['spDetailGraphClass'] = 'hidden';
-} else {
-    $t->data['spDetailGraphClass'] = '';
-}
-
-$t->show();
diff --git a/www/summary.php b/www/summary.php
index b041e4f391ff0235260aad0ee6d11cdea3c91d63..05d5214e11bb56a820c9d3e6acdaf9cd3700ab96 100644
--- a/www/summary.php
+++ b/www/summary.php
@@ -1,62 +1,9 @@
 <?php
 
-use SimpleSAML\Configuration;
-use SimpleSAML\Session;
-use SimpleSAML\XHTML\Template;
-use SimpleSAML\Logger;
-
 /**
- * @author Pavel Vyskočil <vyskocilpavel@muni.cz>
+ * @author Pavel Břoušek <brousek@ics.muni.cz>
  */
 
-const CONFIG_FILE_NAME = 'module_statisticsproxy.php';
-const MODE = 'mode';
-
-$configMode = Configuration::getConfig(CONFIG_FILE_NAME);
-$config = Configuration::getInstance();
-$session = Session::getSessionFromRequest();
-
-$mode = $configMode->getString(MODE, 'PROXY');
-
-$t = new Template($config, 'proxystatistics:summary-tpl.php');
-
-$t->data['lastDays'] = filter_input(
-    INPUT_GET,
-    'lastDays',
-    FILTER_VALIDATE_INT,
-    ['options'=>['default'=>0,'min_range'=>0]]
-);
-$t->data['tab'] = 0;
-
-if ($mode === 'IDP') {
-    $t->data['summaryGraphs'] = [
-        'identityProviders' => 'hidden',
-        'identityProvidersLegend' => '',
-        'identityProvidersGraph' => '',
-        'serviceProviders' => 'col-md-12 graph',
-        'serviceProvidersLegend' => 'col-md-6',
-        'serviceProvidersGraph' => 'col-md-6 col-md-offset-3'
-    ];
-} elseif ($mode === 'SP') {
-    $t->data['summaryGraphs'] = [
-        'identityProviders' => 'col-md-12 graph',
-        'identityProvidersLegend' => 'col-md-6',
-        'identityProvidersGraph' => 'col-md-6 col-md-offset-3',
-        'serviceProviders' => 'hidden',
-        'serviceProvidersLegend' => '',
-        'serviceProvidersGraph' => ''
-    ];
-} elseif ($mode === 'PROXY') {
-    $t->data['summaryGraphs'] = [
-        'identityProviders' => 'col-md-6 graph',
-        'identityProvidersLegend' => 'col-md-12',
-        'identityProvidersGraph' => 'col-md-12',
-        'serviceProviders' => 'col-md-6 graph',
-        'serviceProvidersLegend' => 'col-md-12',
-        'serviceProvidersGraph' => 'col-md-12'
-    ];
-} else {
-    Logger::error('Unknown mode is set. Mode has to be one of the following: PROXY, IDP, SP.');
-}
+use SimpleSAML\Module\proxystatistics\Templates;
 
-$t->show();
+Templates::showSummary();