Release 2.0.20 (#4028)
* max duration per entry increased from 8 to 10 hours * fixes #3981 - clickable area in dropdown too small * fixes #4008 - duplicate activities in project-details report * headers and summary styling in project-details report * show billable stats in project-details report * added new invoice variable for entry.duration_format
This commit is contained in:
@@ -73,6 +73,7 @@ export default class KimaiFormSelect extends KimaiFormPlugin {
|
|||||||
let options = {
|
let options = {
|
||||||
lockOptgroupOrder: true,
|
lockOptgroupOrder: true,
|
||||||
allowEmptyOption: !isRequired,
|
allowEmptyOption: !isRequired,
|
||||||
|
hidePlaceholder: false,
|
||||||
plugins: plugins,
|
plugins: plugins,
|
||||||
// if there are more than X entries, the other ones are hidden and can only be found
|
// if there are more than X entries, the other ones are hidden and can only be found
|
||||||
// by typing some characters to trigger the internal option search
|
// by typing some characters to trigger the internal option search
|
||||||
|
|||||||
@@ -79,15 +79,6 @@ table.dataTable thead > tr > th.hw-min {
|
|||||||
max-height: 340px;
|
max-height: 340px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
|
||||||
.card-tools {
|
|
||||||
/* a button that is directly connected with a pagination element inside the box tools (eg. project list on customer detail page) */
|
|
||||||
.btn-pager {
|
|
||||||
float: left;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#customer_details_box,
|
#customer_details_box,
|
||||||
#project_details_box,
|
#project_details_box,
|
||||||
#activity_details_box {
|
#activity_details_box {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
"friendsofsymfony/rest-bundle": "^3.0",
|
"friendsofsymfony/rest-bundle": "^3.0",
|
||||||
"gedmo/doctrine-extensions": "^3.6",
|
"gedmo/doctrine-extensions": "^3.6",
|
||||||
"jms/serializer-bundle": "^5.0",
|
"jms/serializer-bundle": "^5.0",
|
||||||
"kevinpapst/tabler-bundle": "^0.16",
|
"kevinpapst/tabler-bundle": "^0.17",
|
||||||
"league/csv": "^9.4",
|
"league/csv": "^9.4",
|
||||||
"mpdf/mpdf": "^8.0",
|
"mpdf/mpdf": "^8.0",
|
||||||
"nelmio/api-doc-bundle": "^4.0",
|
"nelmio/api-doc-bundle": "^4.0",
|
||||||
|
|||||||
14
composer.lock
generated
14
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "26d6641fd4c8f977b0a50cab05e1df87",
|
"content-hash": "63b69c5c8020a78b308210425547d46c",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -2416,16 +2416,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "kevinpapst/tabler-bundle",
|
"name": "kevinpapst/tabler-bundle",
|
||||||
"version": "0.16",
|
"version": "0.17",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/kevinpapst/TablerBundle.git",
|
"url": "https://github.com/kevinpapst/TablerBundle.git",
|
||||||
"reference": "c5986daa3595fc5163f7d0cb15f69dc5f696b987"
|
"reference": "0061d82a28059cf876cf819f1780607aac131d32"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/c5986daa3595fc5163f7d0cb15f69dc5f696b987",
|
"url": "https://api.github.com/repos/kevinpapst/TablerBundle/zipball/0061d82a28059cf876cf819f1780607aac131d32",
|
||||||
"reference": "c5986daa3595fc5163f7d0cb15f69dc5f696b987",
|
"reference": "0061d82a28059cf876cf819f1780607aac131d32",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2474,7 +2474,7 @@
|
|||||||
"description": "Admin/Backend theme bundle for Symfony based on Tabler.io",
|
"description": "Admin/Backend theme bundle for Symfony based on Tabler.io",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/kevinpapst/TablerBundle/issues",
|
"issues": "https://github.com/kevinpapst/TablerBundle/issues",
|
||||||
"source": "https://github.com/kevinpapst/TablerBundle/tree/0.16"
|
"source": "https://github.com/kevinpapst/TablerBundle/tree/0.17"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2486,7 +2486,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-05-15T12:01:30+00:00"
|
"time": "2023-05-15T18:25:59+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laminas/laminas-escaper",
|
"name": "laminas/laminas-escaper",
|
||||||
|
|||||||
43
phpstan.neon
43
phpstan.neon
@@ -4836,11 +4836,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Plugin/PluginMetadata.php
|
path: src/Plugin/PluginMetadata.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot access offset 'amount' on mixed\\.$#"
|
message: "#^Cannot access offset 'amount' on mixed\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4881,16 +4876,6 @@ parameters:
|
|||||||
count: 2
|
count: 2
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Activity\\|null\\.$#"
|
|
||||||
count: 2
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Cannot call method getId\\(\\) on mixed\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Cannot call method getMonth\\(\\) on App\\\\Model\\\\Statistic\\\\Year\\|null\\.$#"
|
message: "#^Cannot call method getMonth\\(\\) on App\\\\Model\\\\Statistic\\\\Year\\|null\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4911,24 +4896,14 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$activity of method App\\\\Model\\\\ActivityStatistic\\:\\:setActivity\\(\\) expects App\\\\Entity\\\\Activity, App\\\\Entity\\\\Activity\\|null given\\.$#"
|
|
||||||
count: 2
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$billableDuration of method App\\\\Model\\\\Statistic\\\\Month\\:\\:setBillableDuration\\(\\) expects int, float\\|int given\\.$#"
|
message: "#^Parameter \\#1 \\$billableDuration of method App\\\\Model\\\\Statistic\\\\Month\\:\\:setBillableDuration\\(\\) expects int, float\\|int given\\.$#"
|
||||||
count: 2
|
count: 2
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$billableDuration of method App\\\\Model\\\\Statistic\\\\Year\\:\\:setBillableDuration\\(\\) expects int, float\\|int given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$counter of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setCounter\\(\\) expects int, float\\|int given\\.$#"
|
message: "#^Parameter \\#1 \\$counter of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setCounter\\(\\) expects int, float\\|int given\\.$#"
|
||||||
count: 3
|
count: 1
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
-
|
||||||
@@ -4948,7 +4923,7 @@ parameters:
|
|||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$duration of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setDuration\\(\\) expects int, float\\|int given\\.$#"
|
message: "#^Parameter \\#1 \\$duration of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setDuration\\(\\) expects int, float\\|int given\\.$#"
|
||||||
count: 3
|
count: 1
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
-
|
||||||
@@ -4971,11 +4946,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$key of function array_key_exists expects int\\|string, int\\|null given\\.$#"
|
|
||||||
count: 2
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$project of class App\\\\Reporting\\\\ProjectDetails\\\\ProjectDetailsModel constructor expects App\\\\Entity\\\\Project, App\\\\Entity\\\\Project\\|null given\\.$#"
|
message: "#^Parameter \\#1 \\$project of class App\\\\Reporting\\\\ProjectDetails\\\\ProjectDetailsModel constructor expects App\\\\Entity\\\\Project, App\\\\Entity\\\\Project\\|null given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
@@ -4993,7 +4963,7 @@ parameters:
|
|||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$recordDuration of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setDurationBillable\\(\\) expects int, float\\|int given\\.$#"
|
message: "#^Parameter \\#1 \\$recordDuration of method App\\\\Model\\\\TimesheetCountedStatistic\\:\\:setDurationBillable\\(\\) expects int, float\\|int given\\.$#"
|
||||||
count: 3
|
count: 1
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
-
|
||||||
@@ -5013,12 +4983,7 @@ parameters:
|
|||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Parameter \\#1 \\$totalDuration of method App\\\\Model\\\\Statistic\\\\Timesheet\\:\\:setTotalDuration\\(\\) expects int, float\\|int given\\.$#"
|
message: "#^Parameter \\#1 \\$totalDuration of method App\\\\Model\\\\Statistic\\\\Timesheet\\:\\:setTotalDuration\\(\\) expects int, float\\|int given\\.$#"
|
||||||
count: 3
|
count: 2
|
||||||
path: src/Project/ProjectStatisticService.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$user of class App\\\\Model\\\\UserStatistic constructor expects App\\\\Entity\\\\User, mixed given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/Project/ProjectStatisticService.php
|
path: src/Project/ProjectStatisticService.php
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|||||||
2
public/build/app.45d541e0.js
Normal file
2
public/build/app.45d541e0.js
Normal file
File diff suppressed because one or more lines are too long
1
public/build/app.66162a27.css
Normal file
1
public/build/app.66162a27.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,10 +3,10 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"js": [
|
"js": [
|
||||||
"/build/runtime.f0079159.js",
|
"/build/runtime.f0079159.js",
|
||||||
"/build/app.bed7a1bf.js"
|
"/build/app.45d541e0.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
"/build/app.80fb16e8.css"
|
"/build/app.66162a27.css"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"export-pdf": {
|
"export-pdf": {
|
||||||
@@ -63,8 +63,8 @@
|
|||||||
},
|
},
|
||||||
"integrity": {
|
"integrity": {
|
||||||
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
"/build/runtime.f0079159.js": "sha384-H22sAW1aTvyIPqvHOvGXWSWTxf0y6mptp+MsVmyXCfjx/WJjBbhX9gbUZ+qIuihV",
|
||||||
"/build/app.bed7a1bf.js": "sha384-OCg9guQNr23augSqlruX0IK2FWrKO1MWMx0sBRIzAkKgg561U4stZRHfxQbrwJeG",
|
"/build/app.45d541e0.js": "sha384-KhCwP7jLZJ8+g02Lr+oITVZ8woE+EU01t/VQNuDyHmpVPCVggaaoI9EU9lJeB62l",
|
||||||
"/build/app.80fb16e8.css": "sha384-kDryvDuNVyJAdIyQEuF5NpT1duQ4VmScmUNtha3B0ssQyzLuQdWBz/G3m70wao12",
|
"/build/app.66162a27.css": "sha384-TeR8dC4yDJMVshAZ+sa3WotB4RqTHOiTtHROHg7jXEE29WCabeBlXB6MKiPdnsu3",
|
||||||
"/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u",
|
"/build/export-pdf.d367a32e.js": "sha384-Z5baqnzjI636nYFs4g63ViIKBZKRW4Jhv/7PQmTEQlqhfA7eK0vUMUtiyy0R5A9u",
|
||||||
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
"/build/export-pdf.d8a6c23b.css": "sha384-ztepocHE4rnGE9eKZ4kL6jTKaePUyiwiB9TjJjstjpf/ckcKg1HedrEOOk/8ElJg",
|
||||||
"/build/invoice.2604495e.js": "sha384-D6JvhGSqlx7z72b/qD3nF3QDXPy+XsCSRGtWfs1icjDKOcd2UzuXwuSa/E1Fg2TJ",
|
"/build/invoice.2604495e.js": "sha384-D6JvhGSqlx7z72b/qD3nF3QDXPy+XsCSRGtWfs1icjDKOcd2UzuXwuSa/E1Fg2TJ",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build/app.css": "/build/app.80fb16e8.css",
|
"build/app.css": "/build/app.66162a27.css",
|
||||||
"build/app.js": "/build/app.bed7a1bf.js",
|
"build/app.js": "/build/app.45d541e0.js",
|
||||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||||
"build/export-pdf.js": "/build/export-pdf.d367a32e.js",
|
"build/export-pdf.js": "/build/export-pdf.d367a32e.js",
|
||||||
"build/invoice.css": "/build/invoice.5bea118e.css",
|
"build/invoice.css": "/build/invoice.5bea118e.css",
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '2.0.19';
|
public const VERSION = '2.0.20';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 20019;
|
public const VERSION_ID = 20020;
|
||||||
/**
|
/**
|
||||||
* The software name
|
* The software name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ final class Configuration implements ConfigurationInterface
|
|||||||
->defaultValue(0)
|
->defaultValue(0)
|
||||||
->end()
|
->end()
|
||||||
->integerNode('long_running_duration')
|
->integerNode('long_running_duration')
|
||||||
->defaultValue(480)
|
->defaultValue(600)
|
||||||
->end()
|
->end()
|
||||||
->booleanNode('require_activity')
|
->booleanNode('require_activity')
|
||||||
->defaultTrue()
|
->defaultTrue()
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ final class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
|
|||||||
'entry.total_plain' => $rate,
|
'entry.total_plain' => $rate,
|
||||||
'entry.currency' => $currency,
|
'entry.currency' => $currency,
|
||||||
'entry.duration' => $item->getDuration(),
|
'entry.duration' => $item->getDuration(),
|
||||||
|
'entry.duration_format' => $formatter->getFormattedDuration($item->getDuration()),
|
||||||
'entry.duration_decimal' => $formatter->getFormattedDecimalDuration($item->getDuration()),
|
'entry.duration_decimal' => $formatter->getFormattedDecimalDuration($item->getDuration()),
|
||||||
'entry.duration_minutes' => (int) ($item->getDuration() / 60),
|
'entry.duration_minutes' => (int) ($item->getDuration() / 60),
|
||||||
'entry.begin' => $formatter->getFormattedDateTime($begin),
|
'entry.begin' => $formatter->getFormattedDateTime($begin),
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ class Timesheet
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* For unified access, used in frontend.
|
* For unified access, used in frontend.
|
||||||
*
|
* @see getDuration()
|
||||||
* @return int
|
|
||||||
*/
|
*/
|
||||||
public function getValue(): int
|
public function getValue(): int
|
||||||
{
|
{
|
||||||
@@ -27,8 +26,7 @@ class Timesheet
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* For unified access, used in frontend.
|
* For unified access, used in frontend.
|
||||||
*
|
* @see getTotalDuration()
|
||||||
* @return int
|
|
||||||
*/
|
*/
|
||||||
public function getDuration(): int
|
public function getDuration(): int
|
||||||
{
|
{
|
||||||
@@ -47,8 +45,7 @@ class Timesheet
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* For unified access, used in frontend.
|
* For unified access, used in frontend.
|
||||||
*
|
* @see getTotalRate()
|
||||||
* @return float
|
|
||||||
*/
|
*/
|
||||||
public function getRate(): float
|
public function getRate(): float
|
||||||
{
|
{
|
||||||
@@ -67,8 +64,7 @@ class Timesheet
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* For unified access, used in frontend.
|
* For unified access, used in frontend.
|
||||||
*
|
* @see getTotalInternalRate()
|
||||||
* @return float
|
|
||||||
*/
|
*/
|
||||||
public function getInternalRate(): float
|
public function getInternalRate(): float
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,21 +29,26 @@ final class UserYear
|
|||||||
|
|
||||||
public function getDuration(): int
|
public function getDuration(): int
|
||||||
{
|
{
|
||||||
$duration = 0;
|
return $this->year->getDuration();
|
||||||
foreach ($this->year->getMonths() as $month) {
|
|
||||||
$duration += $month->getDuration();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $duration;
|
public function getBillableDuration(): int
|
||||||
|
{
|
||||||
|
return $this->year->getBillableDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRate(): float
|
public function getRate(): float
|
||||||
{
|
{
|
||||||
$rate = 0;
|
return $this->year->getRate();
|
||||||
foreach ($this->year->getMonths() as $month) {
|
|
||||||
$rate += $month->getRate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $rate;
|
public function getBillableRate(): float
|
||||||
|
{
|
||||||
|
return $this->year->getBillableRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInternalRate(): float
|
||||||
|
{
|
||||||
|
return $this->year->getInternalRate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,12 @@
|
|||||||
|
|
||||||
namespace App\Model\Statistic;
|
namespace App\Model\Statistic;
|
||||||
|
|
||||||
final class Year extends Timesheet
|
final class Year
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var Month[]
|
* @var Month[]
|
||||||
*/
|
*/
|
||||||
private array $months = [];
|
private array $months = [];
|
||||||
private int $billableDuration = 0;
|
|
||||||
private float $billableRate = 0.00;
|
|
||||||
|
|
||||||
public function __construct(private string $year)
|
public function __construct(private string $year)
|
||||||
{
|
{
|
||||||
@@ -51,23 +49,58 @@ final class Year extends Timesheet
|
|||||||
return array_values($this->months);
|
return array_values($this->months);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBillableDuration(): int
|
public function getDuration(): int
|
||||||
{
|
{
|
||||||
return $this->billableDuration;
|
$duration = 0;
|
||||||
|
|
||||||
|
foreach ($this->months as $month) {
|
||||||
|
$duration += $month->getDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setBillableDuration(int $billableDuration): void
|
return $duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBillableDuration(): int
|
||||||
{
|
{
|
||||||
$this->billableDuration = $billableDuration;
|
$duration = 0;
|
||||||
|
|
||||||
|
foreach ($this->months as $month) {
|
||||||
|
$duration += $month->getBillableDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRate(): float
|
||||||
|
{
|
||||||
|
$rate = 0.0;
|
||||||
|
|
||||||
|
foreach ($this->months as $month) {
|
||||||
|
$rate += $month->getRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBillableRate(): float
|
public function getBillableRate(): float
|
||||||
{
|
{
|
||||||
return $this->billableRate;
|
$rate = 0.0;
|
||||||
|
|
||||||
|
foreach ($this->months as $month) {
|
||||||
|
$rate += $month->getBillableRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setBillableRate(float $billableRate): void
|
return $rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInternalRate(): float
|
||||||
{
|
{
|
||||||
$this->billableRate = $billableRate;
|
$rate = 0.0;
|
||||||
|
|
||||||
|
foreach ($this->months as $month) {
|
||||||
|
$rate += $month->getInternalRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use App\Reporting\ProjectDetails\ProjectDetailsQuery;
|
|||||||
use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
||||||
use App\Reporting\ProjectView\ProjectViewModel;
|
use App\Reporting\ProjectView\ProjectViewModel;
|
||||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||||
|
use App\Repository\ActivityRepository;
|
||||||
use App\Repository\Loader\ProjectLoader;
|
use App\Repository\Loader\ProjectLoader;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
@@ -35,7 +36,6 @@ use App\Repository\UserRepository;
|
|||||||
use App\Timesheet\DateTimeFactory;
|
use App\Timesheet\DateTimeFactory;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
|
||||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,7 +43,13 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|||||||
*/
|
*/
|
||||||
class ProjectStatisticService
|
class ProjectStatisticService
|
||||||
{
|
{
|
||||||
public function __construct(private ProjectRepository $repository, private TimesheetRepository $timesheetRepository, private EventDispatcherInterface $dispatcher, private UserRepository $userRepository)
|
public function __construct(
|
||||||
|
private ProjectRepository $projectRepository,
|
||||||
|
private ActivityRepository $activityRepository,
|
||||||
|
private TimesheetRepository $timesheetRepository,
|
||||||
|
private EventDispatcherInterface $dispatcher,
|
||||||
|
private UserRepository $userRepository
|
||||||
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +80,7 @@ class ProjectStatisticService
|
|||||||
$lastChange = clone $query->getLastChange();
|
$lastChange = clone $query->getLastChange();
|
||||||
$now = new DateTime('now', $lastChange->getTimezone());
|
$now = new DateTime('now', $lastChange->getTimezone());
|
||||||
|
|
||||||
$qb2 = $this->repository->createQueryBuilder('t1');
|
$qb2 = $this->projectRepository->createQueryBuilder('t1');
|
||||||
$qb2
|
$qb2
|
||||||
->select('1')
|
->select('1')
|
||||||
->from(Timesheet::class, 't')
|
->from(Timesheet::class, 't')
|
||||||
@@ -82,7 +88,7 @@ class ProjectStatisticService
|
|||||||
->andWhere($qb2->expr()->gte('t.begin', ':begin'))
|
->andWhere($qb2->expr()->gte('t.begin', ':begin'))
|
||||||
;
|
;
|
||||||
|
|
||||||
$qb = $this->repository->createQueryBuilder('p');
|
$qb = $this->projectRepository->createQueryBuilder('p');
|
||||||
$qb
|
$qb
|
||||||
->select('p, c')
|
->select('p, c')
|
||||||
->leftJoin('p.customer', 'c')
|
->leftJoin('p.customer', 'c')
|
||||||
@@ -99,13 +105,13 @@ class ProjectStatisticService
|
|||||||
->setParameter('begin', $lastChange, Types::DATETIME_MUTABLE)
|
->setParameter('begin', $lastChange, Types::DATETIME_MUTABLE)
|
||||||
;
|
;
|
||||||
|
|
||||||
$this->repository->addPermissionCriteria($qb, $user);
|
$this->projectRepository->addPermissionCriteria($qb, $user);
|
||||||
|
|
||||||
/** @var Project[] $projects */
|
/** @var Project[] $projects */
|
||||||
$projects = $qb->getQuery()->getResult();
|
$projects = $qb->getQuery()->getResult();
|
||||||
|
|
||||||
// pre-cache customer objects instead of joining them
|
// pre-cache customer objects instead of joining them
|
||||||
$loader = new ProjectLoader($this->repository->createQueryBuilder('p')->getEntityManager(), false, false, false);
|
$loader = new ProjectLoader($this->projectRepository->createQueryBuilder('p')->getEntityManager(), false, false, false);
|
||||||
$loader->loadResults($projects);
|
$loader->loadResults($projects);
|
||||||
|
|
||||||
return $projects;
|
return $projects;
|
||||||
@@ -121,7 +127,7 @@ class ProjectStatisticService
|
|||||||
$begin = $dateRange->getBegin();
|
$begin = $dateRange->getBegin();
|
||||||
$end = $dateRange->getEnd();
|
$end = $dateRange->getEnd();
|
||||||
|
|
||||||
$qb = $this->repository->createQueryBuilder('p');
|
$qb = $this->projectRepository->createQueryBuilder('p');
|
||||||
$qb
|
$qb
|
||||||
->select('p')
|
->select('p')
|
||||||
->leftJoin('p.customer', 'c')
|
->leftJoin('p.customer', 'c')
|
||||||
@@ -144,7 +150,7 @@ class ProjectStatisticService
|
|||||||
;
|
;
|
||||||
|
|
||||||
if (!$query->isIncludeNoWork()) {
|
if (!$query->isIncludeNoWork()) {
|
||||||
$qb2 = $this->repository->createQueryBuilder('t1');
|
$qb2 = $this->projectRepository->createQueryBuilder('t1');
|
||||||
$qb2
|
$qb2
|
||||||
->select('1')
|
->select('1')
|
||||||
->from(Timesheet::class, 't')
|
->from(Timesheet::class, 't')
|
||||||
@@ -183,13 +189,13 @@ class ProjectStatisticService
|
|||||||
->setParameter('customer', $query->getCustomer());
|
->setParameter('customer', $query->getCustomer());
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->repository->addPermissionCriteria($qb, $user);
|
$this->projectRepository->addPermissionCriteria($qb, $user);
|
||||||
|
|
||||||
/** @var Project[] $projects */
|
/** @var Project[] $projects */
|
||||||
$projects = $qb->getQuery()->getResult();
|
$projects = $qb->getQuery()->getResult();
|
||||||
|
|
||||||
// pre-cache customer objects instead of joining them
|
// pre-cache customer objects instead of joining them
|
||||||
$loader = new ProjectLoader($this->repository->createQueryBuilder('p')->getEntityManager(), false, false, false);
|
$loader = new ProjectLoader($this->projectRepository->createQueryBuilder('p')->getEntityManager(), false, false, false);
|
||||||
$loader->loadResults($projects);
|
$loader->loadResults($projects);
|
||||||
|
|
||||||
return $projects;
|
return $projects;
|
||||||
@@ -398,18 +404,17 @@ class ProjectStatisticService
|
|||||||
// fetch stats grouped by ACTIVITY for all time
|
// fetch stats grouped by ACTIVITY for all time
|
||||||
$qb1 = clone $qb;
|
$qb1 = clone $qb;
|
||||||
$qb1
|
$qb1
|
||||||
->leftJoin(Activity::class, 'a', Join::WITH, 'a.id = t.activity')
|
->addSelect('IDENTITY(t.activity) as activity')
|
||||||
->addSelect('a as activity')
|
->addGroupBy('t.activity')
|
||||||
->addGroupBy('a')
|
|
||||||
;
|
;
|
||||||
|
|
||||||
/** @var array<ActivityStatistic> $activities */
|
/** @var array<string, ActivityStatistic> $activities */
|
||||||
$activities = [];
|
$activities = [];
|
||||||
foreach ($qb1->getQuery()->getResult() as $tmp) {
|
/** @var array{"duration": int, "rate": float, "internalRate": float, "count": int, "billable": bool, "activity": int} $tmp */
|
||||||
$activityId = $tmp['activity']->getId();
|
foreach ($qb1->getQuery()->getArrayResult() as $tmp) {
|
||||||
|
$activityId = $tmp['activity'];
|
||||||
if (!\array_key_exists($activityId, $activities)) {
|
if (!\array_key_exists($activityId, $activities)) {
|
||||||
$activity = new ActivityStatistic();
|
$activity = new ActivityStatistic();
|
||||||
$activity->setActivity($tmp['activity']);
|
|
||||||
$activities[$activityId] = $activity;
|
$activities[$activityId] = $activity;
|
||||||
} else {
|
} else {
|
||||||
$activity = $activities[$activityId];
|
$activity = $activities[$activityId];
|
||||||
@@ -423,10 +428,27 @@ class ProjectStatisticService
|
|||||||
if ($tmp['billable']) {
|
if ($tmp['billable']) {
|
||||||
$activity->setDurationBillable($activity->getDurationBillable() + $tmp['duration']);
|
$activity->setDurationBillable($activity->getDurationBillable() + $tmp['duration']);
|
||||||
$activity->setRateBillable($activity->getRateBillable() + $tmp['rate']);
|
$activity->setRateBillable($activity->getRateBillable() + $tmp['rate']);
|
||||||
|
$activity->setInternalRateBillable($activity->getInternalRateBillable() + $tmp['internalRate']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($activities as $activity) {
|
/** @var array<string, Activity> $activityIdToActivity */
|
||||||
|
$activityIdToActivity = [];
|
||||||
|
|
||||||
|
if (\count($activities) > 0) {
|
||||||
|
// prepare activities for later use
|
||||||
|
$qbActivity = $this->activityRepository->createQueryBuilder('a');
|
||||||
|
$qbActivity->select('a')->where($qbActivity->expr()->in('a.id', array_keys($activities)));
|
||||||
|
|
||||||
|
/** @var array<int, Activity> $activityResults */
|
||||||
|
$activityResults = $qbActivity->getQuery()->getResult();
|
||||||
|
foreach ($activityResults as $item) {
|
||||||
|
$activityIdToActivity[$item->getId()] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($activities as $activityId => $activity) {
|
||||||
|
$activity->setActivity($activityIdToActivity[$activityId]);
|
||||||
$model->addActivity($activity);
|
$model->addActivity($activity);
|
||||||
}
|
}
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
@@ -450,7 +472,9 @@ class ProjectStatisticService
|
|||||||
$qb2->select('u')->where($qb2->expr()->in('u.id', $userIds));
|
$qb2->select('u')->where($qb2->expr()->in('u.id', $userIds));
|
||||||
/** @var array<int, UserStatistic> $users */
|
/** @var array<int, UserStatistic> $users */
|
||||||
$users = [];
|
$users = [];
|
||||||
foreach ($qb2->getQuery()->getResult() as $user) {
|
/** @var array<int, User> $userResult */
|
||||||
|
$userResult = $qb2->getQuery()->getResult();
|
||||||
|
foreach ($userResult as $user) {
|
||||||
$users[$user->getId()] = new UserStatistic($user);
|
$users[$user->getId()] = new UserStatistic($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,41 +557,33 @@ class ProjectStatisticService
|
|||||||
$tmp->setMonth(new Month($i));
|
$tmp->setMonth(new Month($i));
|
||||||
}
|
}
|
||||||
$years[$year['year']] = $tmp;
|
$years[$year['year']] = $tmp;
|
||||||
} else {
|
|
||||||
$tmp = $years[$year['year']];
|
|
||||||
}
|
|
||||||
$tmp->setTotalRate($tmp->getTotalRate() + $year['rate']);
|
|
||||||
$tmp->setTotalInternalRate($tmp->getTotalInternalRate() + $year['internalRate']);
|
|
||||||
$tmp->setTotalDuration($tmp->getTotalDuration() + $year['duration']);
|
|
||||||
|
|
||||||
if ($year['billable']) {
|
|
||||||
$tmp->setBillableDuration($tmp->getBillableDuration() + $year['duration']);
|
|
||||||
$tmp->setBillableRate($tmp->getBillableRate() + $year['rate']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var array<string, array<string, ActivityStatistic>> $yearActivities */
|
||||||
$yearActivities = [];
|
$yearActivities = [];
|
||||||
foreach ($years as $yearName => $yearStat) {
|
foreach ($years as $yearName => $yearStat) {
|
||||||
// fetch yearly stats grouped by ACTIVITY and YEAR
|
// fetch yearly stats grouped by ACTIVITY and YEAR
|
||||||
$qb2 = clone $qb;
|
$qb2 = clone $qb;
|
||||||
$qb2
|
$qb2
|
||||||
->leftJoin(Activity::class, 'a', Join::WITH, 'a.id = t.activity')
|
->addSelect('IDENTITY(t.activity) as activity')
|
||||||
->addSelect('a as activity')
|
|
||||||
->addSelect('YEAR(t.date) as year')
|
->addSelect('YEAR(t.date) as year')
|
||||||
->andWhere('YEAR(t.date) = :year')
|
->andWhere('YEAR(t.date) = :year')
|
||||||
->setParameter('year', $yearName)
|
->setParameter('year', $yearName)
|
||||||
->addGroupBy('year')
|
->addGroupBy('year')
|
||||||
->addGroupBy('a')
|
->addGroupBy('t.activity')
|
||||||
;
|
;
|
||||||
|
|
||||||
foreach ($qb2->getQuery()->getResult() as $tmp) {
|
/** @var array<int, array{"duration": int, "rate": float, "internalRate": float, "count": int, "billable": bool, "activity": int, "year": int}> $statsTmp */
|
||||||
$activityId = $tmp['activity']->getId();
|
$statsTmp = $qb2->getQuery()->getArrayResult();
|
||||||
|
foreach ($statsTmp as $tmp) {
|
||||||
|
$activityId = $tmp['activity'];
|
||||||
if (!\array_key_exists($yearName, $yearActivities)) {
|
if (!\array_key_exists($yearName, $yearActivities)) {
|
||||||
$yearActivities[$yearName] = [];
|
$yearActivities[$yearName] = [];
|
||||||
}
|
}
|
||||||
if (!\array_key_exists($activityId, $yearActivities[$yearName])) {
|
if (!\array_key_exists($activityId, $yearActivities[$yearName])) {
|
||||||
$activity = new ActivityStatistic();
|
$activity = new ActivityStatistic();
|
||||||
$activity->setActivity($tmp['activity']);
|
$activity->setActivity($activityIdToActivity[$activityId]);
|
||||||
$yearActivities[$yearName][$activityId] = $activity;
|
$yearActivities[$yearName][$activityId] = $activity;
|
||||||
} else {
|
} else {
|
||||||
$activity = $yearActivities[$yearName][$activityId];
|
$activity = $yearActivities[$yearName][$activityId];
|
||||||
@@ -580,14 +596,14 @@ class ProjectStatisticService
|
|||||||
if ($tmp['billable']) {
|
if ($tmp['billable']) {
|
||||||
$activity->setDurationBillable($activity->getDurationBillable() + $tmp['duration']);
|
$activity->setDurationBillable($activity->getDurationBillable() + $tmp['duration']);
|
||||||
$activity->setRateBillable($activity->getRateBillable() + $tmp['rate']);
|
$activity->setRateBillable($activity->getRateBillable() + $tmp['rate']);
|
||||||
|
$activity->setInternalRateBillable($activity->getInternalRateBillable() + $tmp['internalRate']);
|
||||||
}
|
}
|
||||||
$model->addYearActivity($tmp['year'], $activity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($yearActivities as $year => $yearlyActivities) {
|
foreach ($yearActivities as $year => $yearlyActivities) {
|
||||||
foreach ($yearlyActivities as $activity) {
|
foreach ($yearlyActivities as $activityId => $activityStatistic) {
|
||||||
$model->addYearActivity($year, $activity);
|
$model->addYearActivity($year, $activityStatistic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,7 +647,7 @@ class ProjectStatisticService
|
|||||||
$user = $query->getUser();
|
$user = $query->getUser();
|
||||||
$today = clone $query->getToday();
|
$today = clone $query->getToday();
|
||||||
|
|
||||||
$qb = $this->repository->createQueryBuilder('p');
|
$qb = $this->projectRepository->createQueryBuilder('p');
|
||||||
$qb
|
$qb
|
||||||
->select('p')
|
->select('p')
|
||||||
->leftJoin('p.customer', 'c')
|
->leftJoin('p.customer', 'c')
|
||||||
@@ -675,13 +691,13 @@ class ProjectStatisticService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->repository->addPermissionCriteria($qb, $user);
|
$this->projectRepository->addPermissionCriteria($qb, $user);
|
||||||
|
|
||||||
/** @var Project[] $projects */
|
/** @var Project[] $projects */
|
||||||
$projects = $qb->getQuery()->getResult();
|
$projects = $qb->getQuery()->getResult();
|
||||||
|
|
||||||
// pre-cache customer objects instead of joining them
|
// pre-cache customer objects instead of joining them
|
||||||
$loader = new ProjectLoader($this->repository->createQueryBuilder('p')->getEntityManager(), false, false, false);
|
$loader = new ProjectLoader($this->projectRepository->createQueryBuilder('p')->getEntityManager(), false, false, false);
|
||||||
$loader->loadResults($projects);
|
$loader->loadResults($projects);
|
||||||
|
|
||||||
return $projects;
|
return $projects;
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ final class ProjectDetailsModel
|
|||||||
$users[$id] = $userStat;
|
$users[$id] = $userStat;
|
||||||
}
|
}
|
||||||
$userStat->setDuration($userStat->getDuration() + $userYear->getDuration());
|
$userStat->setDuration($userStat->getDuration() + $userYear->getDuration());
|
||||||
|
$userStat->setDurationBillable($userStat->getDurationBillable() + $userYear->getBillableDuration());
|
||||||
$userStat->setRate($userStat->getRate() + $userYear->getRate());
|
$userStat->setRate($userStat->getRate() + $userYear->getRate());
|
||||||
|
$userStat->setRateBillable($userStat->getRateBillable() + $userYear->getBillableRate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 table-responsive">
|
<div class="col-xs-12 table-responsive">
|
||||||
<table class="table table-hover dataTable">
|
<table class="table table-hover dataTable">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ 'username'|trans }}</th>
|
<th>{{ 'username'|trans }}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -152,6 +153,8 @@
|
|||||||
<th class="text-center">{{ monthName }}</th>
|
<th class="text-center">{{ monthName }}</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{% set yearTotal = 0 %}
|
{% set yearTotal = 0 %}
|
||||||
{% for userYearStat in userYearStats|sort((a, b) => b.duration <=> a.duration) %}
|
{% for userYearStat in userYearStats|sort((a, b) => b.duration <=> a.duration) %}
|
||||||
{% set user = userYearStat.user %}
|
{% set user = userYearStat.user %}
|
||||||
@@ -175,11 +178,13 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr>
|
</tbody>
|
||||||
<th></th>
|
<tfoot>
|
||||||
<th class="text-nowrap text-center total">
|
<tr class="summary">
|
||||||
|
<td></td>
|
||||||
|
<td class="text-nowrap text-center total">
|
||||||
{{ yearTotal|duration }}
|
{{ yearTotal|duration }}
|
||||||
</th>
|
</td>
|
||||||
{% for monthNumber in 1..12 %}
|
{% for monthNumber in 1..12 %}
|
||||||
{% set total = 0 %}
|
{% set total = 0 %}
|
||||||
{% for userYearStat in userYearStats %}
|
{% for userYearStat in userYearStats %}
|
||||||
@@ -188,11 +193,12 @@
|
|||||||
{% set total = total + month.duration %}
|
{% set total = total + month.duration %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<th class="text-nowrap text-center total">
|
<td class="text-nowrap text-center total">
|
||||||
{{ total|duration }}
|
{{ total|duration }}
|
||||||
</th>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,8 +217,25 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-sm-9 col-md-8 col-lg-6">
|
<div class="col-xs-12 col-sm-9 col-md-8 col-lg-6">
|
||||||
<table class="table table-hover dataTable">
|
<table class="table table-hover dataTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th class="text-nowrap text-end">{{ 'duration'|trans }}</th>
|
||||||
|
{% if view_revenue %}
|
||||||
|
<th class="text-nowrap text-end">{{ 'billable'|trans }}</th>
|
||||||
|
<th class="text-nowrap text-end">{{ 'stats.amountTotal'|trans }}</th>
|
||||||
|
{% endif %}
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% set totalRate = 0.0 %}
|
||||||
|
{% set billable = 0 %}
|
||||||
{% for stat in activities|sort((a, b) => b.duration <=> a.duration) %}
|
{% for stat in activities|sort((a, b) => b.duration <=> a.duration) %}
|
||||||
{% set dataset = dataset|merge([{'value': stat.duration, 'name': stat.name, 'color': stat.activity.color|colorize(stat.activity.name), 'duration': stat.duration|duration, 'rate': stat.rate|money(currency)}]) %}
|
{% set rate = stat.rate %}
|
||||||
|
{% set totalRate = totalRate + rate %}
|
||||||
|
{% set billable = billable + stat.rateBillable %}
|
||||||
|
{% set dataset = dataset|merge([{'value': stat.duration, 'name': stat.name, 'color': stat.activity.color|colorize(stat.activity.name), 'duration': stat.duration|duration, 'rate': rate|money(currency)}]) %}
|
||||||
{% set percentage = 0 %}
|
{% set percentage = 0 %}
|
||||||
{% if totalDuration > 0 and stat.duration > 0 %}
|
{% if totalDuration > 0 and stat.duration > 0 %}
|
||||||
{% set percentage = (100 / (totalDuration / stat.duration)) %}
|
{% set percentage = (100 / (totalDuration / stat.duration)) %}
|
||||||
@@ -221,11 +244,24 @@
|
|||||||
<td>{{ widgets.label_activity(stat.activity, {'inherit': false, 'random': true}) }}</td>
|
<td>{{ widgets.label_activity(stat.activity, {'inherit': false, 'random': true}) }}</td>
|
||||||
<td class="text-nowrap text-end">{{ stat.duration|duration }}</td>
|
<td class="text-nowrap text-end">{{ stat.duration|duration }}</td>
|
||||||
{% if view_revenue %}
|
{% if view_revenue %}
|
||||||
|
<td class="text-nowrap text-end">{{ stat.rateBillable|money(currency) }}</td>
|
||||||
<td class="text-nowrap text-end">{{ stat.rate|money(currency) }}</td>
|
<td class="text-nowrap text-end">{{ stat.rate|money(currency) }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="text-nowrap text-end">{{ percentage|number_format(1) }} %</td>
|
<td class="text-nowrap text-end">{{ percentage|number_format(1) }} %</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="summary">
|
||||||
|
<td></td>
|
||||||
|
<td class="text-nowrap text-end">{{ totalDuration|duration }}</td>
|
||||||
|
{% if view_revenue %}
|
||||||
|
<td class="text-nowrap text-end">{{ billable|money(currency) }}</td>
|
||||||
|
<td class="text-nowrap text-end">{{ totalRate|money(currency) }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-3 col-md-4 col-lg-6">
|
<div class="col-xs-12 col-sm-3 col-md-4 col-lg-6">
|
||||||
@@ -430,7 +466,20 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-sm-9 col-md-8 col-lg-6">
|
<div class="col-xs-12 col-sm-9 col-md-8 col-lg-6">
|
||||||
<table class="table table-hover dataTable">
|
<table class="table table-hover dataTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th class="text-nowrap text-end">{{ 'duration'|trans }}</th>
|
||||||
|
{% if view_revenue %}
|
||||||
|
<th class="text-nowrap text-end">{{ 'billable'|trans }}</th>
|
||||||
|
<th class="text-nowrap text-end">{{ 'stats.amountTotal'|trans }}</th>
|
||||||
|
{% endif %}
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{% set rateTotal = 0 %}
|
{% set rateTotal = 0 %}
|
||||||
|
{% set rateTotalBillable = 0 %}
|
||||||
{% set totalDuration = 0 %}
|
{% set totalDuration = 0 %}
|
||||||
{% set datasets = [] %}
|
{% set datasets = [] %}
|
||||||
{% set labels = [] %}
|
{% set labels = [] %}
|
||||||
@@ -438,6 +487,7 @@
|
|||||||
{% set user = userStat.user %}
|
{% set user = userStat.user %}
|
||||||
{% set color = user.color|colorize(user.displayName) %}
|
{% set color = user.color|colorize(user.displayName) %}
|
||||||
{% set rateTotal = rateTotal + userStat.rate %}
|
{% set rateTotal = rateTotal + userStat.rate %}
|
||||||
|
{% set rateTotalBillable = rateTotalBillable + userStat.rateBillable %}
|
||||||
{% set totalDuration = totalDuration + userStat.duration %}
|
{% set totalDuration = totalDuration + userStat.duration %}
|
||||||
{% set datasets = datasets|merge([{'name': user.displayName, 'duration': userStat.duration|duration, 'value': userStat.duration, 'color': color, 'rate': userStat.rate|money(currency)}]) %}
|
{% set datasets = datasets|merge([{'name': user.displayName, 'duration': userStat.duration|duration, 'value': userStat.duration, 'color': color, 'rate': userStat.rate|money(currency)}]) %}
|
||||||
{% set percentage = 0 %}
|
{% set percentage = 0 %}
|
||||||
@@ -452,6 +502,9 @@
|
|||||||
{{ userStat.duration|duration }}
|
{{ userStat.duration|duration }}
|
||||||
</td>
|
</td>
|
||||||
{% if view_revenue %}
|
{% if view_revenue %}
|
||||||
|
<td class="text-nowrap text-end">
|
||||||
|
{{ userStat.rateBillable|money(currency) }}
|
||||||
|
</td>
|
||||||
<td class="text-nowrap text-end">
|
<td class="text-nowrap text-end">
|
||||||
{{ userStat.rate|money(currency) }}
|
{{ userStat.rate|money(currency) }}
|
||||||
</td>
|
</td>
|
||||||
@@ -461,14 +514,18 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr>
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="summary">
|
||||||
<td></td>
|
<td></td>
|
||||||
<th class="text-nowrap text-end total">{{ totalDuration|duration }}</th>
|
<td class="text-nowrap text-end total">{{ totalDuration|duration }}</td>
|
||||||
{% if view_revenue %}
|
{% if view_revenue %}
|
||||||
<th class="text-nowrap text-end total">{{ rateTotal|money(currency) }}</th>
|
<td class="text-nowrap text-end total">{{ rateTotalBillable|money(currency) }}</td>
|
||||||
|
<td class="text-nowrap text-end total">{{ rateTotal|money(currency) }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-3 col-md-4 col-lg-6">
|
<div class="col-xs-12 col-sm-3 col-md-4 col-lg-6">
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ class ConfigurationTest extends TestCase
|
|||||||
'allow_overbooking_budget' => true,
|
'allow_overbooking_budget' => true,
|
||||||
'lockdown_period_timezone' => null,
|
'lockdown_period_timezone' => null,
|
||||||
'break_warning_duration' => 0,
|
'break_warning_duration' => 0,
|
||||||
'long_running_duration' => 480,
|
'long_running_duration' => 600,
|
||||||
'require_activity' => true,
|
'require_activity' => true,
|
||||||
],
|
],
|
||||||
'duration_increment' => 15,
|
'duration_increment' => 15,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class InvoiceItemDefaultHydratorTest extends TestCase
|
|||||||
'entry.total_plain',
|
'entry.total_plain',
|
||||||
'entry.currency',
|
'entry.currency',
|
||||||
'entry.duration',
|
'entry.duration',
|
||||||
|
'entry.duration_format',
|
||||||
'entry.duration_decimal',
|
'entry.duration_decimal',
|
||||||
'entry.duration_minutes',
|
'entry.duration_minutes',
|
||||||
'entry.begin',
|
'entry.begin',
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ class DebugRendererTest extends TestCase
|
|||||||
'entry.total_plain',
|
'entry.total_plain',
|
||||||
'entry.currency',
|
'entry.currency',
|
||||||
'entry.duration',
|
'entry.duration',
|
||||||
|
'entry.duration_format',
|
||||||
'entry.duration_decimal',
|
'entry.duration_decimal',
|
||||||
'entry.duration_minutes',
|
'entry.duration_minutes',
|
||||||
'entry.begin',
|
'entry.begin',
|
||||||
|
|||||||
@@ -11,36 +11,51 @@ namespace App\Tests\Model\Statistic;
|
|||||||
|
|
||||||
use App\Model\Statistic\Month;
|
use App\Model\Statistic\Month;
|
||||||
use App\Model\Statistic\Year;
|
use App\Model\Statistic\Year;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \App\Model\Statistic\Year
|
* @covers \App\Model\Statistic\Year
|
||||||
*/
|
*/
|
||||||
class YearTest extends AbstractTimesheetTest
|
class YearTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testDefaultValues()
|
public function testDefaultValues(): void
|
||||||
{
|
{
|
||||||
$sut = new Year('1999');
|
$sut = new Year('1999');
|
||||||
$this->assertDefaultValues($sut);
|
|
||||||
|
|
||||||
self::assertNull($sut->getMonth(1));
|
self::assertNull($sut->getMonth(1));
|
||||||
self::assertEmpty($sut->getMonths());
|
self::assertEmpty($sut->getMonths());
|
||||||
self::assertIsArray($sut->getMonths());
|
self::assertIsArray($sut->getMonths());
|
||||||
self::assertEquals('1999', $sut->getYear());
|
self::assertEquals('1999', $sut->getYear());
|
||||||
self::assertSame(0, $sut->getBillableDuration());
|
self::assertSame(0.0, $sut->getRate());
|
||||||
self::assertSame(0.0, $sut->getBillableRate());
|
self::assertSame(0.0, $sut->getBillableRate());
|
||||||
|
self::assertSame(0, $sut->getDuration());
|
||||||
|
self::assertSame(0, $sut->getBillableDuration());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetter()
|
public function testSetter(): void
|
||||||
{
|
{
|
||||||
$sut = new Year('1999');
|
$sut = new Year('1999');
|
||||||
$this->assertSetter($sut);
|
|
||||||
|
|
||||||
$sut->setMonth(new Month('01'));
|
$january = new Month('01');
|
||||||
$sut->setMonth(new Month('02'));
|
$january->setBillableDuration(1234);
|
||||||
$sut->setMonth(new Month('03'));
|
$january->setBillableRate(4321.12);
|
||||||
|
$january->setTotalDuration(2345);
|
||||||
|
$january->setTotalRate(5555.55);
|
||||||
|
|
||||||
|
$february = new Month('02');
|
||||||
|
$february->setBillableDuration(1234);
|
||||||
|
$february->setBillableRate(4321.12);
|
||||||
|
|
||||||
|
$march = new Month('03');
|
||||||
|
$march->setBillableDuration(1);
|
||||||
|
$march->setBillableRate(1.1);
|
||||||
|
|
||||||
|
$sut->setMonth($january);
|
||||||
|
$sut->setMonth($february);
|
||||||
|
$sut->setMonth($march);
|
||||||
self::assertEquals(3, \count($sut->getMonths()));
|
self::assertEquals(3, \count($sut->getMonths()));
|
||||||
|
|
||||||
$sut->setMonth(new Month('01'));
|
$sut->setMonth(new Month('02'));
|
||||||
|
|
||||||
self::assertEquals(3, \count($sut->getMonths()));
|
self::assertEquals(3, \count($sut->getMonths()));
|
||||||
|
|
||||||
@@ -49,9 +64,9 @@ class YearTest extends AbstractTimesheetTest
|
|||||||
self::assertInstanceOf(Month::class, $sut->getMonth(3));
|
self::assertInstanceOf(Month::class, $sut->getMonth(3));
|
||||||
self::assertNull($sut->getMonth(4));
|
self::assertNull($sut->getMonth(4));
|
||||||
|
|
||||||
$sut->setBillableDuration(123456);
|
self::assertSame(1235, $sut->getBillableDuration());
|
||||||
$sut->setBillableRate(123.456789);
|
self::assertSame(2345, $sut->getDuration());
|
||||||
self::assertSame(123456, $sut->getBillableDuration());
|
self::assertSame(4322.22, $sut->getBillableRate());
|
||||||
self::assertSame(123.456789, $sut->getBillableRate());
|
self::assertSame(5555.55, $sut->getRate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7917,16 +7917,6 @@ parameters:
|
|||||||
count: 1
|
count: 1
|
||||||
path: Model/Statistic/TimesheetTest.php
|
path: Model/Statistic/TimesheetTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Model\\\\Statistic\\\\YearTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Model/Statistic/YearTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method App\\\\Tests\\\\Model\\\\Statistic\\\\YearTest\\:\\:testSetter\\(\\) has no return type specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: Model/Statistic/YearTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method App\\\\Tests\\\\Model\\\\TimesheetCountedStatisticTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
message: "#^Method App\\\\Tests\\\\Model\\\\TimesheetCountedStatisticTest\\:\\:testDefaultValues\\(\\) has no return type specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<!-- Reports -->
|
<!-- Reports -->
|
||||||
<trans-unit id="tLV1hoP" resname="report_tasks_assigned">
|
<trans-unit id="tLV1hoP" resname="report_tasks_assigned">
|
||||||
<source>report_tasks_assigned</source>
|
<source>report_tasks_assigned</source>
|
||||||
<target>Meine Aufgaben</target>
|
<target>Aufgaben pro Benutzer</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8c0YWZm" resname="report_tasks_teams">
|
<trans-unit id="8c0YWZm" resname="report_tasks_teams">
|
||||||
<source>report_tasks_teams</source>
|
<source>report_tasks_teams</source>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<!-- Reports -->
|
<!-- Reports -->
|
||||||
<trans-unit id="tLV1hoP" resname="report_tasks_assigned">
|
<trans-unit id="tLV1hoP" resname="report_tasks_assigned">
|
||||||
<source>report_tasks_assigned</source>
|
<source>report_tasks_assigned</source>
|
||||||
<target>My tasks</target>
|
<target>Tasks per user</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8c0YWZm" resname="report_tasks_teams">
|
<trans-unit id="8c0YWZm" resname="report_tasks_teams">
|
||||||
<source>report_tasks_teams</source>
|
<source>report_tasks_teams</source>
|
||||||
|
|||||||
Reference in New Issue
Block a user