Add option for custom work and expenses.

master
Lio Novelli 2022-04-30 14:55:18 +02:00
parent df662c3eb9
commit 9f8c1616f5
15 changed files with 856 additions and 159 deletions

View File

@ -47,6 +47,10 @@
- Create an invoice
- send the invoice (to Wolfgang)
*** ValueObjects
** Motivation
- practice php
- automatization of repetitive task
@ -58,6 +62,17 @@
** Plan
*** most current
1. For version 0.6.7
- data value objects
- phar file
- additional expenses
2. For version 1.0
- plugin system
- for time tracking services
- for reports
- symfony/config & symfony/di components
*** current
1. For Version 1.0
@ -94,6 +109,7 @@
2. configuration wizard
** Learning
- https://www.youtube.com/watch?v=aCqM9YnjTe0
- Choices (~new ChoiceQuestion~)

View File

@ -2,7 +2,7 @@
"name": "lio/rprt-cli",
"description": "Automate invoicing from youtrack service.",
"type": "project",
"keywords": ["cli", "report"],
"keywords": ["cli", "report", "youtrack"],
"readme": "README.org",
"time": "2021-04-04",
"license": "GPL-3.0-or-later",

404
app/composer.lock generated
View File

@ -722,20 +722,20 @@
},
{
"name": "psr/container",
"version": "1.1.1",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
"reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
"url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea",
"reference": "513e0666f7216c7459170d56df27dfcefe1689ea",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
"php": ">=7.4.0"
},
"type": "library",
"autoload": {
@ -762,7 +762,11 @@
"container-interop",
"psr"
],
"time": "2021-03-05T17:36:06+00:00"
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/1.1.2"
},
"time": "2021-11-05T16:50:12+00:00"
},
{
"name": "psr/event-dispatcher",
@ -1064,27 +1068,29 @@
},
{
"name": "symfony/console",
"version": "v5.2.6",
"version": "v5.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d"
"reference": "ffe3aed36c4d60da2cf1b0a1cee6b8f2e5fa881b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d",
"reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d",
"url": "https://api.github.com/repos/symfony/console/zipball/ffe3aed36c4d60da2cf1b0a1cee6b8f2e5fa881b",
"reference": "ffe3aed36c4d60da2cf1b0a1cee6b8f2e5fa881b",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php73": "^1.8",
"symfony/polyfill-php80": "^1.15",
"symfony/service-contracts": "^1.1|^2",
"symfony/string": "^5.1"
"symfony/polyfill-php73": "^1.9",
"symfony/polyfill-php80": "^1.16",
"symfony/service-contracts": "^1.1|^2|^3",
"symfony/string": "^5.1|^6.0"
},
"conflict": {
"psr/log": ">=3",
"symfony/dependency-injection": "<4.4",
"symfony/dotenv": "<5.1",
"symfony/event-dispatcher": "<4.4",
@ -1092,16 +1098,16 @@
"symfony/process": "<4.4"
},
"provide": {
"psr/log-implementation": "1.0"
"psr/log-implementation": "1.0|2.0"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/config": "^4.4|^5.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/event-dispatcher": "^4.4|^5.0",
"symfony/lock": "^4.4|^5.0",
"symfony/process": "^4.4|^5.0",
"symfony/var-dumper": "^4.4|^5.0"
"psr/log": "^1|^2",
"symfony/config": "^4.4|^5.0|^6.0",
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
"symfony/event-dispatcher": "^4.4|^5.0|^6.0",
"symfony/lock": "^4.4|^5.0|^6.0",
"symfony/process": "^4.4|^5.0|^6.0",
"symfony/var-dumper": "^4.4|^5.0|^6.0"
},
"suggest": {
"psr/log": "For using the console logger",
@ -1140,20 +1146,37 @@
"console",
"terminal"
],
"time": "2021-03-28T09:42:18+00:00"
"support": {
"source": "https://github.com/symfony/console/tree/v5.4.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-04-12T16:02:29+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v2.2.0",
"version": "v2.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665"
"reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665",
"reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
"reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
"shasum": ""
},
"require": {
@ -1162,7 +1185,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
"dev-main": "2.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -1190,7 +1213,24 @@
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"time": "2020-09-07T11:33:47+00:00"
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-01-02T09:53:40+00:00"
},
{
"name": "symfony/event-dispatcher",
@ -1581,28 +1621,31 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.22.1",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "c6c942b1ac76c82448322025e084cadc56048b4e"
"reference": "30885182c981ab175d4d034db0f6f469898070ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e",
"reference": "c6c942b1ac76c82448322025e084cadc56048b4e",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
"reference": "30885182c981ab175d4d034db0f6f469898070ab",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1610,12 +1653,12 @@
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
},
"files": [
"bootstrap.php"
]
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@ -1639,20 +1682,37 @@
"polyfill",
"portable"
],
"time": "2021-01-07T16:49:33+00:00"
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-10-20T20:35:02+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.22.1",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170"
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170",
"reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783",
"shasum": ""
},
"require": {
@ -1664,7 +1724,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1672,12 +1732,12 @@
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
},
"files": [
"bootstrap.php"
]
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Grapheme\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@ -1703,7 +1763,24 @@
"portable",
"shim"
],
"time": "2021-01-22T09:19:47+00:00"
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-11-23T21:10:46+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
@ -1794,16 +1871,16 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.22.1",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248"
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248",
"reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8",
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8",
"shasum": ""
},
"require": {
@ -1815,7 +1892,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1823,12 +1900,12 @@
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Intl\\Normalizer\\": ""
},
"classmap": [
"Resources/stubs"
]
@ -1857,32 +1934,52 @@
"portable",
"shim"
],
"time": "2021-01-22T09:19:47+00:00"
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.22.1",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "5232de97ee3b75b0360528dae24e73db49566ab1"
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1",
"reference": "5232de97ee3b75b0360528dae24e73db49566ab1",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1890,12 +1987,12 @@
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@ -1920,7 +2017,24 @@
"portable",
"shim"
],
"time": "2021-01-22T09:19:47+00:00"
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-11-30T18:21:41+00:00"
},
{
"name": "symfony/polyfill-php72",
@ -2000,16 +2114,16 @@
},
{
"name": "symfony/polyfill-php73",
"version": "v1.22.1",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
"reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2"
"reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2",
"reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2",
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5",
"reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5",
"shasum": ""
},
"require": {
@ -2018,7 +2132,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -2026,12 +2140,12 @@
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php73\\": ""
},
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php73\\": ""
},
"classmap": [
"Resources/stubs"
]
@ -2058,20 +2172,37 @@
"portable",
"shim"
],
"time": "2021-01-07T16:49:33+00:00"
"support": {
"source": "https://github.com/symfony/polyfill-php73/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-06-05T21:20:04+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.22.1",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91"
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91",
"reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c",
"shasum": ""
},
"require": {
@ -2080,7 +2211,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -2088,12 +2219,12 @@
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
@ -2124,25 +2255,46 @@
"portable",
"shim"
],
"time": "2021-01-07T16:49:33+00:00"
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-03-04T08:16:47+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v2.2.0",
"version": "v2.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1"
"reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1",
"reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c",
"reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/container": "^1.0"
"psr/container": "^1.1",
"symfony/deprecation-contracts": "^2.1|^3"
},
"conflict": {
"ext-psr": "<1.1|>=2"
},
"suggest": {
"symfony/service-implementation": ""
@ -2150,7 +2302,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
"dev-main": "2.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@ -2186,44 +2338,63 @@
"interoperability",
"standards"
],
"time": "2020-09-07T11:33:47+00:00"
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v2.5.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-03-13T20:07:29+00:00"
},
{
"name": "symfony/string",
"version": "v5.2.6",
"version": "v6.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572"
"reference": "ac0aa5c2282e0de624c175b68d13f2c8f2e2649d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572",
"reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572",
"url": "https://api.github.com/repos/symfony/string/zipball/ac0aa5c2282e0de624c175b68d13f2c8f2e2649d",
"reference": "ac0aa5c2282e0de624c175b68d13f2c8f2e2649d",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"php": ">=8.0.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "~1.15"
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"symfony/translation-contracts": "<2.0"
},
"require-dev": {
"symfony/error-handler": "^4.4|^5.0",
"symfony/http-client": "^4.4|^5.0",
"symfony/translation-contracts": "^1.1|^2",
"symfony/var-exporter": "^4.4|^5.0"
"symfony/error-handler": "^5.4|^6.0",
"symfony/http-client": "^5.4|^6.0",
"symfony/translation-contracts": "^2.0|^3.0",
"symfony/var-exporter": "^5.4|^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\String\\": ""
},
"files": [
"Resources/functions.php"
],
"psr-4": {
"Symfony\\Component\\String\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
@ -2252,7 +2423,24 @@
"utf-8",
"utf8"
],
"time": "2021-03-17T17:12:15+00:00"
"support": {
"source": "https://github.com/symfony/string/tree/v6.0.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2022-04-22T08:18:02+00:00"
},
{
"name": "symfony/yaml",

View File

@ -23,11 +23,6 @@
border-left: 0.1mm solid #000000;
border-right: 0.1mm solid #000000;
}
table thead th {
background-color: #EEEEEE;
text-align: center;
border: 0.1mm solid #000000;
}
table tbody td {
background-color: #FFF;
border: 0.1mm solid #000000;
@ -35,9 +30,26 @@
padding-left: 2mm;
}
tbody td.td-3,
tbody td.td-1 {
tbody td.td-1,
.right {
text-align: right;
}
/*tbody td.td-2,*/
.center {
text-align: center;
}
table thead th,
table tr.bold td,
table tr.bold td.td-3 {
background-color: #EEEEEE;
border: 0.1mm solid #000000;
font-weight: bold;
}
table tr.center td,
table tr.center td.td-3 {
text-align: center!important;
}
/*
tbody tr.tr-2 td,
tbody tr.tr-3 td {
background-color: #EEE;
@ -46,32 +58,30 @@
tbody tr.tr-3 td {
font-weight: bold;
}
tbody td.td-2,
.center {
text-align: center;
}
.right {
text-align: right;
*/
.left {
text-align: left;
}
</style>
</head>
<body>
<p class="center">My company name</br>My company address</br>123456789, mycompany@email.com</br>987654321</p>
<p></p>
<p>Other company</br>Other company address</br>1111 City</br>State</p>
<p></p>
<p></p>
<p></p>
<p class="right">City, on [[today]]</p>
<p><strong>Honorarnote</br>Nummer [[number]]</strong></p>
<p></p>
<p>Für meine Tätigkeit Programmieren von [[date_start]] bis [[date_end]] erlaube ich mir, folgenden Betrag in Rechnung zu stellen:</p>
[[table]]
<p>Ich ersuche Sie höflich, den oben angeführt auf meine Kontonummer [[account_number]] mit der Bankleitzahl ABCDSI33 zu überweisen.</p>
<p>Vielen Dank für den Auftrag,</br>
<p class="center">Spletni razvoj, Liopold Doron Novelli s.p.,<br>Prešernova 23, 1236 Trzin<br>8638314000, liopold@drunomics.com<br>25752910</p>
<p><br><br></p>
<p>drunomics GmbH<br>Getriedmarkt 11/12<br>1060 Wien<br>Austria</p>
<p><br></p>
<p><br></p>
<p><br></p>
<p class="right">Laibach, am [[today]]</p>
<p><strong>Honorarnote<br>Nummer [[number]]</strong></p>
<p><br></p>
<p>Für meine Tätigkeit Programmieren von [[date_start]] bis [[date_end]] erlaube ich mir, folgenden Betrag in Rechnung zu stellen:<br></p>
<p><br></p>
<div class="table">[[table]]</div>
<p><br>Ich ersuche Sie höflich, den oben angeführt auf meine Kontonummer SI56 6100 0002 3993 491 mit der Bankleitzahl HDELSI22 zu überweisen.<br></p>
<p><br>Vielen Dank für den Auftrag,<br>
mit besten Grüßen,</p>
<p></p>
<p></p>
<p>My name</p>
<p><br><br></p>
<p><br><br></p>
<p>Liopold Doron Novelli</p>
</body>
</html>

View File

@ -8,6 +8,8 @@ use RprtCli\Utils\Configuration\ConfigurationInterface;
use RprtCli\Utils\Configuration\ConfigurationService;
use RprtCli\Utils\CsvReport\CsvReport;
use RprtCli\Utils\CsvReport\CsvReportInterface;
use RprtCli\Utils\CsvReport\ReportCsv;
use RprtCli\Utils\CsvReport\ReportCsvInterface;
use GuzzleHttp\Client;
use Mpdf\Mpdf;
use RprtCli\Utils\Mailer\MailerInterface;
@ -24,8 +26,6 @@ return [
'config.file' => 'rprt.config.yml',
'config.path' => '~/.config/rprt-cli/',
'default_locale' => 'en',
// 'translator' => ['default_path' => '%kernel.project_dir%/translations'],
// 'guzzle' => create()->constructor(Client::class),
'guzzle' => get(Client::class),
'mpdf' => get(Mpdf::class),
ConfigurationInterface::class => get(ConfigurationService::class),
@ -49,11 +49,11 @@ return [
// 'locale' => get('config.service')->method('get', 'en'),
// Translator::class => create()->constructor('sl')->method('addLoader', 'po', new PoFileLoader),
// 'translator' => get(Translator::class),
CsvReportInterface::class => get(CsvReport::class),
CsvReport::class => create()->constructor(
ReportCsvInterface::class => get(ReportCsv::class),
ReportCsv::class => create()->constructor(
get('config.service')
),
'csv.report' => get(CsvReportInterface::class),
'csv.report' => get(ReportCsvInterface::class),
MailerInterface::class => get(MailerService::class),
MailerService::class => create()->constructor(
get('config.service'),

View File

@ -7,13 +7,16 @@ declare(strict_types=1);
namespace RprtCli\Commands;
use RprtCli\Utils\Configuration\ConfigurationInterface;
use RprtCli\Utils\CsvReport\CsvReportInterface;
use RprtCli\Utils\CsvReport\ReportCsvInterface;
use RprtCli\Utils\Mailer\MailerInterface;
use RprtCli\Utils\PdfExport\PdfExportInterface;
use RprtCli\Utils\TimeTrackingServices\YoutrackInterface;
use RprtCli\ValueObjects\Expenses;
use RprtCli\ValueObjects\WorkInvoiceElement;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@ -34,8 +37,11 @@ class RprtCommand extends Command
protected $pdfExport;
const TYPE_WORK = 1;
const TYPE_EXPENSE = 2;
public function __construct(
CsvReportInterface $csv,
ReportCsvInterface $csv,
ConfigurationInterface $configuration,
YoutrackInterface $youtrack,
PdfExportInterface $pdf_export,
@ -81,7 +87,7 @@ class RprtCommand extends Command
'test',
't',
InputOption::VALUE_NONE,
'Test login into youtrack service.'
'Test login into youtrack service. Prints out your name.'
);
$this->addOption(
'output',
@ -101,6 +107,20 @@ class RprtCommand extends Command
InputOption::VALUE_REQUIRED,
'Comma separated list of recipients that should get the exported pdf.'
);
$this->addOption(
'expenses',
'e',
InputOption::VALUE_OPTIONAL,
'List of additional expenses in format expense1=value1;expenses2=value2... or empty for interactive output.',
FALSE
);
$this->addOption(
'custom',
'c',
InputOption::VALUE_OPTIONAL,
'Additional custom work untracked in format: name1=time1;name2=time2... Project to assign work items to has to be configured in app config. Leave empty for interactive output.',
FALSE
);
}
protected function execute(InputInterface $input, OutputInterface $output) : int
@ -113,25 +133,38 @@ class RprtCommand extends Command
$report_id = $this->youtrack->getReportId();
$file = $this->youtrack->downloadReport($report_id);
}
if ($input->hasParameterOption('expenses') || $input->hasParameterOption('-e')) {
$expenses = $this->getExpenses($input->getOption('expenses'));
}
if ($input->hasParameterOption('custom') || $input->hasParameterOption('-c')) {
$custom = $this->getCustomWork($input->getOption('custom'));
}
if ($youtrack || $file = $input->getOption('file')) {
// Youtrack can also provide a file name.
var_dump($file);
$data = $this->csv->getReportData($file);
$table = $this->generateTable($output, $data);
if (!empty($expenses)) {
$data = array_merge($data, $expenses);
}
// $table = $this->generateTable($output, $data);
$table = $this->getTable($output, $data);
$table->render();
if ($pdf = $input->getOption('pdf')) {
if ($input->getOption('pdf')) {
$nice_data = $this->csv->arangeDataForDefaultPdfExport($data);
// @TODO method gatherTokens();
if ($output = $input->getOption('output')) {
$this->pdfExport->setOutput($output);
if ($out = $input->getOption('output')) {
$this->pdfExport->setOutput($out);
}
$output_path = $this->pdfExport->fromDefaultDataToPdf($nice_data);
// Notify the user where the file was generated to.
$output->writeln("The file was generated at ${output_path}.");
}
// return Command::SUCCESS;
}
if ($send = $input->getOption('send') && $output_path) {
if ($input->getOption('send') && $output_path) {
// @TODO If no output path print an error.
// Send email to configured address.
if ($recipients = $input->getOption('send-to')) {
$this->mailer->setRecipients(explode(',', $recipients));
@ -143,17 +176,35 @@ class RprtCommand extends Command
return Command::SUCCESS;
}
protected function getTable(OutputInterface $output, array $data) :Table {
$rows = $this->csv->generateTable($data);
$table = new Table($output);
$table->setHeaders([
'Project', 'Hours', 'Rate', 'Price',
]);
foreach ($rows as $key => $row) {
if (!$row) {
$rows[$key] = new TableSeparator();
}
elseif (is_array($row) && is_null($row[1]) && is_null($row[0])) {
// Check which elements in array are null.
$rows[$key] = [new TableCell($row[2], ['colspan' => 3]), $row[3]];
}
}
$table->setRows($rows);
return $table;
}
/**
* Create table from data that is already inline with configuration.
*
* @deprecated
* This method was almost exact copy of CsvReport::arangeDataForDefaultPdfExport
*/
protected function generateTable(OutputInterface $output, array $data) : Table
{
$table = new Table($output);
$table->setHeaders([
// $this->translator->trans('Project', [], 'messages', 'sl_SI'),
// $this->translator->trans('Hours', [], 'messages', 'sl_SI'),
// $this->translator->trans('Rate'),
// $this->translator->trans('Price'),
'Project', 'Hours', 'Rate', 'Price',
]);
[$rows, $totalHours, $totalPrice] = [[], 0, 0];
@ -168,7 +219,7 @@ class RprtCommand extends Command
if ($config['time_format'] === 'm') {
$hours /= 60;
}
$price = $hours * (int) $config['price'];
$price = $hours * (float) $config['price'];
$row = [
$config['name'],
$hours,
@ -178,7 +229,15 @@ class RprtCommand extends Command
$rows[] = $row;
$totalHours += $hours;
$totalPrice += $price;
unset($data[$name]);
}
if (!empty($data)) {
foreach ($data as $name => $value) {
if (strpos(strtolower($name), 'expanses') !== FALSE) {
}
}
}
$rows[] = new TableSeparator();
// @TODO Check rate in final result.
// $rows[] = [$this->translator->trans('Sum'), $totalHours, $config['price'], $totalPrice];
@ -206,4 +265,71 @@ class RprtCommand extends Command
// $table->setStyle('borderless');
$table->render();
}
/**
* Gets the expenses array.
*
* @return Expenses[]
*/
protected function getExpenses($expenses) {
$output = [];
if (is_string($expenses)) {
foreach (explode(';', $expenses) as $expense) {
[$name, $value] = explode('=', $expense);
$output[] = new Expenses($name, (float) $value);
}
}
else {
$continue = TRUE;
while ($continue) {
$name = readline('Enter expenses name or leave empty to stop: ');
$value = (float) readline('Enter expenses value: ');
if (!empty($name)) {
$output[] = new Expenses($name, $value);
}
else {
$continue = FALSE;
}
}
}
return $output;
}
protected function getCustomWorkOrExpenses($custom, $type) {
$output = [];
if (is_string($custom)) {
foreach (explode(';', $custom) as $item) {
[$name, $value] = explode('=', $item);
$output[] = $this->createInvoiceElement($name, (float) $value, $type);
}
} else {
$continue = TRUE;
if ($type == self::TYPE_WORK) {
$message_name = 'Enter project name or leave empty to stop: ';
$message_value = 'Enter time spent of project: ';
} elseif ($type == self::TYPE_EXPENSE) {
$message_name = 'Enter expenses name or leave empty to stop: ';
$message_value = 'Enter expenses value: ';
}
while ($continue) {
$name = readline($message_name);
$value = (float) readline($message_value);
if (!empty($name)) {
$output[] = $this->createInvoiceElement($name, $value, $type);
} else {
$continue = FALSE;
}
}
}
return $output;
}
protected function createInvoiceElement(string $name, float $value, int $type) {
if ($type == self::TYPE_WORK) {
return new WorkInvoiceElement($name, (float) $value);
} elseif ($type == self::TYPE_EXPENSE) {
return new Expenses($name, (float) $value);
}
throw new \Exception('Unkown invoice element type.');
}
}

View File

@ -15,6 +15,9 @@ use function reset;
/**
* Creates a report of projects and hours.
*
* @deprecated
* Use ReportCsv class instead.
*/
class CsvReport implements CsvReportInterface
{
@ -90,7 +93,7 @@ class CsvReport implements CsvReportInterface
}
$hours = $data[$name];
if ($config['time_format'] === 'm') {
$hours /= 60;
$hours /= 60.0;
}
$price = $hours * (int) $config['price'];
$row = [

View File

@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\CsvReport;
use RprtCli\Utils\Configuration\ConfigurationInterface;
use RprtCli\ValueObjects\ExpensesInterface;
use RprtCli\ValueObjects\WorkInvoiceElement;
use RprtCli\ValueObjects\WorkInvoiceElementInterface;
use function array_key_first;
use function array_keys;
use function fgetcsv;
use function fopen;
use function preg_match;
use function reset;
/**
* Creates a report of projects and hours.
*
* Uses value objects instead of arrays.
*/
class ReportCsv implements ReportCsvInterface
{
/**
* A configuration service.
*
* @var ConfigurationInterface
*/
protected $configurationService;
public function __construct(ConfigurationInterface $config)
{
$this->configurationService = $config;
}
/**
* {@inheritdoc}
*/
public function getReportData(string $filePath) : array
{
$output = [];
// @TODO replace with config service.
// $config = $this->dummyConfig()['projects'];
$config = $this->configurationService->get('projects');
foreach (array_keys($config) as $key) {
$output[$key] = 0;
}
if ($file = fopen($filePath, 'r')) {
while (($line = fgetcsv($file)) !== false) {
$parsed = $this->parseCsvFile($line);
// $key = reset(array_keys($parsed));
$key = array_key_first($parsed);
if (isset($output[$key])) {
$output[$key] += (float) reset($parsed);
}
}
}
$report_data = [];
foreach ($output as $project => $hours) {
$report_data[] = new WorkInvoiceElement($project, $hours);
}
return $report_data;
}
/**
* Get correct values from the raw data lines of csv.
*
* @param array $rawData
* Columns with data are specified in config.
*
* @return array
* Project key and unit of time spent.
*/
protected function parseCsvFile(array $rawData) : array
{
$config = $this->configurationService->get('projects');
foreach ($config as $key => $project) {
if (preg_match('/' . $project['pattern'] . '/', $rawData[1])) {
return [$key => $rawData[4]];
}
}
return [];
}
/**
* Input is array of Work elements and expenses.
*/
public function generateTable(array $data): array {
[$rows, $totalHours, $totalPrice, $add_separator] = [[], 0, 0, FALSE];
$projectsConfig = $this->configurationService->get('projects');
// $header = $this->configurationService->get('export.labels', null);
$header = null;
if (is_array($header)) {
$rows[] = $header;
}
// First only list work invoice elements.
foreach ($data as $key => $invoice_element) {
if ($invoice_element instanceof WorkInvoiceElementInterface) {
$add_separator = TRUE;
$project = $invoice_element->getName();
$time = $invoice_element->getTime();
$config = $projectsConfig[$project];
$hours = $config['time_format'] == 'm' ? $time/60 : $time;
$price = $hours * (float) $config['price'];
$row = [
$config['label'] ?? $project,
number_format($hours, 2, ',', '.'),
number_format($config['price'], 2, ',', '.'),
number_format($price, 2, ',', '.'),
];
$totalHours += $hours;
$totalPrice += $price;
$rows[] = $row;
unset($data[$key]);
}
}
if ($add_separator) {
$rows[] = null;
$rows[] = ['Gesamt netto', number_format($totalHours, 2, ',', '.'), ' ', number_format($totalPrice, 2, ',', '.')];
$add_separator = FALSE;
}
foreach ($data as $invoice_element) {
if ($invoice_element instanceof ExpensesInterface) {
if (!isset($added_expenses)) {
// Make next line bold and centered.
$rows[] = 0;
$rows[] = [
null,
null,
'Kosten',
'EUR',
];
// Don't make next line bold. See RprtCli\PdfExport\PdfExportService::parsedDataToHtml.
$rows[] = FALSE;
$added_expenses = TRUE;
}
$add_separator = TRUE;
$rows[] = [
null,
null,
$invoice_element->getName(),
number_format($invoice_element->getValue(), 2, ',', '.'),
];
$totalPrice += $invoice_element->getValue();
}
}
if ($add_separator) {
$rows[] = null;
}
$rows[] = [null, null, 'Gessamt brutto', number_format($totalPrice, 2, ',', '.')];
return $rows;
}
/**
* {@inheritdoc}
*/
public function arangeDataForDefaultPdfExport(array $data) :array {
$rows = $this->generateTable($data);
$header = $this->configurationService->get('export.labels', null);
array_unshift($rows, $header);
foreach ($rows as $key => $row) {
if (!$row) {
unset($data[$key]);
}
}
return $rows;
}
/**
* Should be moved into test class.
*/
protected function dummyConfig() : array
{
return [
'projects' => [
'LDP' => [
'name' => 'lupus.digital',
'pattern' => 'LDP-[0-9]+',
'price' => 25,
// optional specify columns
],
'WV' => [
'name' => 'Wirtschaftsverlag',
'pattern' => 'WV-[0-9]+',
'price' => 25,
// optional specify columns
],
'Other' => [
'name' => 'Other projects',
'pattern' => '(?!.\bLDP\b)(?!.\bWV\b)',
'price' => 25,
// optional specify columns
],
],
];
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\CsvReport;
/**
* Handles creating report data from csv file downloaded from youtrack service.
*/
interface ReportCsvInterface
{
/**
* Returns array of hours per configured projects.
*
* @todo - get data from variable.
*
*
* Project key as key and number of hours as value.
*/
public function getReportData(string $filePath): array;
/**
* Returns array of rows created from array of InvoiceElements.
*
* If row is null, it is meant to be a table separator.
*/
public function generateTable(array $data) :array;
/**
* Data for default drunomics pdf export.
*
* @param array $data
* Parsed data from csv report.
*/
public function arangeDataForDefaultPdfExport(array $data): array;
}

View File

@ -47,14 +47,26 @@ class PdfExportService implements PdfExportInterface {
// @TODO move this method to CsvReport.
// Add classes to table elements.
// @TODO would it make sense to allow more per user extending?
// @TODO - too much assumptions on data structure. Create a class with
// precise data structure and use that to pass the data around.
public function parsedDataToHtmlTable(array $data): ?string {
$table = '<table><thead><tr class="tr-header">';
$header = array_shift($data);
$classes = '';
foreach ($header as $index => $cell) {
$table .= "<th class=\"th-{$index}\">{$cell}</th>";
}
$table .= '</tr></thead><tbody>';
foreach ($data as $row_index => $row) {
if (!$row) {
if ($row === NULL) {
$classes = 'bold';
}
elseif ($row === 0) {
$classes = 'bold center';
}
continue;
}
list($cells, $colspan) = [[], 0];
foreach ($row as $index => $cell) {
if (!$cell) {
@ -70,10 +82,12 @@ class PdfExportService implements PdfExportInterface {
$cells[] = "<td class=\"td-{$index}\">{$cell}</td>";
}
}
$rows[] = "<tr class=\"tr-{$row_index}\">" . implode($cells) . '</tr>';
$rows[] = "<tr class=\"tr-{$row_index} {$classes}\">" . implode($cells) . '</tr>';
$classes = '';
}
$table .= implode('', $rows);
$table .= '</tbody></table>';
var_dump($table);
return $table;
}
@ -178,8 +192,8 @@ class PdfExportService implements PdfExportInterface {
$tokens['today'] = date('j. m. y');
$month_ago = strtotime('1 month ago');
$tokens['number'] = date('m/y', $month_ago);
$tokens['date_start'] = date('1. m. y', $month_ago);
$tokens['date_end'] = date("Y-m-d", mktime(0, 0, 0, (int) date("m"), 0));
$tokens['date_start'] = date('1. m. Y', $month_ago);
$tokens['date_end'] = date("d. m. Y", mktime(0, 0, 0, (int) date("m"), 0));
return $tokens;
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace RprtCli\ValueObjects;
class Expenses implements ExpensesInterface {
/**
* Expenses in current currency.
*/
private float $value;
/**
* Name of the expenses;
*/
private string $name;
public function __construct(string $name, float $value) {
$this->name = $name;
$this->value = $value;
}
public function getValue(): float
{
return $this->value;
}
public function getName(): string
{
return $this->name;
}
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace RprtCli\ValueObjects;
interface ExpensesInterface extends InvoiceElementInterface {
public function getValue() :float;
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace RprtCli\ValueObjects;
/**
* Main interface for invoice elements.
*/
interface InvoiceElementInterface {
/**
* Project or expenses name.
*/
public function getName() :string;
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace RprtCli\ValueObjects;
class WorkInvoiceElement implements WorkInvoiceElementInterface {
private float $time;
/**
* Project name.
*/
private string $name;
public function __construct(string $name, float $time) {
$this->name = $name;
$this->time = $time;
}
public function getTime() :float {
return $this->time;
}
public function getName() :string {
return $this->name;
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace RprtCli\ValueObjects;
interface WorkInvoiceElementInterface extends InvoiceElementInterface {
public function getTime() :float ;
/**
* Get project name.
*/
public function getName() :string ;
}