Compare commits

...

53 commits

Author SHA1 Message Date
920bccb45e Fixed first day missing in monthly stats 2022-03-01 09:53:23 +01:00
Max
ab2b508e98 Updated credentials for easier deployment 2022-02-11 21:39:13 +01:00
Max
aa706bfcaf Daily and monthly stats 2022-02-11 21:34:38 +01:00
Max
9cfba93f65 Updated changelog 2022-02-11 21:21:28 +01:00
Max
b60f33b454 Slight visual tweak 2022-02-11 21:21:13 +01:00
Max
e82a083c86 Added tool 2022-02-11 21:16:15 +01:00
Max
3544f44a74 Removed unnecessary print statement 2022-02-11 21:16:06 +01:00
Max
84e6cb106c Basic daily and monthly stats 2022-02-02 14:22:27 +01:00
Max
47032055b6 Simplified iso date 2022-02-02 14:22:18 +01:00
Max
46194ab4fe Adjusted the way statistics get processed 2022-02-02 13:05:56 +01:00
Max
a18f1b0fb2 Reworked statistics interface 2022-02-02 11:50:34 +01:00
Linus
9145059e46
Ups, commitet to wrong branch 2021-12-23 18:02:33 +01:00
Linus
935eab8d97
Add automatic python testing CI 2021-12-23 18:02:02 +01:00
Max
b9cfb52098 Package lock 2021-11-28 22:18:24 +01:00
Max
12c8ceb5e1 Added icon only logo 2021-11-23 15:12:53 +01:00
Max
5977940a39 Added tools page 2021-11-23 14:58:10 +01:00
Max
7af0422937 Added GitHub Icon 2021-11-23 14:44:44 +01:00
Max
7a6854bfc7 Added more space between title and content 2021-11-23 14:40:07 +01:00
Max
206ef4c503 Fixed live counter not updating correctly 2021-11-07 23:22:05 +01:00
Max
6a96ef55c0 Added live record timer 2021-11-07 23:13:46 +01:00
Max
98d19e5e0b Added footer and credits and new timer package 2021-11-07 22:20:30 +01:00
Max
edc54207d1 Added robots.txt 2021-07-28 00:05:09 +02:00
Max
dc8d0803b2 Added simple statistics 2021-07-28 00:01:28 +02:00
Max
0433ef1c28 Added statistics page + navigation 2021-07-28 00:00:46 +02:00
Max
cee0139e06 Added statistics tools 2021-07-28 00:00:29 +02:00
Max
d435d5db04 Added statistics API 2021-07-28 00:00:10 +02:00
Max
2770f0e8d8 Added date format helper 2021-07-27 23:45:49 +02:00
Max
1f4bc947b4 Adjusted formatting 2021-07-27 23:44:20 +02:00
Max
97e0bd4c43 Fixed formatting 2021-07-27 23:25:45 +02:00
Max
706d3a9641 Added custom formatting config 2021-07-27 23:16:36 +02:00
Max
cb14c24d58 Fixed formatting 2021-07-27 23:14:59 +02:00
Max
167a002f95 Fixed incorrect names 2021-07-27 23:06:53 +02:00
Max
7b71848e52 Merge branch 'master' of https://github.com/mgfcf/juggl 2021-07-26 22:57:42 +02:00
Max
c17c6cfa02 Fixed stats call + minor formatting 2021-07-26 22:57:39 +02:00
Max
18c4991c52 Fixed undefined variable 2021-07-26 22:57:02 +02:00
9d4d08d0a5 Auto fixed vulnerabilities 2021-07-14 10:07:42 +02:00
Max
7c422d58af Added stats endpoint prototype 2021-05-23 00:43:12 +02:00
Max
2cba0279e6 Small visual tweaks 2021-05-21 19:19:44 +02:00
Max
db6dc225cf Improved order of lists 2021-04-13 23:40:14 +02:00
Max
20a1378e03 Fixed missing tags 2021-04-13 23:39:47 +02:00
Max
e9154924f8 Fixed missing history, due to visibility 2021-04-13 19:21:40 +02:00
Max
8c884779bf Fixed visibility filter endpoint 2021-04-13 19:21:19 +02:00
Max
2525b3c805 Fixed visibility-filter endpoint 2021-04-13 19:10:12 +02:00
Max
978fa507bd Added visibility filter for endpoint 2021-04-13 15:32:10 +02:00
Max
d9cd7ae327 Added date to changelog 2021-04-13 11:55:45 +02:00
Max
cac93173fc Fixed Record visibility architecture 2021-04-13 00:52:53 +02:00
Max
e3e3651c56 Added default width 2021-04-13 00:39:15 +02:00
Max
e178ba4ac9 Added changelog page 2021-04-13 00:38:55 +02:00
Max
a0ef6e7dff Added basic visibility feature 2021-04-13 00:27:41 +02:00
Max
8e189a9e7c Implemented visibility interface 2021-04-12 12:50:55 +02:00
Max
2556a424a1 Now parsing visibility 2021-04-12 12:50:40 +02:00
Max
6e179766a8 Fixed id names 2021-04-12 12:32:12 +02:00
Max
2e2074ea1e Tweaked design 2021-04-12 00:09:56 +02:00
41 changed files with 1671 additions and 239 deletions

19
.prettierrc Normal file
View file

@ -0,0 +1,19 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

420
package-lock.json generated
View file

