From 36ffe294ca63710a7bf92873defb29ebf1970bed Mon Sep 17 00:00:00 2001 From: Harm Akkerman Date: Wed, 1 Jun 2022 21:15:30 +0200 Subject: [PATCH 1/2] Include library for dev purposes --- composer.json | 1 + composer.lock | 183 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 181 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 95a360a..3036dd7 100644 --- a/composer.json +++ b/composer.json @@ -10,5 +10,6 @@ ], "require": {}, "require-dev": { + "christophwurst/nextcloud": "^23.0" } } diff --git a/composer.lock b/composer.lock index c9b161c..5780326 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,186 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2e664be3728bc19c6f91bbed1ff62617", + "content-hash": "f406174408d593dde45d93667f53b5bc", "packages": [], - "packages-dev": [], + "packages-dev": [ + { + "name": "christophwurst/nextcloud", + "version": "v23.0.2", + "source": { + "type": "git", + "url": "https://github.com/ChristophWurst/nextcloud_composer.git", + "reference": "7f7b957934d22205aab66c568ac3029bd8f2110d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ChristophWurst/nextcloud_composer/zipball/7f7b957934d22205aab66c568ac3029bd8f2110d", + "reference": "7f7b957934d22205aab66c568ac3029bd8f2110d", + "shasum": "" + }, + "require": { + "php": "^7.3 || ~8.0.0", + "psr/container": "^1.0", + "psr/event-dispatcher": "^1.0", + "psr/log": "^1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "23.0.0-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "AGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Christoph Wurst", + "email": "christoph@winzerhof-wurst.at" + } + ], + "description": "Composer package containing Nextcloud's public API (classes, interfaces)", + "time": "2022-02-22T09:41:29+00:00" + }, + { + "name": "psr/container", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2021-03-05T17:36:06+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2021-05-03T11:20:27+00:00" + } + ], "aliases": [], "minimum-stability": "stable", "stability-flags": [], @@ -14,5 +191,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "1.1.0" } From 5396c96fc509981e0499b8ada7c77b9da333483d Mon Sep 17 00:00:00 2001 From: Harm Akkerman Date: Wed, 1 Jun 2022 21:16:38 +0200 Subject: [PATCH 2/2] Implemented cost field in workitem and display it in dashboard and reports --- appinfo/routes.php | 10 +- js/src/dashboard.js | 482 ++++++++++-------- js/src/reports.js | 55 +- js/src/timer.js | 86 ++-- lib/Controller/AjaxController.php | 143 +++--- lib/Db/ReportItem.php | 4 +- lib/Db/ReportItemMapper.php | 34 +- lib/Db/WorkInterval.php | 4 +- .../Version000020Date20220528101009.php | 75 +++ templates/content/dashboard.php | 3 +- 10 files changed, 552 insertions(+), 344 deletions(-) create mode 100644 lib/Migration/Version000020Date20220528101009.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 64aad6b..ebb203b 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -8,7 +8,7 @@ * it's instantiated in there */ - + return [ 'routes' => [ ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], @@ -18,9 +18,9 @@ ['name' => 'dashboard#index', 'url' => '/dashboard', 'verb' => 'GET'], ['name' => 'reports#index', 'url' => '/reports', 'verb' => 'GET'], ['name' => 'timelines#index', 'url' => '/timelines', 'verb' => 'GET'], - + ['name' => 'timelinesAdmin#index', 'url' => '/timelines-admin', 'verb' => 'GET'], - + ['name' => 'tags#index', 'url' => '/tags', 'verb' => 'GET'], ['name' => 'goals#index', 'url' => '/goals', 'verb' => 'GET'], @@ -31,8 +31,10 @@ ['name' => 'ajax#update_work_interval', 'url' => '/ajax/update-work-interval/{id}', 'verb' => 'POST'], ['name' => 'ajax#add_work_interval', 'url' => '/ajax/add-work-interval/{name}', 'verb' => 'POST'], ['name' => 'ajax#delete_work_interval', 'url' => '/ajax/delete-work-interval/{id}', 'verb' => 'POST'], + ['name' => 'ajax#add_cost', 'url' => '/ajax/add-cost/{id}', 'verb' => 'POST'], + - ['name' => 'ajax#get_clients', 'url' => '/ajax/clients', 'verb' => 'GET'], + ['name' => 'ajax#get_clients', 'url' => '/ajax/clients', 'verb' => 'GET'], ['name' => 'ajax#add_client', 'url' => '/ajax/add-client/{name}', 'verb' => 'POST'], ['name' => 'ajax#edit_client', 'url' => '/ajax/edit-client/{id}', 'verb' => 'POST'], ['name' => 'ajax#delete_client', 'url' => '/ajax/delete-client/{id}', 'verb' => 'POST'], diff --git a/js/src/dashboard.js b/js/src/dashboard.js index c51517e..aa0a93e 100644 --- a/js/src/dashboard.js +++ b/js/src/dashboard.js @@ -11,243 +11,309 @@ require('../../css/style.css'); var Chart = require("chart.js"); Chart.plugins.register({ - afterDraw: function(chart) { - if (chart.data.datasets.length === 0 || chart.data.datasets[0].data.length === 0) { - // No data is present - var ctx = chart.chart.ctx; - var width = chart.chart.width; - var height = chart.chart.height - chart.clear(); - - ctx.save(); - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.font = "16px normal 'Helvetica Nueue'"; - ctx.fillText('No data to display', width / 2, height / 2); - ctx.restore(); - } -} + afterDraw: function (chart) { + if (chart.data.datasets.length === 0 || chart.data.datasets[0].data.length === 0) { + // No data is present + var ctx = chart.chart.ctx; + var width = chart.chart.width; + var height = chart.chart.height + chart.clear(); + + ctx.save(); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.font = "16px normal 'Helvetica Nueue'"; + ctx.fillText('No data to display', width / 2, height / 2); + ctx.restore(); + } + } }); var dtf = require("./dateformat.js"); -(function() { - $.ajaxSetup({ - headers: { 'RequestToken': OC.requestToken } - }); - - $( function() { - $(document).ready(function() { - - var start = moment().subtract(29, 'days'); - var end = moment(); - var group1 = 'client'; - var group2 = 'project'; - var group3 = ''; - var filterProjectId = ''; - var filterClientId = ''; - var myDoughnutChart = null; - - function cb(start, end) { - $('#report-range span').html(start.format(dtf.dformat()) + ' - ' + end.format(dtf.dformat())); - } - $("#report-range").daterangepicker({ - timePicker: false, - startDate: start, - endDate: end, - showCustomRangeLabel: false, - ranges: { - 'Today': [moment().startOf('day'), moment().endOf('day')], - 'This week': [moment().startOf('week'), moment().endOf('week')], - 'Last 7 Days': [moment().startOf('day').subtract(6, 'days'), moment().endOf('day')], - 'Last 30 Days': [moment().startOf('day').subtract(29, 'days'), moment().endOf('day')], - 'Last 90 Days': [moment().startOf('day').subtract(89, 'days'), moment().endOf('day')], - 'Last 365 Days': [moment().startOf('day').subtract(364, 'days'), moment().endOf('day')], - 'This Month': [moment().startOf('month'), moment().endOf('day')], - 'This Year': [moment().startOf('year'), moment().endOf('day')], - 'Starting last year': [moment().startOf('year').subtract(1, 'year'), moment().endOf('day')], - 'Last 3 years': [moment().startOf('day').subtract(3, 'year'), moment().endOf('day')], - 'Last 5 years': [moment().startOf('day').subtract(5, 'year'), moment().endOf('day')], - }, - locale: { - format: dtf.dformat(), - firstDay: firstDay - } - },cb); - $("#report-range").on('apply.daterangepicker', function(ev, picker) { - //days = Math.round((picker.endDate.unix()-picker.startDate.unix()) / 86400); - start = picker.startDate; +(function () { + $.ajaxSetup({ + headers: {'RequestToken': OC.requestToken} + }); + + $(function () { + $(document).ready(function () { + + var start = moment().subtract(29, 'days'); + var end = moment(); + var group1 = 'client'; + var group2 = 'project'; + var group3 = ''; + var filterProjectId = ''; + var filterClientId = ''; + var myDoughnutChart = null; + var costChart = null; + + function cb(start, end) { + $('#report-range span').html(start.format(dtf.dformat()) + ' - ' + end.format(dtf.dformat())); + } + + $("#report-range").daterangepicker({ + timePicker: false, + startDate: start, + endDate: end, + showCustomRangeLabel: false, + ranges: { + 'Today': [moment().startOf('day'), moment().endOf('day')], + 'This week': [moment().startOf('week'), moment().endOf('week')], + 'Last 7 Days': [moment().startOf('day').subtract(6, 'days'), moment().endOf('day')], + 'Last 30 Days': [moment().startOf('day').subtract(29, 'days'), moment().endOf('day')], + 'Last 90 Days': [moment().startOf('day').subtract(89, 'days'), moment().endOf('day')], + 'Last 365 Days': [moment().startOf('day').subtract(364, 'days'), moment().endOf('day')], + 'This Month': [moment().startOf('month'), moment().endOf('day')], + 'This Year': [moment().startOf('year'), moment().endOf('day')], + 'Starting last year': [moment().startOf('year').subtract(1, 'year'), moment().endOf('day')], + 'Last 3 years': [moment().startOf('day').subtract(3, 'year'), moment().endOf('day')], + 'Last 5 years': [moment().startOf('day').subtract(5, 'year'), moment().endOf('day')], + }, + locale: { + format: dtf.dformat(), + firstDay: firstDay + } + }, cb); + $("#report-range").on('apply.daterangepicker', function (ev, picker) { + //days = Math.round((picker.endDate.unix()-picker.startDate.unix()) / 86400); + start = picker.startDate; + getData(); + }); + cb(start, end); + + + var chartData = {}; + var costChartData = {}; + getData(); - }); - cb(start, end); - - - var chartData = {}; - getData(); - function getData(){ - var baseUrl = OC.generateUrl('/apps/timetracker/ajax/report?name=&from='+start.unix()+'&to='+end.unix()+'&group1='+group1+'&group2='+group2+'&timegroup='+group3+'&filterProjectId='+filterProjectId+'&filterClientId='+filterClientId); - var default_colors = [ - '#3366CC', - '#DC3912', - '#FF9900', - '#109618', - '#990099', - '#3B3EAC', - '#0099C6', - '#DD4477', - '#66AA00', - '#B82E2E', - '#316395', - '#994499', - '#22AA99', - '#AAAA11', - '#6633CC', - '#E67300', - '#8B0707', - '#329262', - '#5574A6', - '#3B3EAC', - '#C71585', - '#006400', - '#4B0082', - '#8B0000', - '#FF4500', - '#BDB76B', - '#008080', - '#FFE4E1', - '#800000', - '#000080', - '#2F4F4F', - '#FF1493', - '#008000', - '#800080', - '#FF0000', - '#FF8C00', - '#20B2AA', - '#FAF0E6', - '#FFD700', - '#A52A2A', - '#708090', - '#DB7093', - '#808000', - '#9400D3', - '#FFDAB9', - '#7FFFD4' - ] - $.ajax({ - /*headers: {requesttoken: oc_requesttoken},*/ - url: baseUrl, - method: 'GET', - dataType: 'json', - success: function (d) { + + function getData() { + var baseUrl = OC.generateUrl('/apps/timetracker/ajax/report?name=&from=' + start.unix() + '&to=' + end.unix() + '&group1=' + group1 + '&group2=' + group2 + '&timegroup=' + group3 + '&filterProjectId=' + filterProjectId + '&filterClientId=' + filterClientId); + var default_colors = [ + '#3366CC', + '#DC3912', + '#FF9900', + '#109618', + '#990099', + '#3B3EAC', + '#0099C6', + '#DD4477', + '#66AA00', + '#B82E2E', + '#316395', + '#994499', + '#22AA99', + '#AAAA11', + '#6633CC', + '#E67300', + '#8B0707', + '#329262', + '#5574A6', + '#3B3EAC', + '#C71585', + '#006400', + '#4B0082', + '#8B0000', + '#FF4500', + '#BDB76B', + '#008080', + '#FFE4E1', + '#800000', + '#000080', + '#2F4F4F', + '#FF1493', + '#008000', + '#800080', + '#FF0000', + '#FF8C00', + '#20B2AA', + '#FAF0E6', + '#FFD700', + '#A52A2A', + '#708090', + '#DB7093', + '#808000', + '#9400D3', + '#FFDAB9', + '#7FFFD4' + ] + $.ajax({ + /*headers: {requesttoken: oc_requesttoken},*/ + url: baseUrl, + method: 'GET', + dataType: 'json', + success: function (d) { chartData = { - labels: [], - datasets: [{ data: [], backgroundColor: []}, { data: [], backgroundColor: []},], - - }; - var clientMap = {}; - var nclients = 0; - // extract clients in clientMap - var totalMinutes = 0; - for (var x = 0; x < d.items.length; x++){ + labels: [], + datasets: [{data: [], backgroundColor: []}, {data: [], backgroundColor: []},], + }; + costChartData = { + labels: [], + datasets: [{data: [], backgroundColor: []}, {data: [], backgroundColor: []},], + }; + var clientMap = {}; + var nclients = 0; + var totalCost = 0; + // extract clients in clientMap + var totalMinutes = 0; + for (var x = 0; x < d.items.length; x++) { cid = d.items[x].client; - if (cid == null){ - cid = -1; + if (cid == null) { + cid = -1; } - if (clientMap[cid] === undefined){ - clientMap[cid] = {duration:d.items[x].totalDuration, order:nclients, client:(d.items[x].client == null)?'Not Set':d.items[x].client} - totalMinutes += d.items[x].totalDuration/60.0; + if (clientMap[cid] === undefined) { + clientMap[cid] = { + duration: d.items[x].totalDuration, + order: nclients, + client: (d.items[x].client == null) ? 'Not Set' : d.items[x].client, + cost: d.items[x].cost + } + totalMinutes += d.items[x].totalDuration / 60.0; + totalCost += d.items[x].cost; nclients++; } else { - clientMap[cid].duration = clientMap[cid].duration + d.items[x].totalDuration; - totalMinutes += d.items[x].totalDuration/60.0; + clientMap[cid].duration = clientMap[cid].duration + d.items[x].totalDuration; + clientMap[cid].cost = clientMap[cid].cost + d.items[x].cost; + totalMinutes += d.items[x].totalDuration / 60.0; + totalCost += d.items[x].cost; } - } - var mx = 0; - var nindex = nclients; - - var sortable = []; - for (var client in clientMap) { - sortable.push([client, clientMap[client].order]); - } - - sortable.sort(function(a, b) { - return a[1] - b[1]; - }); - for (var i = 0; i < sortable.length; i++) { - t = sortable[i]; - key = t[0]; - + } + var mx = 0; + var nindex = nclients; + + var sortable = []; + for (var client in clientMap) { + sortable.push([client, clientMap[client].order]); + } + + sortable.sort(function (a, b) { + return a[1] - b[1]; + }); + for (var i = 0; i < sortable.length; i++) { + t = sortable[i]; + key = t[0]; + // first add clients chartData.datasets[0].data[clientMap[key].order] = clientMap[key].duration / 60.0; chartData.datasets[1].data[clientMap[key].order] = 0; - if (clientMap[key].client == -1){ - chartData.labels[clientMap[key].order] = "Client Not Set"; - + + costChartData.datasets[0].data[clientMap[key].order] = (clientMap[key].cost / 100); + costChartData.datasets[1].data[clientMap[key].order] = 0; + + if (clientMap[key].client == -1) { + chartData.labels[clientMap[key].order] = "Client Not Set"; + costChartData.labels[clientMap[key].order] = "Client Not Set"; + } else { - chartData.labels[clientMap[key].order] = clientMap[key].client; + chartData.labels[clientMap[key].order] = clientMap[key].client; + costChartData.labels[clientMap[key].order] = clientMap[key].client; } chartData.datasets[0].backgroundColor[clientMap[key].order] = default_colors[clientMap[key].order]; chartData.datasets[1].backgroundColor[clientMap[key].order] = default_colors[clientMap[key].order]; + + costChartData.datasets[0].backgroundColor[clientMap[key].order] = default_colors[clientMap[key].order]; + costChartData.datasets[1].backgroundColor[clientMap[key].order] = default_colors[clientMap[key].order]; // add projects for each client - - for (var x = 0; x < d.items.length; x++){ - - if (d.items[x].client === key || (d.items[x].client == null && key == -1)){ - chartData.datasets[0].data[nindex] = 0; - chartData.datasets[1].data[nindex] = d.items[x].totalDuration/60.0; - - chartData.datasets[1].backgroundColor[nindex] = default_colors[nindex]; - chartData.datasets[0].backgroundColor[nindex] = default_colors[nindex]; - if (d.items[x].project == null){ - chartData.labels[nindex] = "Project Not Set"; - } else { - chartData.labels[nindex] = d.items[x].project; + + for (var x = 0; x < d.items.length; x++) { + + if (d.items[x].client === key || (d.items[x].client == null && key == -1)) { + chartData.datasets[0].data[nindex] = 0; + chartData.datasets[1].data[nindex] = d.items[x].totalDuration / 60.0; + + costChartData.datasets[0].data[nindex] = 0; + costChartData.datasets[1].data[nindex] = d.items[x].cost; + + chartData.datasets[1].backgroundColor[nindex] = default_colors[nindex]; + chartData.datasets[0].backgroundColor[nindex] = default_colors[nindex]; + + costChartData.datasets[1].backgroundColor[nindex] = default_colors[nindex]; + costChartData.datasets[0].backgroundColor[nindex] = default_colors[nindex]; + + + if (d.items[x].project == null) { + chartData.labels[nindex] = "Project Not Set"; + costChartData.labels[nindex] = "Project Not Set" + } else { + chartData.labels[nindex] = d.items[x].project; + costChartData.labels[nindex] = d.items[x].project; + } + nindex++; } - nindex++; - } } - }; - if (myDoughnutChart != null){ + } + ; + + console.log(chartData); + console.log(costChartData); + if (myDoughnutChart != null) { myDoughnutChart.destroy(); - } + } + + if (costChart != null) { + costChart.destroy(); + } - var ctx = document.getElementById("myChart").getContext("2d"); + var ctx = document.getElementById("myChart").getContext("2d"); - myDoughnutChart = new Chart(ctx, { + myDoughnutChart = new Chart(ctx, { type: 'doughnut', data: chartData, options: { - tooltips: { - callbacks: { - title: function(tooltipItem, data) { - return data['labels'][tooltipItem[0]['index']]; - }, - label: function(tooltipItem, data) { - - mm = data['datasets'][tooltipItem.datasetIndex]['data'][tooltipItem['index']]; - h = Math.trunc(mm / 60); - m = Math.trunc(mm % 60); - return (h+" hours "+m+" minutes") - }, - afterLabel: function(tooltipItem, data) { - var dataset = data['datasets'][tooltipItem.datasetIndex]; - if (!(0 in dataset["_meta"])) - return ''; - var percent = Math.round((dataset['data'][tooltipItem['index']] / dataset["_meta"][0]['total']) * 100) - return '(' + percent + '%)'; - }, - }, - - } + tooltips: { + callbacks: { + title: function (tooltipItem, data) { + return data['labels'][tooltipItem[0]['index']]; + }, + label: function (tooltipItem, data) { + + mm = data['datasets'][tooltipItem.datasetIndex]['data'][tooltipItem['index']]; + h = Math.trunc(mm / 60); + m = Math.trunc(mm % 60); + return (h + " hours " + m + " minutes") + }, + afterLabel: function (tooltipItem, data) { + var dataset = data['datasets'][tooltipItem.datasetIndex]; + if (!(0 in dataset["_meta"])) + return ''; + var percent = Math.round((dataset['data'][tooltipItem['index']] / dataset["_meta"][0]['total']) * 100) + return '(' + percent + '%)'; + }, + }, + + } } }); + + var costChartCtx = document.getElementById("costChart").getContext("2d"); + + costChart = new Chart(costChartCtx, { + type: 'doughnut', + data: costChartData, + options: { + tooltips: { + callbacks: { + title: function (tooltipItem, data) { + return data['labels'][tooltipItem[0]['index']]; + }, + label: function (tooltipItem, data) { + var c = data['datasets'][tooltipItem.datasetIndex]['data'][tooltipItem['index']]; + return "Cost: " + (c / 100).toFixed(2); + } + }, + + } + } + }); + h = Math.trunc(totalMinutes / 60); m = Math.trunc(totalMinutes % 60); - $('#summary').html('Total time: '+h+" hours "+m+" minutes"); - } - }); - } - }); - } ); + $('#summary').html('Total time: ' + h + " hours " + m + " minutes" + "
" + "Total costs: " + (totalCost / 100).toFixed(2)); + } + }); + } + }); + }); }()); diff --git a/js/src/reports.js b/js/src/reports.js index be731b8..a154de1 100644 --- a/js/src/reports.js +++ b/js/src/reports.js @@ -52,7 +52,7 @@ var dtf = require("./dateformat.js"); 'The Month Before Last': [moment().subtract(2, 'month').startOf('month'), moment().subtract(2, 'month').endOf('month')], 'This Year': [moment().startOf('year'), moment().endOf('year')], 'Last Year': [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')], - + }, locale: { format: dtf.dformat(), @@ -68,15 +68,15 @@ var dtf = require("./dateformat.js"); $("#group1").select2(); $("#group2").select2(); $("#group3").select2(); - $('#group1').on("select2:select select2:unselect", function(e) { + $('#group1').on("select2:select select2:unselect", function(e) { group1 = e.params.data.id; getReport(); }); - $('#group2').on("select2:select select2:unselect", function(e) { + $('#group2').on("select2:select select2:unselect", function(e) { group2 = e.params.data.id; getReport(); }); - $('#group3').on("select2:select select2:unselect", function(e) { + $('#group3').on("select2:select select2:unselect", function(e) { group3 = e.params.data.id; getReport(); }); @@ -96,15 +96,15 @@ var dtf = require("./dateformat.js"); ); return $state; }, - ajax: { + ajax: { tags: true, url: OC.generateUrl('/apps/timetracker/ajax/projects'), - + dataType: 'json', delay: 250, - + processResults: function (data, page) { //json parse - return { + return { results: $.map(data.Projects,function(val, i){ return { id: val.id, text:val.name, color: val.color}; }), @@ -114,13 +114,13 @@ var dtf = require("./dateformat.js"); }; }, cache: false, - + }, }); - $('#filter-project').on("select2:select select2:unselect", function(e) { - - + $('#filter-project').on("select2:select select2:unselect", function(e) { + + filterProjectId = ($(e.target).val() != null)? $(e.target).val() : ""; getReport(); }); @@ -133,14 +133,14 @@ var dtf = require("./dateformat.js"); escapeMarkup : function(markup) { return markup; }, placeholder: "Select client", allowClear: true, - ajax: { + ajax: { tags: true, url: OC.generateUrl('/apps/timetracker/ajax/clients'), - + dataType: 'json', delay: 250, processResults: function (data, page) { //json parse - return { + return { results: $.map(data.Clients,function(val, i){ return { id: val.id, text:val.name}; }), @@ -150,14 +150,14 @@ var dtf = require("./dateformat.js"); }; }, cache: false, - + }, }); - - $('#filter-client').on("select2:select select2:unselect", function(e) { - - + + $('#filter-client').on("select2:select select2:unselect", function(e) { + + filterClientId = ($(e.target).val() != null)? $(e.target).val() : ""; getReport(); }); @@ -165,8 +165,8 @@ var dtf = require("./dateformat.js"); $('input.select2-input').attr('autocomplete', "xxxxxxxxxxx"); - - + + function getReport(){ var baseUrl = OC.generateUrl('/apps/timetracker/ajax/report?name=&from='+start.unix()+'&to='+end.unix()+'&group1='+group1+'&group2='+group2+'&timegroup='+group3+'&filterProjectId='+filterProjectId+'&filterClientId='+filterClientId); function pad(n, width, z) { @@ -203,6 +203,10 @@ var dtf = require("./dateformat.js"); var nullCheckAccessor = function(value, data, type, params, column){ return value ? value : ''; } + + var money = function(value, data, type, params, component) { + return value / 100; + } var table = new Tabulator("#report", { ajaxURL:baseUrl, layout:"fitColumns", @@ -229,6 +233,7 @@ var dtf = require("./dateformat.js"); return moment.unix(t).format(dtf.dtformat()); } }}, + {title:"Cost", field:"cost", mutator:money, accessorDownload: nullCheckAccessor, widthGrow:1, bottomCalc: "sum", formatter: "money", bottomCalcFormatter: "money"}, //column will be allocated 1/5 of the remaining space {title:"Total Duration", field:"totalDuration",accessorDownload:totalDurationAccessor,formatter:function(cell, formatterParams, onRendered){ //cell - the cell component //formatterParams - parameters set for the column @@ -238,7 +243,7 @@ var dtf = require("./dateformat.js"); var m = Math.floor( (duration/60) % 60 ); var h = Math.floor( (duration/(60*60))); return pad(h,2) + ':' + pad(m,2) + ':' + pad(s,2); - + },bottomCalc:"sum", bottomCalcParams:{ precision:1, },bottomCalcFormatter:function(cell, formatterParams, onRendered){ @@ -263,11 +268,11 @@ var dtf = require("./dateformat.js"); var time = cell.getRow().getData().time; var duration = cell.getRow().getData().totalDuration; return moment.unix(parseInt(time) + parseInt(duration)).format(dtf.dtformat()); - + }}, ], ajaxResponse:function(url, params, response){ - + return response.items; //return the tableData property of a response json object }, }); diff --git a/js/src/timer.js b/js/src/timer.js index 094cd4c..d4c3ffe 100644 --- a/js/src/timer.js +++ b/js/src/timer.js @@ -13,10 +13,10 @@ require('../../css/style.css'); var dtf = require("./dateformat.js"); ( - + function() { - + $.ajaxSetup({ headers: { 'RequestToken': OC.requestToken } }); @@ -70,7 +70,7 @@ function() { '`': '`', '=': '=' }; - + function escapeHtml (string) { return String(string).replace(/[&<>"'`=\/]/g, function (s) { return entityMap[s]; @@ -116,10 +116,10 @@ function() { validateManualEntryFields(); }); - + $("#dialog-manual-entry").dialog({ autoOpen: false, - buttons : + buttons : [ { id: 'confirm-button', text: "Confirm", @@ -183,7 +183,7 @@ function() { alert( "error" ); }) .always(function() { - + }); } @@ -223,7 +223,7 @@ function() { //contentType: "application/json; charset=utf-8" }); $.getJSON( baseUrl, function( data ) { - + if (data.running.length > 0){ localStorage.setItem('isTimerStarted', true); localStorage.setItem('timerStartTime', data.running[0].start); @@ -246,16 +246,18 @@ function() { $('#timer').html(secondsToTimer(0)); $('#start-tracking > span').addClass("play-button").removeClass("stop-button"); } - + var days = []; $.each( data.days, function( dayName, dayMap ) { - + var dayItems = []; $.each(dayMap, function (dayItemName, workItem){ var children = []; - + $.each(workItem.children, function (ckey, child){ + var cost = '' + if (child.cost !== 0) { var cost = (child.cost / 100).toFixed(2) } children.push( "
  • "+ "
    "+ @@ -270,6 +272,7 @@ function() { ""+ + "
    "+ "
    "+ tsToHour(child.start)+" - "+((child.running == 1)?'':tsToHour(child.start+child.duration))+ "
    "+ @@ -302,8 +305,8 @@ function() { days.push( "
    "+ "
      "+ "
    • " + - "
      " + dayName +"
      "+ - dayItems.join("") + + "
      " + dayName +"
      "+ + dayItems.join("") + "
    • "+ "
    "+ "
    " ); @@ -314,6 +317,27 @@ function() { html: days.join( "" ) })); + $(".cost").focusout(function(e) { + e.preventDefault(); + var input = $(this); + var cost = $(this).val(); + var id = $(e.target).data('myid'); + var baseUrl = OC.generateUrl('/apps/timetracker/ajax/add-cost/' + id); + if (cost !== undefined) { + $.post(baseUrl, {cost: cost}, 'json').done(function (e) { + input.css('border', 'solid 1px green'); + setTimeout(function () { + input.css('border', ''); + }, 3000); + }).fail(function (xhr, status, error) { + var errorMessage = JSON.parse(xhr.responseText); + if (errorMessage.error !== undefined) { + alert(errorMessage.error); + } + input.css('border', 'solid 1px red'); + }); + } + }); $(".wi-child-hours").each(function(){ $(this).daterangepicker({ @@ -348,7 +372,7 @@ function() { $('.wi-child-name').click(function(e) { e.preventDefault(); dialogWorkItemEditForm.target = e.target; - + var form = dialogWorkItemEditForm.find( "form" ) form.find("#name").val($(e.target).data("name")); form.find("#details").val($(e.target).data("details")); @@ -425,11 +449,11 @@ function() { }, ajax: { url: projectsAjaxUrl, - + dataType: 'json', delay: 250, processResults: function (data) { //json parse - return { + return { results: $.map(data.Projects,function(val, i){ return { id: val.id, text:val.name, color: val.color}; }), @@ -439,35 +463,35 @@ function() { }; }, cache: false, - + }, }); $(this).data('myid',id); - + $(this).val(projectId).trigger('change'); //clearInterval(interval); //}.bind(this),0); }); - + var tagsAjaxUrl = OC.generateUrl('/apps/timetracker/ajax/tags'); $(".set-tag").each(function(){ //var interval = setInterval( function() { - + $(this).select2({ tags: true, containerCssClass:'tags-select', placeholder: "Select tags...", allowClear: true, - - ajax: { + + ajax: { url: function () { return tagsAjaxUrl+'?workItem='+$(this).data('myid');}, data: function (params){ var query = { q: params.term, type: 'public' } - + // Query parameters will be ?search=[term]&type=public return query; }, @@ -478,7 +502,7 @@ function() { dataType: 'json', delay: 250, processResults: function (data) { //json parse - return { + return { results: $.map(data.Tags,function(val, i){ return { id: val.id, text:val.name}; }), @@ -501,8 +525,8 @@ function() { var myid = $(e.target).data('myid'); var selectedId = $(e.target).val(); var jqxhr = $.post( "ajax/update-work-interval/"+myid,{projectId:selectedId}, function() { - - + + }) .done(function(data, status, jqXHR) { var response = data; @@ -524,7 +548,7 @@ function() { }, 0); }); - $(".set-tag").on("change", function (e) { + $(".set-tag").on("change", function (e) { var myid = $(e.target).data('myid'); var selectedTag = $(e.target).val(); var jqxhr = $.post( "ajax/update-work-interval/"+myid,{tagId:selectedTag.join(",")}, function() { @@ -579,10 +603,10 @@ function() { }).always(function() { getWorkItems(); }); - + } function stopTimer(onStopped = null, args = []){ - + var workName = $('#work-input').val(); if (workName == ''){ workName = 'no description'; @@ -608,7 +632,7 @@ function() { alert( "error" ); }) .always(function() { - + }); } @@ -616,7 +640,7 @@ function() { $( "#datepicker-from" ).datepicker(); $( "#datepicker-to" ).datepicker(); - + if(localStorage.getItem('isTimerStarted') === 'true'){ $('#start-tracking > span').addClass("stop-button").removeClass("play-button"); } else { @@ -631,5 +655,5 @@ function() { return false; }); } ); - + }()); diff --git a/lib/Controller/AjaxController.php b/lib/Controller/AjaxController.php index b064743..82e6ef5 100644 --- a/lib/Controller/AjaxController.php +++ b/lib/Controller/AjaxController.php @@ -1,5 +1,6 @@ userId = $UserId; @@ -102,7 +103,7 @@ public function workIntervals() { if ($wi->projectId != null){ $project = $this->projectMapper->find($wi->projectId); } - + $tags = []; $wiToTags = $this->workIntervalToTagMapper->findAllForWorkInterval($wi->id); foreach($wiToTags as $wiToTag){ @@ -110,7 +111,7 @@ public function workIntervals() { if ($t != null) $tags[] = $t; } - + $wa = ['duration' => $wi->duration, 'id' => $wi->id, 'name' => $wi->name, @@ -120,19 +121,20 @@ public function workIntervals() { 'start' => $wi->start, 'tags' => $tags, 'userUid' => $wi->userUid, + 'cost' => $wi->cost, 'projectName' => ($project === null)?null:$project->name, 'projectColor' => ($project === null)?null:$project->color, ]; $days[$dt][$wi->name]['children'][] = $wa; $days[$dt][$wi->name]['totalTime'] += $wa['duration']; } - + $running = $this->workIntervalMapper->findAllRunning($this->userId); return new JSONResponse(["WorkIntervals" => $l, "running" => $running, 'days' => $days, 'now' => time()]); } - + public function isThisAdminUser(){ return \OC_User::isAdminUser(\OC_User::getUser()); } @@ -147,9 +149,31 @@ public function isUserAdmin($user){ * @NoCSRFRequired */ public function index() { - + } + public function addCost($id) + { + $wi = $this->workIntervalMapper->find($id); + $cost = $this->request->cost; + + if (!is_numeric($cost)) { + return new JSONResponse(['error' => 'Non numeric value'], Http::STATUS_BAD_REQUEST); + } + + $costInCents = $cost * 100; + + $wi->setCost($costInCents); + + try { + $this->workIntervalMapper->update($wi); + } catch (\Exception $e) { + return new JSONResponse(['error' => true, 'message' => $e->getMessage()]); + } + + return new JSONResponse(['success' => true, 'test' => $cost ?? 't']); + } + /** * * @NoAdminRequired @@ -176,11 +200,11 @@ public function startTimer($name) { $winterval->setRunning(1); $winterval->setName($name); $winterval->setUserUid($this->userId); - + // first get tags and project ids from the last work item with the same name $lwinterval = $this->workIntervalMapper->findLatestByName($this->userId, $name); if ($projectId == null && $lwinterval != null){ - + $winterval->setProjectId($lwinterval->projectId); } @@ -200,7 +224,7 @@ public function startTimer($name) { } } - + if ($tags != null){ $tagsArray = explode(",", $tags); foreach($tagsArray as $t){ @@ -213,12 +237,12 @@ public function startTimer($name) { } - - - + + + //echo json_encode((array)$winterval); return new JSONResponse(["WorkIntervals" => $winterval, "running" => 1]); - + } @@ -237,7 +261,7 @@ public function stopTimer($name) { } $running = $this->workIntervalMapper->findAllRunning($this->userId); - + $now = time(); foreach($running as $r){ $r->setRunning(0); @@ -260,7 +284,7 @@ public function deleteWorkInterval($id) { $this->workIntervalMapper->delete($wi); $running = $this->workIntervalMapper->findAllRunning($this->userId); - + return new JSONResponse(["WorkIntervals" => json_decode(json_encode($running), true)]); } @@ -270,9 +294,9 @@ public function deleteWorkInterval($id) { */ public function updateWorkInterval($id) { - + $wi = $this->workIntervalMapper->find($id); - + if (isset($this->request->name)) { if (strlen($this->request->name) > 255){ return new JSONResponse(["Error" => "Name too long"]); @@ -306,14 +330,14 @@ public function updateWorkInterval($id) { $newWiToTag->setTagId($tag); $newWiToTag->setCreatedAt(time()); $this->workIntervalToTagMapper->insert($newWiToTag); - + } - + } } } - + if (isset($this->request->tagId)) { if (is_array($this->request->tagId)){ $tags = $this->request->tagId; @@ -324,7 +348,7 @@ public function updateWorkInterval($id) { $this->workIntervalToTagMapper->deleteAllForWorkInterval($id); $project = null; $locked = 0; - + foreach($tags as $tag){ if (empty($tag)) @@ -365,10 +389,10 @@ public function updateWorkInterval($id) { $de->setTimeZone(new \DateTimeZone('UTC')); $wi->setDuration($de->getTimestamp() - $dt->getTimestamp()); } - + $this->workIntervalMapper->update($wi); $running = $this->workIntervalMapper->findAllRunning($this->userId); - + return new JSONResponse(["WorkIntervals" => json_decode(json_encode($running), true)]); } @@ -379,11 +403,11 @@ public function updateWorkInterval($id) { */ public function addWorkInterval() { - + $wi = new WorkInterval(); $wi->setUserUid($this->userId); $wi->setRunning(0); - + if (isset($this->request->name)) { $wi->setName(urldecode($this->request->name)); } @@ -414,9 +438,9 @@ public function addWorkInterval() { $newWiToTag->setTagId($tag); $newWiToTag->setCreatedAt(time()); $this->workIntervalToTagMapper->insert($newWiToTag); - + } - + } } @@ -426,7 +450,7 @@ public function addWorkInterval() { $this->workIntervalToTagMapper->deleteAllForWorkInterval($id); $project = null; $locked = 0; - + foreach($tags as $tag){ if (empty($tag)) @@ -456,7 +480,7 @@ public function addWorkInterval() { } $this->workIntervalMapper->insert($wi); - + return new JSONResponse(["WorkIntervals" => json_decode(json_encode($running), true)]); } @@ -524,7 +548,7 @@ public function deleteClient($id) { $utoc = $this->userToClientMapper->findForUserAndClient($this->userId, $c); if ($utoc != null){ - + $this->userToClientMapper->delete($utoc); } return $this->getClients(); @@ -541,7 +565,7 @@ public function getClients(){ return new JSONResponse(["Clients" => json_decode(json_encode($clients), true)]); } - + /** @@ -639,10 +663,10 @@ public function editProject($id) { $this->userToProjectMapper->insert($up); } } - + if (isset($this->request->archived) && $p->getArchived() != $this->request->archived){ if (($this->isThisAdminUser() || $p->createdByUserUid == $this->userId) ){ - + $archived = $this->request->archived; $p->setArchived($archived); } else { @@ -651,7 +675,7 @@ public function editProject($id) { } $this->projectMapper->update($p); - + return $this->getProjects(); } /** @@ -665,7 +689,7 @@ public function deleteProject($id) { $utop = $this->userToProjectMapper->findForUserAndProject($this->userId, $p); if ($utop != null){ - + $this->userToProjectMapper->delete($utop); } return $this->getProjects(); @@ -686,11 +710,11 @@ public function deleteProjectWithData($id) { $this->tagMapper->allowedTags($id,[]); $this->projectMapper->delete($id); - + return $this->getProjects(); } - + /** * * @NoAdminRequired @@ -744,7 +768,7 @@ public function getProjectsTable(){ $out['clientId'] = $client->id; } } - + $outProjects[] = $out; } @@ -770,7 +794,7 @@ public function addTag($name) { return new JSONResponse(["Error" => "This tag name already exists"]); } - + return $this->getTags(); } /** @@ -793,7 +817,7 @@ public function editTag($id) { } $c->setName($name); $this->tagMapper->update($c); - + return $this->getTags(); } /** @@ -823,7 +847,7 @@ public function getTags(){ $wi = $this->workIntervalMapper->find($workItem); if ($wi->projectId != null){ $project = $this->projectMapper->find($wi->projectId); - + } } if($project != null && $project->locked){ @@ -869,7 +893,7 @@ public function getReport(){ $name = $this->userId; } - + if(!$this->isThisAdminUser()){ $allowedClients = $this->clientMapper->findAll($this->userId); $allowedClientsId = array_map(function($client){ return $client->id;}, $allowedClients ); @@ -889,7 +913,7 @@ public function getReport(){ } } - + $filterTagId = []; $groupOn1 = $this->request->group1; $groupOn2 = $this->request->group2; @@ -923,7 +947,7 @@ public function postTimeline(){ $name = $this->userId; } - + if(!$this->isThisAdminUser()){ $allowedClients = $this->clientMapper->findAll($this->userId); $allowedClientsId = array_map(function($client){ return $client->id;}, $allowedClients ); @@ -943,12 +967,12 @@ public function postTimeline(){ } } - + $filterTagId = []; $groupOn1 = $this->request->group1; $groupOn2 = $this->request->group2; $items = $this->reportItemMapper->report($name, $from, $to, $filterProjectId, $filterClientId, $filterTagId, $timegroup, $groupOn1, $groupOn2, $this->isThisAdminUser(), 0, 1000); - + $timeline = new Timeline(); $timeline->setUserUid($this->userId); $timeline->setGroup1($this->request->group1); @@ -976,6 +1000,7 @@ public function postTimeline(){ $te->setTimeInterval($i->time); $te->setTotalDuration($i->totalDuration); $te->setCreatedAt(time()); + $te->setCost($i->cost); $this->timelineEntryMapper->insert($te); } @@ -1015,7 +1040,7 @@ public function secondsToTime($seconds){ $timeFormat = sprintf('%02d:%02d:%02d', $hours, $mins, $secs); return $timeFormat; } - + /** * * @NoAdminRequired @@ -1040,7 +1065,7 @@ public function downloadTimeline($id){ fputcsv($output, array('id', 'User Uid', 'Name', 'Project Name', 'Client Name', 'Time Interval', 'Total Duration')); $totalDuration = 0; foreach($te as $t){ - + fputcsv($output, [$t->id, $t->userUid, $t->name, $t->projectName, $t->clientName, $t->timeInterval, $this->secondsToTime($t->totalDuration)]); $totalDuration += $t->totalDuration; } @@ -1092,7 +1117,7 @@ public function emailTimeline($id) { $emails = explode(';',$email); $subject = $this->request->subject; $content = $this->request->content; - + // output headers so that the file is downloaded rather than displayed header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=timeline-'.$user.'-'.$id.'.csv'); @@ -1106,14 +1131,14 @@ public function emailTimeline($id) { fputcsv($output, array('id', 'User Uid', 'Name', 'Project Name', 'Client Name', 'Time Interval', 'Total Duration')); $totalDuration = 0; foreach($te as $t){ - + fputcsv($output, [$t->id, $t->userUid, $t->name, $t->projectName, $t->clientName, $t->timeInterval, $this->secondsToTime($t->totalDuration)]); $totalDuration += $t->totalDuration; } fputcsv($output, ['TOTAL', '', '', '', '', '', $this->secondsToTime($totalDuration)]); - - - + + + $mailer = \OC::$server->getMailer(); $message = $mailer->createMessage(); $attach = $mailer->createAttachmentFromPath($path); @@ -1124,7 +1149,7 @@ public function emailTimeline($id) { //$message->setHtmlBody($content); $message->attach($attach); $mailer->send($message); - + fclose($output); unlink($path); return new JSONResponse([]); @@ -1157,10 +1182,10 @@ public function addGoal() { return new JSONResponse(["Error" => "There can be only one goal per project"]); } - + return $this->getGoals(); } - + /** * * @NoAdminRequired @@ -1229,7 +1254,7 @@ public function getGoals(){ $goals = $this->goalMapper->findAll($this->userId); $weekStart = $this->getStartOfWeek(time())->format('Y-m-d'); $monthStart = $this->getStartOfMonth(time())->format('Y-m'); - + $ret = []; foreach($goals as $goal){ $rgoal = []; @@ -1252,11 +1277,11 @@ public function getGoals(){ if ($goal->interval == 'Weekly'){ if ($interval == $this->getStartOfWeek($repItem->time)->format('Y-m-d')) { $workedInInterval += $repItem->totalDuration; - } + } } elseif ($goal->interval == 'Monthly'){ if ($interval == $this->getStartOfMonth($repItem->time)->format('Y-m')) { $workedInInterval += $repItem->totalDuration; - } + } } } $debtSeconds += ($goal->hours*3600 - $workedInInterval); diff --git a/lib/Db/ReportItem.php b/lib/Db/ReportItem.php index c7cb0eb..1388980 100644 --- a/lib/Db/ReportItem.php +++ b/lib/Db/ReportItem.php @@ -17,12 +17,13 @@ class ReportItem extends Entity { public $project; public $clientId; public $client; + public $cost; public function __construct() { // add types in constructor - + $this->addType('id', 'integer'); $this->addType('name', 'string'); $this->addType('details', 'string'); @@ -32,5 +33,6 @@ public function __construct() { $this->addType('ftime', 'string'); $this->addType('totalDuration', 'integer'); $this->addType('project', 'string'); + $this->addType('cost', 'integer'); } } diff --git a/lib/Db/ReportItemMapper.php b/lib/Db/ReportItemMapper.php index 16be6e6..2ee180b 100644 --- a/lib/Db/ReportItemMapper.php +++ b/lib/Db/ReportItemMapper.php @@ -32,11 +32,17 @@ public function __construct(IDBConnection $db) { public $client; */ - + public function report($user, $from, $to, $filterProjectId, $filterClientId, $filterTagId, $timegroup, $groupOn1, $groupOn2, $admin, $start, $limit ){ - + $selectFields = ['min(wi.id) as id', 'sum(duration) as "totalDuration"']; - + + if ($timegroup !== null) { + $selectFields[]= "SUM(wi.cost) as cost"; + } else { + $selectFields[] = 'wi.cost as cost'; + } + $aggregation = true; if(empty($groupOn1) && empty($groupOn2) && empty($timegroup)) { $selectFields[] = 'min(wi.details) as "details"'; @@ -103,7 +109,7 @@ public function report($user, $from, $to, $filterProjectId, $filterClientId, $fi } } - + if ($aggregation){ if(($groupOn1 != 'project') && ($groupOn2 != 'project')){ $selectFields[] = '\'*\' as "projectId"'; @@ -113,12 +119,12 @@ public function report($user, $from, $to, $filterProjectId, $filterClientId, $fi $selectFields[] = 'group_concat(distinct p.name) as project'; } } else { - + $selectFields[] = '\'*\' as "projectId"'; $selectFields[] = 'p.name as project'; } - - + + if(($groupOn1 != 'client') && ($groupOn2 != 'client')){ $selectFields[] = '\'*\' as "clientId"'; if ($this->dbengine == 'POSTGRES') { @@ -126,23 +132,23 @@ public function report($user, $from, $to, $filterProjectId, $filterClientId, $fi } else { $selectFields[] = 'group_concat(distinct c.name) as client'; } - + } else { $selectFields[] = '\'*\' as "clientId"'; $selectFields[] = 'c.name as client'; } - + if(($groupOn1 != 'userUid') && ($groupOn2 != 'userUid') && $aggregation){ if ($this->dbengine == 'POSTGRES') { $selectFields[] = 'string_agg(distinct user_uid, \',\') as "userUid"'; } else { $selectFields[] = 'group_concat(distinct user_uid) as "userUid"'; } - + } else { $selectFields[] = 'user_uid as "userUid"'; } - + } $selectItems = implode(", ",$selectFields). @@ -167,7 +173,7 @@ public function report($user, $from, $to, $filterProjectId, $filterClientId, $fi foreach($filterProjectId as $f){ $qm[] = '?'; $params[] = $f; - + if($f == null) { $append = ' or wi.project_id is null '; } @@ -205,7 +211,7 @@ public function report($user, $from, $to, $filterProjectId, $filterClientId, $fi // } $groups[] = 'ftime'; } - + if (!empty($groupOn1)){ if ($groupOn1 == "project" || $groupOn1 == "client" || $groupOn1 == "name" || $groupOn1 == "userUid") // $groups[] = $groupOn1; @@ -250,6 +256,6 @@ public function report($user, $from, $to, $filterProjectId, $filterClientId, $fi return $this->findEntities($sql, $params, $limit, $start); } - + } diff --git a/lib/Db/WorkInterval.php b/lib/Db/WorkInterval.php index 998036d..d546f16 100644 --- a/lib/Db/WorkInterval.php +++ b/lib/Db/WorkInterval.php @@ -14,11 +14,12 @@ class WorkInterval extends Entity { public $start; public $duration; public $running; + public $cost; public function __construct() { // add types in constructor - + $this->addType('id', 'integer'); $this->addType('name', 'string'); $this->addType('details', 'string'); @@ -27,5 +28,6 @@ public function __construct() { $this->addType('start', 'integer'); $this->addType('duration', 'integer'); $this->addType('running', 'integer'); + $this->addType('cost', 'integer'); } } diff --git a/lib/Migration/Version000020Date20220528101009.php b/lib/Migration/Version000020Date20220528101009.php new file mode 100644 index 0000000..1f4bcc5 --- /dev/null +++ b/lib/Migration/Version000020Date20220528101009.php @@ -0,0 +1,75 @@ + + * + * @author Your name + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\TimeTracker\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Auto-generated migration step: Please modify to your needs! + */ +class Version000020Date20220528101009 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + */ + public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + $schema = $schemaClosure(); + if ($schema->hasTable('timetracker_work_interval')) { + $table = $schema->getTable('timetracker_work_interval'); + if (!$table->hasColumn('cost')) { + $table->addColumn('cost', 'integer', [ + 'notnull' => false, + 'length' => 10, + ]); + } + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/templates/content/dashboard.php b/templates/content/dashboard.php index ac5462c..66ac410 100644 --- a/templates/content/dashboard.php +++ b/templates/content/dashboard.php @@ -15,7 +15,8 @@
    +
    - \ No newline at end of file +