@ -1855,9 +1855,9 @@
"dev": true
},
"ssri": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz",
"integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.1.tgz",
"integrity": "sha512-w+daCzXN89PseTL99MkA+fxJEcU3wfaE/ah0i0lnOlpG1CYLJ2ZjzEry68YBKfLs4JfoTShrTEsJkAZuNZ/stw==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1",
@ -2585,11 +2585,18 @@
"dev": true
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.10.0"
"follow-redirects": "^1.14.0"
},
"dependencies": {
"follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
}
}
},
"babel-eslint": {
@ -2979,16 +2986,30 @@
}
},
"browserslist": {
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.0.tgz",
"integrity": "sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==",
"version": "4.16.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001165",
"colorette": "^1.2.1",
"electron-to-chromium": "^1.3.621",
"caniuse-lite": "^1.0.30001219",
"colorette": "^1.2.2",
"electron-to-chromium": "^1.3.723",
"escalade": "^3.1.1",
"node-releases": "^1.1.67"
"node-releases": "^1.1.71"
},
"dependencies": {
"caniuse-lite": {
"version": "1.0.30001245",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz",
"integrity": "sha512-768fM9j1PKXpOCKws6eTo3RHmvTUsG9UrpT4WoREFeZgJBTi4/X9g565azS/rVUGtqb8nt7FjLeF5u4kukERnA==",
"dev": true
},
"colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
}
}
},
"buffer": {
@ -3555,9 +3576,9 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"color-string": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
"integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz",
"integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==",
"dev": true,
"requires": {
"color-name": "^1.0.0",
@ -4138,15 +4159,72 @@
"dev": true
},
"cssnano": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz",
"integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz",
"integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==",
"dev": true,
"requires": {
"cosmiconfig": "^5.0.0",
"cssnano-preset-default": "^4.0.7",
"cssnano-preset-default": "^4.0.8",
"is-resolvable": "^1.0.0",
"postcss": "^7.0.0"
},
"dependencies": {
"cssnano-preset-default": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz",
"integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==",
"dev": true,
"requires": {
"css-declaration-sorter": "^4.0.1",
"cssnano-util-raw-cache": "^4.0.1",
"postcss": "^7.0.0",
"postcss-calc": "^7.0.1",
"postcss-colormin": "^4.0.3",
"postcss-convert-values": "^4.0.1",
"postcss-discard-comments": "^4.0.2",
"postcss-discard-duplicates": "^4.0.2",
"postcss-discard-empty": "^4.0.1",
"postcss-discard-overridden": "^4.0.1",
"postcss-merge-longhand": "^4.0.11",
"postcss-merge-rules": "^4.0.3",
"postcss-minify-font-values": "^4.0.2",
"postcss-minify-gradients": "^4.0.2",
"postcss-minify-params": "^4.0.2",
"postcss-minify-selectors": "^4.0.2",
"postcss-normalize-charset": "^4.0.1",
"postcss-normalize-display-values": "^4.0.2",
"postcss-normalize-positions": "^4.0.2",
"postcss-normalize-repeat-style": "^4.0.2",
"postcss-normalize-string": "^4.0.2",
"postcss-normalize-timing-functions": "^4.0.2",
"postcss-normalize-unicode": "^4.0.1",
"postcss-normalize-url": "^4.0.1",
"postcss-normalize-whitespace": "^4.0.2",
"postcss-ordered-values": "^4.1.2",
"postcss-reduce-initial": "^4.0.3",
"postcss-reduce-transforms": "^4.0.2",
"postcss-svgo": "^4.0.3",
"postcss-unique-selectors": "^4.0.1"
}
},
"postcss-svgo": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz",
"integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==",
"dev": true,
"requires": {
"postcss": "^7.0.0",
"postcss-value-parser": "^3.0.0",
"svgo": "^1.0.0"
}
},
"postcss-value-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
"dev": true
}
}
},
"cssnano-preset-default": {
@ -4617,9 +4695,9 @@
"dev": true
},
"dns-packet": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
"integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
"integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
"dev": true,
"requires": {
"ip": "^1.1.0",
@ -4684,12 +4762,20 @@
"dev": true
},
"domhandler": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz",
"integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==",
"dev": true,
"requires": {
"domelementtype": "1"
"domelementtype": "^2.2.0"
},
"dependencies": {
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"dev": true
}
}
},
"domutils": {
@ -4770,30 +4856,30 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.3.633",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz",
"integrity": "sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA==",
"version": "1.3.775",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.775.tgz",
"integrity": "sha512-EGuiJW4yBPOTj2NtWGZcX93ZE8IGj33HJAx4d3ouE2zOfW2trbWU+t1e0yzLr1qQIw81++txbM3BH52QwSRE6Q==",
"dev": true
},
"elliptic": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"dev": true
}
}
@ -5726,7 +5812,8 @@
"follow-redirects": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==",
"dev": true
},
"for-in": {
"version": "1.0.2",
@ -5981,9 +6068,9 @@
}
},
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
@ -6223,9 +6310,9 @@
"dev": true
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==",
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"hpack.js": {
@ -6351,34 +6438,43 @@
}
},
"htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
"dev": true,
"requires": {
"domelementtype": "^1.3.1",
"domhandler": "^2.3.0",
"domutils": "^1.5.1",
"entities": "^1.1.1",
"inherits": "^2.0.1",
"readable-stream": "^3.1.1"
"domelementtype": "^2.0.1",
"domhandler": "^4.0.0",
"domutils": "^2.5.2",
"entities": "^2.0.0"
},
"dependencies": {
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"dev": true
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dom-serializer": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
"integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"dev": true
},
"domutils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
"integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
"dev": true,
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
}
}
}
@ -7416,9 +7512,9 @@
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"lodash.defaultsdeep": {
@ -8102,9 +8198,9 @@
}
},
"node-releases": {
"version": "1.1.67",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz",
"integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==",
"version": "1.1.73",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
"integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==",
"dev": true
},
"node-sass": {
@ -8788,9 +8884,9 @@
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"path-to-regexp": {
@ -8920,9 +9016,9 @@
"dev": true
},
"postcss": {
"version": "7.0.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
"integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==",
"version": "7.0.36",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
"integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -10042,16 +10138,16 @@
"dev": true
},
"renderkid": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.4.tgz",
"integrity": "sha512-K2eXrSOJdq+HuKzlcjOlGoOarUu5SDguDEhE7+Ah4zuOWL40j8A/oHvLlLob9PSTNvVnBd+/q0Er1QfpEuem5g==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz",
"integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==",
"dev": true,
"requires": {
"css-select": "^1.1.0",
"dom-converter": "^0.2",
"htmlparser2": "^3.3.0",
"lodash": "^4.17.20",
"strip-ansi": "^3.0.0"
"css-select": "^4.1.3",
"dom-converter": "^0.2.0",
"htmlparser2": "^6.1.0",
"lodash": "^4.17.21",
"strip-ansi": "^3.0.1"
},
"dependencies": {
"ansi-regex": {
@ -10061,31 +10157,59 @@
"dev": true
},
"css-select": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz",
"integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==",
"dev": true,
"requires": {
"boolbase": "~1.0.0",
"css-what": "2.1",
"domutils": "1.5.1",
"nth-check": "~1.0.1"
"boolbase": "^1.0.0",
"css-what": "^5.0.0",
"domhandler": "^4.2.0",
"domutils": "^2.6.0",
"nth-check": "^2.0.0"
}
},
"css-what": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz",
"integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==",
"dev": true
},
"dom-serializer": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz",
"integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==",
"dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
}
},
"domelementtype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
"dev": true
},
"domutils": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz",
"integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==",
"dev": true,
"requires": {
"dom-serializer": "0",
"domelementtype": "1"
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
}
},
"nth-check": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz",
"integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==",
"dev": true,
"requires": {
"boolbase": "^1.0.0"
}
},
"strip-ansi": {
@ -11147,9 +11271,9 @@
}
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"
@ -11298,9 +11422,9 @@
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true
}
}
@ -12082,9 +12206,9 @@
}
},
"url-parse": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"dev": true,
"requires": {
"querystringify": "^2.1.1",
@ -12280,9 +12404,9 @@
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.1.2",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
"version": "npm:vue-loader@16.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
"dev": true,
"optional": true,
"requires": {
@ -12292,9 +12416,9 @@
},
"dependencies": {
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"dev": true,
"optional": true,
"requires": {
@ -12344,6 +12468,11 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
"vue-timers": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/vue-timers/-/vue-timers-2.0.4.tgz",
"integrity": "sha512-QOEVdO4V4o9WjFG6C0Kn9tfdTeeECjqvEQozcQlfL1Tn8v0qx4uUPhTYoc1+s6qoJnSbu8f68x8+nm1ZEir0kw=="
},
"vuex": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.0.tgz",
@ -13093,45 +13222,12 @@
"dev": true
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"dev": true,
"requires": {
"string-width": "^1.0.2 || 2"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"dev": true,
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
}
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"word-wrap": {
@ -13176,9 +13272,9 @@
}
},
"ws": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
"integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
"dev": true,
"requires": {
"async-limiter": "~1.0.0"

View file

@ -9,12 +9,13 @@
"lint:fix": "eslint --fix --ext .js,.vue ."
},
"dependencies": {
"axios": "^0.21.0",
"axios": "^0.21.4",
"bootstrap": "^4.5.3",
"bootstrap-vue": "^2.21.1",
"core-js": "^3.6.5",
"vue": "^2.6.12",
"vue-router": "^3.2.0",
"vue-timers": "^2.0.4",
"vuex": "^3.4.0"
},
"devDependencies": {

View file

@ -5,8 +5,8 @@ error_reporting(E_ALL | E_STRICT);
$config = [
"host" => "localhost",
"dbname" => "juggl",
"username" => "juggl",
"password" => "?=5,}f_F&){;@xthx-[i",
"dbname" => "admin_juggl",
"username" => "admin_juggl",
"password" => "}dyn{5O!tUlZD;9R?lbi$.@=I,_a2L",
"table_prefix" => "ju_"
];

View file

@ -24,8 +24,12 @@ class GetRecordsBranch extends ApiBranch
if ($params->exists(["finished"])) {
$finished = $params->get("finished");
}
$visible = NULL;
if ($params->exists(["visible"])) {
$visible = $params->get("visible");
}
$records = getRecords($user_id, $limit, $finished);
$records = getRecords($user_id, $limit, $finished, $visible);
$json = new JsonBuilder();
$json->addRecords($records);

38
public/api/getStats.php Normal file
View file

@ -0,0 +1,38 @@
<?php
session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/jsonBuilder.inc.php");
require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class GetStatsBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
$from_date = date("Y-m-d");
if ($params->exists(["from_date"])) {
$from_date = $params->get("from_date");
}
$until_date = date("Y-m-d");
if ($params->exists(["until_date"])) {
$until_date = $params->get("until_date");
}
$stats = getStats($user_id, $from_date, $until_date);
$json = new JsonBuilder();
$json->addStats($stats);
respondJson($json);
}
}
$branch = new GetStatsBranch();
$branch->execute();

View file

@ -7,6 +7,7 @@ class DbOperations
{
$this->resetQuery();
$this->tablePrefix = $tablePrefix;
$this->customValueId = 0;
require(__DIR__ . "/../config/config.php");
$this->config = $config;
@ -55,6 +56,22 @@ class DbOperations
return $this;
}
function groupBy(array $attributes, bool $addTableName = true)
{
for ($i = 0; $i < count($attributes); $i++) {
$a = $attributes[$i];
// Add table name prefix if missing
if ($addTableName && strpos($a, ".") === false) {
$attributes[$i] = "$this->table.$a";
}
}
$formattedAttributes = implode(', ', $attributes);
$this->addToQuery("GROUP BY $formattedAttributes");
return $this;
}
function orderBy(string $attribute, string $order = Order::ASC)
{

View file

@ -86,7 +86,8 @@ class JsonBuilder
"record_tag_id" => "",
"name" => "",
"user_id" => "",
"visible" => ""
"visible" => "",
"bundle" => ""
);
$this->jsonData['record_tags'] = array();
@ -96,6 +97,27 @@ class JsonBuilder
return $this;
}
function addStats(array $stats)
{
if ($stats === null) return;
$columns = array(
"name" => "",
"project_id" => "",
"color" => "",
"visible" => "",
"duration" => "",
"record_count" => "",
"date" => ""
);
$this->jsonData['stats'] = array();
foreach ($stats as $tag) {
$this->jsonData['stats'][] = $this->createJsonArray($tag, $columns);
}
return $this;
}
private function createJsonArray(array $data, array $columns)
{
$jsonArray = array();

View file

@ -160,11 +160,17 @@ function getRunningRecords($user_id)
return $results;
}
function getRecords($user_id, $limit = NULL, $finished = NULL)
function getRecords($user_id, $limit = NULL, $finished = NULL, $visible = NULL)
{
$db = new DbOperations();
$db->select("time_records");
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->select("time_records", ["record_id", "start_time", "end_time", "duration", "user_id", "project_id", "start_device_id"]);
if ($visible != NULL) {
$db->innerJoin("projects", "project_id");
}
$db->where("ju_time_records.user_id", Comparison::EQUAL, $user_id);
if ($visible != NULL) {
$db->where("ju_projects.visible", Comparison::EQUAL, $visible);
}
if ($finished != NULL) {
if ($finished) {
$db->addSql(" AND end_time IS NOT NULL");
@ -248,7 +254,7 @@ function updateRecordTag($user_id, $tag)
// Update given parameters
$data = [];
$props = ["name", "visible"];
$props = ["name", "visible", "bundle"];
foreach ($props as $p) {
if (array_key_exists($p, $tag)) {
$data[$p] = $tag[$p];
@ -373,7 +379,9 @@ function getRecordExternalData($record)
$data = [
"record_tag_id" => $tag["record_tag_id"],
"name" => $tag["name"],
"user_id" => $tag["user_id"]
"user_id" => $tag["user_id"],
"visible" => $tag["visible"],
"bundle" => $tag["bundle"]
];
$tags[] = $data;
}
@ -456,3 +464,30 @@ function removeTagFromRecord($tag_id, $record_id)
$db->where("record_id", Comparison::EQUAL, $record_id);
$db->execute();
}
function getStats($user_id, $from_date, $until_date)
{
$sum_duration = "SUM(ju_time_records.duration) AS duration";
$date = "DATE(ju_time_records.start_time) AS date";
$record_count = "COUNT(*) AS record_count";
$where1 = " AND DATE(ju_time_records.start_time) >= DATE( ";
$where2 = " ) AND DATE(ju_time_records.start_time) <= DATE( ";
$where3 = " ) AND ju_time_records.end_time IS NOT NULL ";
$db = new DbOperations();
$db->select("projects", ["ju_projects.name AS name", "ju_projects.project_id", "color", "visible", $sum_duration, $date, $record_count], false);
$db->innerJoin("time_records", "project_id");
// $db->innerJoin("tags_on_records", "record_id", "record_id", "time_records");
// $db->innerJoin("record_tags", "record_tag_id", "record_tag_id", "tags_on_records");
$db->where("ju_projects.user_id", Comparison::EQUAL, $user_id);
$db->addSql($where1);
$db->addValue($from_date);
$db->addSql($where2);
$db->addValue($until_date);
$db->addSql($where3);
$db->groupBy(["ju_projects.project_id", "date"], false);
return $db->execute();
}

2
public/robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-agent: *
Disallow:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 25 KiB

BIN
src/assets/title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View file

@ -1,17 +1,15 @@
<template>
<div>
<b-link to="/">
<b-img
:src="require('../../assets/logo.png')"
alt="Juggl"
:width="widthSize"
:center="center"
/>
<b-img :src="image" alt="Juggl" :width="widthSize" :center="center" />
</b-link>
</div>
</template>
<script>
import logo from "./../../assets/logo.png";
import title from "./../../assets/title.png";
export default {
name: "BaseLogo",
props: {
@ -22,11 +20,15 @@ export default {
center: {
default: false,
type: Boolean
},
iconOnly: {
default: false,
type: Boolean
}
},
computed: {
widthSize: function() {
let sizes = {
let titleSizes = {
mini: "35px",
tiny: "80px",
smaller: "110px",
@ -38,9 +40,34 @@ export default {
huge: "800px",
massive: "960px"
};
let logoSizes = {
mini: "12px",
tiny: "30px",
smaller: "50px",
small: "65px",
normal: "80x",
medium: "100px",
large: "150px",
big: "200px",
huge: "300px",
massive: "400px"
};
var sizes = titleSizes;
if (this.iconOnly) {
sizes = logoSizes;
}
let targetSize = sizes[this.size];
if (targetSize === undefined) return sizes["small"];
else return targetSize;
},
image: function() {
if (this.iconOnly) {
return logo;
} else {
return title;
}
}
}
};

View file

@ -1,6 +1,6 @@
<template>
<section>
<BaseTitle v-if="title" center size="large">{{ title }}</BaseTitle>
<BaseTitle v-if="title" center size="huge">{{ title }}</BaseTitle>
<slot />
</section>
</template>

View file

@ -1,5 +1,9 @@
<template>
<h1 id="title" :class="[size, { center: center }]" class="bold pt-5 pb-3">
<h1
id="title"
:class="[size, { center: center }, { outline: outline }]"
class="bold"
>
<slot />
</h1>
</template>
@ -15,12 +19,18 @@ export default {
center: {
default: true,
type: Boolean
},
outline: {
default: false,
type: Boolean
}
}
};
</script>
<style lang="sass" scoped>
@import '@/style/theme.sass'
#title.tiny
font-size: 1em
#title.small
@ -31,10 +41,17 @@ export default {
font-size: 1.7em
#title.huge
font-size: 2em
#title.giant
font-size: 3em
#title.center
text-align: center
#title.outline
color: transparent
-webkit-text-stroke-width: 1.5px
-webkit-text-stroke-color: $secondary
.bold
font-weight: bold
</style>

View file

@ -5,6 +5,7 @@
variant="outline-primary"
right
>
<b-dropdown-item to="/statistics">Statistics</b-dropdown-item>
<b-dropdown-item to="/logout">Log out</b-dropdown-item>
</b-dropdown>
</template>

View file

@ -1,9 +1,12 @@
<template>
<b-form @submit="submitForm">
<b-form-group id="id-group" label-for="id" label="Record ID">
<b-form-group id="id-group" label-for="id" label="Project ID">
<b-form-input id="id" v-model="form.project_id" required trim disabled>
</b-form-input>
</b-form-group>
<b-form-checkbox id="visible" v-model="form.visible" dark>
Visible
</b-form-checkbox>
<b-form-group id="name-group" label-for="name" label="Name">
<b-form-input id="name" v-model="form.name" required trim> </b-form-input>
</b-form-group>
@ -63,7 +66,8 @@ export default {
project_id: undefined,
start_date: undefined,
name: undefined,
color: undefined
color: undefined,
visible: undefined
}
};
},
@ -105,6 +109,7 @@ export default {
this.form.name = this.project.name;
this.form.start_date = this.project.start_date;
this.form.color = this.project.color;
this.form.visible = this.project.visible;
}
};
</script>

View file

@ -156,6 +156,7 @@ export default {
start_time: this.times.start.date + " " + this.times.start.time,
end_time: this.times.end.date + " " + this.times.end.time,
duration: this.times.duration,
tags: this.record.tags,
start_device_id: this.record.start_device_id // TODO: Remove at some time
};

View file

@ -1,9 +1,12 @@
<template>
<b-form @submit="submitForm">
<b-form-group id="id-group" label-for="id" label="Record ID">
<b-form-group id="id-group" label-for="id" label="Tag ID">
<b-form-input id="id" v-model="form.record_tag_id" required trim disabled>
</b-form-input>
</b-form-group>
<b-form-checkbox id="visible" v-model="form.visible" dark>
Visible
</b-form-checkbox>
<b-form-group id="name-group" label-for="name" label="Name">
<b-form-input id="name" v-model="form.name" required trim> </b-form-input>
</b-form-group>
@ -48,7 +51,8 @@ export default {
working: false,
form: {
record_tag_id: undefined,
name: undefined
name: undefined,
visible: undefined
}
};
},
@ -84,6 +88,7 @@ export default {
created: function() {
this.form.record_tag_id = this.tag.record_tag_id;
this.form.name = this.tag.name;
this.form.visible = this.tag.visible;
}
};
</script>

View file

@ -0,0 +1,151 @@
<template>
<b-table
:items="aggregatedStatistics"
primary-key="day"
hover
:busy="isLoading"
:fields="statistic_fields"
sort-by="day"
sort-desc
>
<template #table-busy>
<div class="text-center">
<b-spinner></b-spinner>
</div>
</template>
<!-- Custom data -->
<template #cell(duration)="data">
{{ getDurationTimestamp(data.item.duration) }}
</template>
<template #cell(distribution)="data">
<JugglProjectDistribution :projects="data.item.projects" />
</template>
<template #cell(details)="data">
<b-button
size="sm"
@click="data.toggleDetails"
variant="outline-secondary"
>
<b-icon class="icon-btn" icon="bar-chart-steps" />
</b-button>
</template>
<template #row-details="data">
<b-card>
<JugglProjectDistribution
:projects="data.item.projects"
names
class="mb-3"
/>
<JugglProjectStatisticsList :statistics="data.item.statistics" />
</b-card>
</template>
</b-table>
</template>
<script>
import { helperService } from "@/services/helper.service.js";
import JugglProjectStatisticsList from "./JugglProjectStatisticsList.vue";
import JugglProjectDistribution from "./JugglProjectDistribution.vue";
export default {
name: "JugglDailyStatisticsList",
components: { JugglProjectStatisticsList, JugglProjectDistribution },
props: {
statistics: {
required: true,
type: Array
}
},
data: () => {
return {
statistic_fields: [
{
key: "day",
label: "Day"
},
{
key: "duration",
label: "Duration"
},
{
key: "distribution",
lavel: "Distribution"
},
{
key: "record_count",
label: "Records"
},
{
key: "details",
label: "Details"
}
]
};
},
computed: {
isLoading: function() {
return this.statistics === undefined;
},
aggregatedStatistics: function() {
if (this.statistics === undefined) {
return [];
}
var aggregated = {};
this.statistics.forEach(stat => {
var dayName = stat.date;
if (aggregated[dayName] === undefined) {
aggregated[dayName] = {
day: dayName,
duration: 0,
record_count: 0,
statistics: [],
projects: {}
};
}
if (aggregated[dayName].projects[stat.project_id] === undefined) {
aggregated[dayName].projects[stat.project_id] = {
project_id: stat.project_id,
duration: 0,
record_count: 0,
color: stat.color,
name: stat.name,
visible: stat.visible,
statistics: []
};
}
aggregated[dayName].duration += Number(stat.duration);
aggregated[dayName].record_count += Number(stat.record_count);
aggregated[dayName].statistics.push(stat);
aggregated[dayName].projects[stat.project_id].duration += Number(
stat.duration
);
aggregated[dayName].projects[stat.project_id].record_count += Number(
stat.record_count
);
aggregated[dayName].projects[stat.project_id].statistics.push(stat);
});
// Simplyfying object to lists
aggregated = Object.values(aggregated);
aggregated.forEach(stat => {
stat.projects = Object.values(stat.projects);
});
return aggregated;
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp
}
};
</script>
<style lang="sass"></style>

View file

@ -0,0 +1,151 @@
<template>
<b-table
:items="aggregatedStatistics"
primary-key="month"
hover
:busy="isLoading"
:fields="statistic_fields"
sort-by="month"
sort-desc
>
<template #table-busy>
<div class="text-center">
<b-spinner></b-spinner>
</div>
</template>
<!-- Custom data -->
<template #cell(duration)="data">
{{ getDurationTimestamp(data.item.duration) }}
</template>
<template #cell(distribution)="data">
<JugglProjectDistribution :projects="data.item.projects" class="mb-3" />
</template>
<template #cell(details)="data">
<b-button
size="sm"
@click="data.toggleDetails"
variant="outline-secondary"
>
<b-icon class="icon-btn" icon="bar-chart-steps" />
</b-button>
</template>
<template #row-details="data">
<b-card>
<JugglProjectDistribution
:projects="data.item.projects"
names
class="mb-3"
/>
<JugglProjectStatisticsList :statistics="data.item.statistics" />
</b-card>
</template>
</b-table>
</template>
<script>
import { helperService } from "@/services/helper.service.js";
import JugglProjectStatisticsList from "./JugglProjectStatisticsList.vue";
import JugglProjectDistribution from "./JugglProjectDistribution.vue";
export default {
name: "JugglMonthlyStatisticsList",
components: { JugglProjectStatisticsList, JugglProjectDistribution },
props: {
statistics: {
required: true,
type: Array
}
},
data: () => {
return {
statistic_fields: [
{
key: "month",
label: "Month"
},
{
key: "duration",
label: "Duration"
},
{
key: "distribution",
lavel: "Distribution"
},
{
key: "record_count",
label: "Records"
},
{
key: "details",
label: "Details"
}
]
};
},
computed: {
isLoading: function() {
return this.statistics === undefined;
},
aggregatedStatistics: function() {
if (this.statistics === undefined) {
return [];
}
var aggregated = {};
this.statistics.forEach(stat => {
var monthName = stat.date.substring(0, 7); // date is in the format "YYYY-MM-DD", so simply cutting of the day
if (aggregated[monthName] === undefined) {
aggregated[monthName] = {
month: monthName,
duration: 0,
record_count: 0,
statistics: [],
projects: {}
};
}
if (aggregated[monthName].projects[stat.project_id] === undefined) {
aggregated[monthName].projects[stat.project_id] = {
project_id: stat.project_id,
duration: 0,
record_count: 0,
color: stat.color,
name: stat.name,
visible: stat.visible,
statistics: []
};
}
aggregated[monthName].duration += Number(stat.duration);
aggregated[monthName].record_count += Number(stat.record_count);
aggregated[monthName].statistics.push(stat);
aggregated[monthName].projects[stat.project_id].duration += Number(
stat.duration
);
aggregated[monthName].projects[stat.project_id].record_count += Number(
stat.record_count
);
aggregated[monthName].projects[stat.project_id].statistics.push(stat);
});
// Simplyfying object to lists
aggregated = Object.values(aggregated);
aggregated.forEach(stat => {
stat.projects = Object.values(stat.projects);
});
return aggregated;
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp
}
};
</script>
<style lang="sass"></style>

View file

@ -0,0 +1,45 @@
<template>
<b-progress :max="totalDuration">
<b-progress-bar
v-for="(project, index) in projects"
v-bind:key="project.project_id"
:value="project.duration"
:style="'background-color: ' + project.color + ' !important'"
:variant="index % 2 == 0 ? 'primary' : 'dark'"
v-b-tooltip.hover
:title="project.name + ' · ' + getDurationTimestamp(project.duration)"
><span v-if="names">{{ project.name }}</span></b-progress-bar
>
</b-progress>
</template>
<script>
import { helperService } from "@/services/helper.service.js";
export default {
name: "JugglProjectDistribution",
props: {
projects: {
required: true,
type: Array
},
names: {
required: false,
type: Boolean,
default: false
}
},
computed: {
totalDuration: function() {
var duration = 0;
this.projects.forEach(stat => (duration += Number(stat.duration)));
return duration;
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp
}
};
</script>
<style></style>

View file

@ -0,0 +1,112 @@
<template>
<b-table
:items="projectStatistics"
primary-key="project_id"
hover
:busy="isLoading"
:fields="statistic_fields"
sort-by="duration"
sort-desc
>
<template #table-busy>
<div class="text-center">
<b-spinner></b-spinner>
</div>
</template>
<!-- Custom data -->
<template #cell(project)="data">
<JugglProjectName :project="data.item" />
</template>
<template #cell(duration)="data">
{{ getDurationTimestamp(data.item.duration) }}
</template>
<template #cell(distribution)="data">
{{ ((data.item.duration / totalDuration) * 100).toFixed(0) }}%
</template>
</b-table>
</template>
<script>
import JugglProjectName from "@/components/juggl/JugglProjectName";
import { helperService } from "@/services/helper.service.js";
export default {
name: "JugglProjectStatisticsList",
components: {
JugglProjectName
},
props: {
statistics: {
required: true,
type: Array
}
},
data: () => {
return {
statistic_fields: [
{
key: "project",
label: "Project"
},
{
key: "duration",
label: "Duration"
},
{
key: "record_count",
label: "Records"
},
{
key: "distribution",
label: "Distribution"
}
]
};
},
computed: {
isLoading: function() {
return this.statistics === undefined;
},
totalDuration: function() {
var duration = 0;
this.statistics.forEach(stat => (duration += Number(stat.duration)));
return duration;
},
projectStatistics: function() {
if (this.statistics === undefined) {
return [];
}
var projects = {};
this.statistics.forEach(stat => {
if (projects[stat.project_id] === undefined) {
projects[stat.project_id] = {
project_id: stat.project_id,
duration: 0,
record_count: 0,
color: stat.color,
name: stat.name,
visible: stat.visible,
statistics: []
};
}
projects[stat.project_id].duration += Number(stat.duration);
projects[stat.project_id].record_count += Number(stat.record_count);
projects[stat.project_id].statistics.push(stat);
});
// Simplyfying object to lists
return Object.values(projects);
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp
}
};
</script>
<style lang="sass"></style>

View file

@ -1,6 +1,7 @@
<template>
<b-table
:items="records"
primary-key="record_id"
hover
:busy="isLoading"
:fields="fields"
@ -27,7 +28,10 @@
</template>
<template #cell(tags)="data">
<JugglTagField :recordId="data.item.record_id" />
<JugglTagField
:recordId="data.item.record_id"
:onlyVisible="onlyVisibleTags"
/>
</template>
<template #cell(details)="data">
@ -90,11 +94,17 @@ export default {
required: false,
type: Boolean,
default: false
},
onlyVisibleTags: {
required: false,
type: Boolean,
default: true
}
},
data() {
return {
iconScale: 1.6,
timerDelta: 0,
requiredFields: [
{
key: "project",
@ -139,8 +149,18 @@ export default {
return fields;
}
},
timers: {
"increaseTimer": { time: 1000, autostart: true, repeat: true }
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp,
getDurationTimestamp: function(duration) {
var localDuration = duration;
if (this.running) {
localDuration += this.timerDelta;
}
return helperService.getDurationTimestamp(localDuration);
},
getProject: function(id) {
var project = store.getters.getProjectById(id);
@ -155,6 +175,15 @@ export default {
},
detailsRecord: function(id) {
this.$router.push("/record/" + id);
},
increaseTimer: function() {
if (this.records.length <= 0) {
return;
}
var record = this.records[0];
var liveDuration = (new Date().getTime() - new Date(record.start_time).getTime()) / 1000;
this.timerDelta = liveDuration - record.duration;
}
}
};

View file

@ -1,5 +1,6 @@
<template>
<div :id="containerId" class="tag-container">
<!-- Tag item list -->
<div
class="tag-item"
v-for="tag in addedTags"
@ -9,10 +10,12 @@
{{ tag.name }}
</div>
<!-- Add-button -->
<div :id="btnId">
<b-icon icon="plus" />
</div>
<!-- Popover -->
<b-popover
:target="btnId"
triggers="click"
@ -53,6 +56,11 @@ export default {
props: {
recordId: {
required: true
},
onlyVisible: {
required: false,
type: Boolean,
default: false
}
},
data() {
@ -95,10 +103,16 @@ export default {
);
},
allTags: function() {
if (this.onlyVisible) {
return store.getters.getFilteredTags({ visible: true });
} else {
return store.getters.tags;
}
},
addedTags: function() {
return this.record.tags;
return Object.values(this.record.tags).filter(
t => !this.onlyVisible || t.visible
);
},
usedTagIds: function() {
var ids = [];

View file

@ -0,0 +1,33 @@
<template>
<footer>
<BaseContainer
width="medium"
center
>
<b-link to="/tools" class="surround-space">Tools</b-link>
<b-link to="/changelog" class="surround-space">Changelog</b-link>
<b-link to="/credits" class="surround-space">Credits</b-link>
</BaseContainer>
</footer>
</template>
<script>
import BaseContainer from "@/components/base/BaseContainer.vue";
export default {
name: "LayoutFooter",
components: {
BaseContainer,
},
};
</script>
<style lang="sass" scoped>
footer
margin-bottom: 3rem
text-align: center
.surround-space
margin-left: 1rem
margin-right: 1rem
</style>

View file

@ -1,35 +1,46 @@
<template>
<div>
<main>
<BaseLogo id="logo" size="medium" center class="space-top" />
<BaseLogo id="logo" size="medium" center class="space-top space-bottom-small" icon-only />
<BaseContainer
:width="width"
center
class="space-bottom"
:class="{ 'center-content': center }"
>
<BaseTitle v-if="title" center size="huge" class="centered">
{{ title }}
<BaseTitle
v-if="title"
center
size="giant"
class="centered space-bottom"
outline
>
{{ title.toUpperCase() }}
</BaseTitle>
<slot />
</BaseContainer>
</main>
<LayoutFooter />
</div>
</template>
<script>
import BaseContainer from "@/components/base/BaseContainer";
import BaseLogo from "@/components/base/BaseLogo";
import BaseTitle from "@/components/base/BaseTitle";
import LayoutFooter from "@/components/layout/LayoutFooter";
export default {
name: "LayoutMinimal",
components: {
BaseContainer,
BaseLogo,
BaseTitle
BaseTitle,
LayoutFooter
},
props: {
width: {
default: "",
default: "slim",
type: String
},
title: {
@ -54,4 +65,7 @@ export default {
.center-content
text-align: center
.space-bottom-small
margin-bottom: 1rem
</style>

View file

@ -18,12 +18,13 @@
center
:class="{ 'center-content': center }"
>
<BaseTitle v-if="title" center size="huge" class="centered">
{{ title }}
<BaseTitle v-if="title" center size="giant" class="centered" outline>
{{ title.toUpperCase() }}
</BaseTitle>
<slot />
</BaseContainer>
</main>
<LayoutFooter/>
</div>
</template>
@ -32,6 +33,7 @@ import BaseContainer from "@/components/base/BaseContainer.vue";
import BaseLogo from "@/components/base/BaseLogo.vue";
import BaseUserDropdown from "@/components/base/BaseUserDropdown.vue";
import BaseTitle from "@/components/base/BaseTitle";
import LayoutFooter from "@/components/layout/LayoutFooter";
export default {
name: "LayoutNavbarPrivate",
@ -39,7 +41,8 @@ export default {
BaseContainer,
BaseLogo,
BaseUserDropdown,
BaseTitle
BaseTitle,
LayoutFooter
},
props: {
title: {
@ -86,6 +89,14 @@ main
margin-top: 6rem
margin-bottom: 6rem
footer
margin-bottom: 3rem
text-align: center
*
margin-left: 1rem
margin-right: 1rem
.center-content
text-align: center
</style>

View file

@ -3,9 +3,12 @@ import App from "./App.vue";
import router from "./router";
import store from "./store";
import { BootstrapVue, BootstrapVueIcons } from "bootstrap-vue";
import VueTimers from 'vue-timers'
Vue.config.productionTip = false;
Vue.use(VueTimers);
// Install BootstrapVue
Vue.use(BootstrapVue);
Vue.use(BootstrapVueIcons);

View file

@ -6,6 +6,10 @@ import NotFound from "../views/NotFound.vue";
import Home from "../views/Home.vue";
import History from "../views/History.vue";
import Manage from "../views/Manage.vue";
import Statistics from "../views/Statistics.vue";
import Changelog from "../views/Changelog.vue";
import Credits from "../views/Credits.vue";
import ToolsDocumentation from "../views/ToolsDocumentation.vue";
Vue.use(VueRouter);
@ -37,6 +41,27 @@ const routes = [
component: Home,
beforeEnter: requireAuth
},
{
path: "/statistics",
name: "Statistics",
component: Statistics,
beforeEnter: requireAuth
},
{
path: "/changelog",
name: "Changelog",
component: Changelog
},
{
path: "/credits",
name: "Credits",
component: Credits
},
{
path: "/tools",
name: "ToolsDocumentation",
component: ToolsDocumentation
},
{
path: "/logout",
name: "Logout",

View file

@ -64,13 +64,28 @@ export const helperService = {
* @returns Date as string in the used format
*/
toISODate(date) {
var timezoneOffset = date.getMinutes() + date.getTimezoneOffset();
var timestamp = date.getTime() + timezoneOffset * 1000;
var correctDate = new Date(timestamp);
return (
date.getFullYear() +
"-" +
(date.getMonth() + 1).toString().padStart(2, "0") +
"-" +
date
.getDate()
.toString()
.padStart(2, "0") +
"T00:00.000Z"
);
},
correctDate.setUTCHours(0, 0, 0, 0);
return correctDate.toISOString();
/**
* Converts a date object into a date-time-timezone string and only keeps the date.
*
* @param {*} date A date object
*
* @returns Date as string in YYYY-MM-DD format
*/
toISODateOnly(date) {
return helperService.toISODate(date).substring(0, 10);
},
/**

View file

@ -22,7 +22,7 @@ export const jugglService = {
getProjects() {
return apiService.post("/getProjects.php").then(r => {
return {
data: r.data,
data: { projects: processProjects(r.data.projects) },
msg: ""
};
});
@ -31,7 +31,28 @@ export const jugglService = {
getTags() {
return apiService.post("/getRecordTags.php").then(r => {
return {
data: r.data,
data: { record_tags: processTags(r.data.record_tags) },
msg: ""
};
});
},
getStatistics(options = {}) {
if (options.from === undefined) {
options.from = new Date();
}
if (options.until === undefined) {
options.until = new Date();
}
return apiService
.post("/getStats.php", {
from_date: helperService.toISODateOnly(options.from),
until_date: helperService.toISODateOnly(options.until)
})
.then(r => {
return {
data: { statistics: processVisibility(r.data.stats) },
msg: ""
};
});
@ -98,6 +119,9 @@ export const jugglService = {
if (options.finished !== undefined) {
payload.finished = options.finished;
}
if (options.visible !== undefined) {
payload.visible = options.visible;
}
return apiService.post("/getRecords.php", payload).then(r => {
return {
@ -248,7 +272,26 @@ function processRecords(data) {
rec.duration = helperService.calcDurationInSeconds(rec.start_time);
}
rec.tags = Object.values(rec.tags);
rec.tags = processTags(Object.values(rec.tags));
});
return data;
}
function processTags(tags) {
return processVisibility(tags);
}
function processProjects(projects) {
return processVisibility(projects);
}
function processVisibility(items) {
Object.values(items).forEach(item => {
if (item.visible === "1") {
item.visible = true;
} else {
item.visible = false;
}
});
return items;
}

View file

@ -6,6 +6,7 @@ export const juggl = {
projects: [],
records: [],
tags: [],
statistics: [],
user: undefined,
auth: undefined,
recordsLimit: 0
@ -20,6 +21,9 @@ export const juggl = {
setTags(state, tags) {
state.tags = tags;
},
setStatistics(state, statistics) {
state.statistics = statistics;
},
setRecordsLimit(state, limit) {
state.recordsLimit = limit;
},
@ -44,10 +48,55 @@ export const juggl = {
finishedRecords: state => {
return Object.values(state.records).filter(record => !record.running);
},
getFilteredRecords: (state, getters) => ({
running = undefined,
projectVisible = undefined,
records = undefined
}) => {
if (records == undefined) {
records = getters.records;
}
var visibleProjects = getters.visibleProjects;
var visibleIds = [];
Object.values(visibleProjects)
.filter(p => p.visible)
.forEach(p => {
visibleIds.push(p.project_id);
});
return Object.values(records).filter(rec => {
if (running !== undefined && running !== rec.running) {
return false;
}
var recProjectVisible = visibleIds.includes(rec.project_id);
if (
projectVisible !== undefined &&
projectVisible !== recProjectVisible
) {
return false;
}
return true;
});
},
getFilteredStatistics: (state, getters) => ({
projectVisible = undefined
}) => {
return Object.values(getters.statistics).filter(statistic => {
if (
projectVisible !== undefined &&
statistic.visible !== projectVisible
) {
return false;
}
return true;
});
},
auth: state => state.auth,
apiUrl: state => state.apiUrl,
user: state => state.user,
isLoggedIn: state => !!state.auth,
statistics: state => state.statistics,
records: state => state.records,
projects: state => state.projects,
tags: state => state.tags,
@ -73,10 +122,10 @@ export const juggl = {
return getters.projectIds.filter(id => !runningProjectIds.includes(id));
},
finishedProjects: (state, getters) => {
var ids = getters.finishedProjectIds;
return Object.values(state.projects).filter(project =>
ids.includes(project.project_id)
);
return getters.getFilteredProjects({ finished: true });
},
visibleProjects: (state, getters) => {
return getters.getFilteredProjects({ visible: true });
},
runningProjects: (state, getters) => {
var ids = getters.runningProjectIds;
@ -89,6 +138,43 @@ export const juggl = {
project => project.project_id === id
);
},
getFilteredProjects: (state, getters) => ({
finished = undefined,
visible = undefined,
projects = undefined
}) => {
if (projects == undefined) {
projects = getters.projects;
}
var runningIds = getters.runningProjectIds;
return Object.values(projects).filter(project => {
var projectFinished = !runningIds.includes(project.project_id);
if (finished !== undefined && finished !== projectFinished) {
return false;
}
if (visible !== undefined && visible !== project.visible) {
return false;
}
return true;
});
},
getFilteredTags: (state, getters) => ({
visible = undefined,
tags = undefined
}) => {
if (tags == undefined) {
tags = getters.tags;
}
return Object.values(tags).filter(tag => {
if (visible != undefined && visible !== tag.visible) {
return false;
}
return true;
});
},
getTagById: (state, getters) => id => {
return Object.values(getters.tags).find(tag => tag.record_tag_id === id);
},
@ -135,12 +221,16 @@ export const juggl = {
return true;
});
},
loadRecords({ commit, state, getters }, { limit, finished }) {
loadRecords({ commit, state, getters }, { limit, finished, visible }) {
if (limit !== undefined) {
commit("setRecordsLimit", limit);
}
var payload = { limit: state.recordsLimit, finished: finished };
var payload = {
limit: state.recordsLimit,
finished: finished,
visible: visible
};
return jugglService.getRecords(payload).then(r => {
var allRecords = Object.values(r.data.records);
@ -154,6 +244,29 @@ export const juggl = {
commit("setRecords", allRecords);
});
},
loadDailyStatistics({ dispatch }, { date }) {
dispatch("loadStatistics", { from: date, until: date });
},
loadMonthlyStatistics(
{ dispatch },
{ startYear, startMonth, endYear, endMonth }
) {
// Month in date object goes from 0 - 11
var options = {
from: new Date(startYear, startMonth - 1, 1),
until: new Date(endYear, endMonth, 0) // 0 leads to the last day of the previous month
};
dispatch("loadStatistics", options);
},
async loadStatistics({ commit }, options) {
var results = Object.values(
(await jugglService.getStatistics(options)).data.statistics
);
commit("setStatistics", results);
},
loadRunningRecords({ commit, getters }) {
return jugglService.getRunningRecords().then(r => {
var allRecords = {

82
src/views/Changelog.vue Normal file
View file

@ -0,0 +1,82 @@
<template>
<LayoutMinimal title="Changelog">
<BaseSection>
<BaseTitle size="small">
11.02.2022
</BaseTitle>
<ul>
<li>Implemented daily and monthly statistics</li>
<li>Added tool</li>
</ul>
</BaseSection>
<BaseSection>
<BaseTitle size="small">
23.11.2021
</BaseTitle>
<ul>
<li>Added tools page</li>
<li>Visual tweaks</li>
</ul>
</BaseSection>
<BaseSection>
<BaseTitle size="small">
07.11.2021
</BaseTitle>
<ul>
<li>Added credits page</li>
<li>Created basic footer</li>
<li>Added live record timer</li>
</ul>
</BaseSection>
<BaseSection>
<BaseTitle size="small">
27.07.2021
</BaseTitle>
<ul>
<li>Added simple statistics</li>
</ul>
</BaseSection>
<BaseSection>
<BaseTitle size="small">
21.05.2021
</BaseTitle>
<ul>
<li>Visual tweaks</li>
</ul>
</BaseSection>
<BaseSection>
<BaseTitle size="small">
12.04.2021
</BaseTitle>
<ul>
<li>Added toggle to change visibility of single projects and tags</li>
</ul>
</BaseSection>
</LayoutMinimal>
</template>
<script>
// @ is an alias to /src
import LayoutMinimal from "@/components/layout/LayoutMinimal";
import BaseTitle from "@/components/base/BaseTitle";
import BaseSection from "../components/base/BaseSection.vue";
export default {
name: "Changelog",
components: {
LayoutMinimal,
BaseTitle,
BaseSection
}
};
</script>
<style lang="sass" scoped>
ul
list-style-type: '+ '
</style>

51
src/views/Credits.vue Normal file
View file

@ -0,0 +1,51 @@
<template>
<LayoutMinimal title="Credits" >
<BaseSection>
<BaseTitle>
vue-timers
</BaseTitle>
<BaseTitle size="tiny">
<b-link href="https://github.com/Kelin2025/vue-timers">
<b-icon-github></b-icon-github>
GitHub
</b-link>
</BaseTitle>
<p class="monospace">
MIT License
<br/>
<br/>
Copyright (c) 2017 Anton Kosykh
<br/>
<br/>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
<br/>
<br/>
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
<br/>
<br/>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</p>
</BaseSection>
</LayoutMinimal>
</template>
<script>
// @ is an alias to /src
import LayoutMinimal from "@/components/layout/LayoutMinimal";
import BaseTitle from "@/components/base/BaseTitle";
import BaseSection from "@/components/base/BaseSection";
export default {
name: "Credits",
components: {
LayoutMinimal,
BaseTitle,
BaseSection
}
};
</script>
<style lang="sass" scoped>
.monospace
font-family: Courier New, Courier, monospace
</style>

View file

@ -26,7 +26,7 @@ import { helperService } from "@/services/helper.service.js";
import store from "@/store";
export default {
name: "Home",
name: "History",
data: () => {
return {
working: true
@ -60,7 +60,7 @@ export default {
store.dispatch("loadTags");
store.dispatch("loadProjects");
store
.dispatch("loadRecords", { limit: 0, finished: true })
.dispatch("loadRecords", { limit: 0, finished: true, visible: true })
.then(() => {
this.working = false;
})

View file

@ -4,8 +4,8 @@
<JugglRecordsList :records="runningRecords" running />
</BaseSection>
<BaseSection title="Projects">
<div v-if="finishedProjects.length > 0">
<JugglProjectsPanel :projects="finishedProjects" />
<div v-if="availableProjects.length > 0">
<JugglProjectsPanel :projects="availableProjects" />
</div>
<div id="add-project-form">
<FormProjectAdd />
@ -43,21 +43,30 @@ export default {
BaseSection
},
computed: {
finishedProjects: () => {
return store.getters.finishedProjects;
availableProjects: () => {
return store.getters.getFilteredProjects({
finished: true,
visible: true
});
},
finishedRecords: () => {
return store.getters.finishedRecords;
return store.getters.getFilteredRecords({
running: false,
projectVisible: true
});
},
runningRecords: () => {
return store.getters.runningRecords;
return store.getters.getFilteredRecords({
running: true,
projectVisible: true
});
}
},
created: () => {
store.dispatch("loadProjects");
store.dispatch("loadTags");
store.dispatch("loadRunningRecords");
store.dispatch("loadRecords", { limit: 10, finished: true });
store.dispatch("loadRecords", { limit: 10, finished: true, visible: true });
}
};
</script>

View file

@ -97,7 +97,7 @@ import FormTagAdd from "@/components/forms/FormTagAdd";
import store from "@/store";
export default {
name: "Home",
name: "Manage",
components: {
LayoutNavbarPrivate,
FormProjectDetails,

View file

@ -1,6 +1,6 @@
<template>
<LayoutMinimal center title="Couldn't find what you were looking for :(">
404
<LayoutMinimal center title="404">
Couldn't find what you were looking for :(
</LayoutMinimal>
</template>

180
src/views/Statistics.vue Normal file
View file

@ -0,0 +1,180 @@
<template>
<LayoutNavbarPrivate title="Statistics">
<BaseContainer class="centered mb-5">
<b-form-radio-group
id="mode-radio"
v-model="mode"
:options="options"
button-variant="outline-primary"
name="radio-btn-outline"
buttons
size="sm"
class="mb-3"
@input="updateStatistics"
></b-form-radio-group>
<b-form id="form-daily" v-if="mode == 'daily'" inline>
<b-form-datepicker
id="startdate"
v-model="daily.startDate"
required
placeholder="Choose a start date"
:max="daily.endDate"
dark
@input="updateStatistics"
>
</b-form-datepicker>
<b-form-datepicker
id="enddate"
v-model="daily.endDate"
required
placeholder="Choose an end date"
:min="daily.startDate"
dark
@input="updateStatistics"
>
</b-form-datepicker>
</b-form>
<b-form id="form-monthly" v-if="mode == 'monthly'" inline>
<b-input-group>
<b-input
id="start-year"
type="number"
placeholder="Start year"
v-model="monthly.startYear"
required
trim
@input="updateStatistics"
class="slim"
>
</b-input>
<b-input
id="start-month"
type="number"
placeholder="Start month"
v-model="monthly.startMonth"
required
trim
@input="updateStatistics"
class="slim"
>
</b-input>
</b-input-group>
<b-input-group>
<b-input
id="end-year"
type="number"
placeholder="End year"
v-model="monthly.endYear"
required
trim
@input="updateStatistics"
class="slim"
>
</b-input>
<b-input
id="end-month"
type="number"
placeholder="End month"
v-model="monthly.endMonth"
required
trim
@input="updateStatistics"
class="slim"
>
</b-input>
</b-input-group>
</b-form>
</BaseContainer>
<BaseSection id="projects" :title="modeTitle">
<JugglDailyStatisticsList
:statistics="visibleStatistics"
v-if="mode == 'daily'"
/>
<JugglMonthlyStatisticsList
:statistics="visibleStatistics"
v-if="mode == 'monthly'"
/>
</BaseSection>
</LayoutNavbarPrivate>
</template>
<script>
import LayoutNavbarPrivate from "@/components/layout/LayoutNavbarPrivate";
import JugglDailyStatisticsList from "@/components/juggl/JugglDailyStatisticsList";
import JugglMonthlyStatisticsList from "@/components/juggl/JugglMonthlyStatisticsList";
import { helperService } from "@/services/helper.service.js";
import BaseSection from "@/components/base/BaseSection";
import BaseContainer from "@/components/base/BaseContainer";
import store from "@/store";
export default {
name: "Statistics",
components: {
LayoutNavbarPrivate,
JugglMonthlyStatisticsList,
JugglDailyStatisticsList,
BaseSection,
BaseContainer
},
data: () => {
return {
working: true,
mode: "daily",
options: [
{ text: "Daily", value: "daily" },
{ text: "Monthly", value: "monthly" }
],
daily: {
startDate: new Date(),
endDate: new Date()
},
monthly: {
startYear: new Date().getFullYear(),
startMonth: new Date().getMonth() + 1,
endYear: new Date().getFullYear(),
endMonth: new Date().getMonth() + 1
}
};
},
mounted: function() {
this.updateStatistics();
},
computed: {
visibleStatistics: () => {
return store.getters.getFilteredStatistics({
projectVisible: true
});
},
modeTitle: function() {
return this.mode[0].toUpperCase() + this.mode.substring(1);
}
},
methods: {
getDurationTimestamp: helperService.getDurationTimestamp,
updateStatistics: function() {
if (this.mode == "daily") {
store.dispatch("loadStatistics", {
from: new Date(this.daily.startDate),
until: new Date(this.daily.endDate)
});
} else if (this.mode == "monthly") {
store.dispatch("loadMonthlyStatistics", this.monthly);
}
}
}
};
</script>
<style lang="sass">
.centered
text-align: center
#form-daily *, #form-monthly *
margin: auto
.slim
max-width: 6rem
</style>

View file

@ -0,0 +1,61 @@
<template>
<LayoutMinimal title="Tools">
<BaseTitle>
Graph Day Distribution
<br />
<small>by Linus Kämmerer</small>
</BaseTitle>
<BaseSection>
<BaseTitle size="tiny">
<b-link
href="https://gist.github.com/linuskmr/f89ac638da036e25d67e147ece67577c"
>
<b-icon-github></b-icon-github>
GitHub
</b-link>
</BaseTitle>
<p class="monospace">
Python script to generate a time distribution graph from the exported
Juggl data. It accumulates the tracked record and shows the total
working hours for different times for each weekday.
</p>
</BaseSection>
<BaseSection>
<BaseTitle>
JSON to CSV
<br />
<small>by Linus Kämmerer</small>
</BaseTitle>
<BaseTitle size="tiny">
<b-link
href="https://gist.github.com/linuskmr/856459cdf6c7ea460503b92e00b2aa26"
>
<b-icon-github></b-icon-github>
GitHub
</b-link>
</BaseTitle>
<p class="monospace">
Python script to convert exported Juggl data from a JSON to a CSV file.
</p>
</BaseSection>
</LayoutMinimal>
</template>
<script>
// @ is an alias to /src
import LayoutMinimal from "@/components/layout/LayoutMinimal";
import BaseTitle from "@/components/base/BaseTitle";
import BaseSection from "../components/base/BaseSection.vue";
export default {
name: "ToolsDocumentation",
components: {
LayoutMinimal,
BaseTitle,
BaseSection
}
};
</script>
<style lang="sass" scoped></style>