Compare commits

...

5 Commits

Author SHA1 Message Date
Lio Novelli 4e3477c4a0 Add entity type management system. 2023-01-10 10:42:36 +01:00
Lio Novelli bbb22ab502 Work on code style. 2023-01-02 22:03:59 +01:00
Lio Novelli 99d16c3a72 Fix phpstan errors, improve automated test. 2023-01-02 20:35:41 +01:00
Lio Novelli 624b895fd6 Fix coding standard. 2023-01-02 15:18:52 +01:00
Lio Novelli ff2e35bb93 Add tests, unify input. 2023-01-01 23:51:59 +01:00
46 changed files with 1745 additions and 349 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
/scratch
.phpcs-cache
*~undo-tree~
/app/tests/data/output

View File

@ -2,7 +2,7 @@
Automate generating invoices from youtrack reports and other time-tracking
related functionality.
** Usage
~./rprt.php invoice -y -p -s~
@ -14,15 +14,14 @@
Asks which report to print from the list of your reports and then prints out
a table with that report.
** Install/Getting started
Get ~.phar~ file from r, run configuration wizzard (planned for version 1.0).
Get ~.phar~ file from r, Run configuration wizzard (planned for version 1.0).
Before version 1.0 is released you have to navigate into ~/app~ directory
and call command through ~.rprt.php~ file.
*** Requirements
1. You have to create a youtrack API token.
@ -45,7 +44,6 @@
- project categories
- hourly wage
- folder for reports
** Components
- report service - provides a csv of a report
@ -61,7 +59,7 @@
*** ValueObjects
*** List my reports
#+begin_example bash
@ -76,21 +74,34 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
I'd be using this app. If it get's picked up, by 5 others - it is worth
5 days of development.
- remove errors from reports
* Development
** Testing
Main source at symfony [[https://symfony.com/doc/current/console.html#testing-commands][console command page]]. If you read thouroughly:
When using the Console component in a standalone project, use Application and
extend the normal \PHPUnit\Framework\TestCase.
** Plan
*** most current
1. For version 0.6.7
- [X] data value objects
- phar file
- [X] phar file
- [X] additional expenses
2. For version 1.0
- plugin system
- for time tracking services
- for reports
- symfony/config & symfony/di components
3. Time tracking service
- youtrack-api
- jira-api
- kimai (https://www.kimai.org/documentation/timesheet.html)
*** current
*** current
1. For Version 1.0
- Track Command (track time from your cli)
@ -106,7 +117,7 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
- Add tests
*** old
*** old
1. Basic structure of the cli-app
1. App preparation
- nice specifications
@ -124,7 +135,6 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
3. Second round of enhancements
1. Invoice output
2. configuration wizard
** Learning
@ -134,8 +144,8 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
** API calls
*** Get csv file
~curl 'https://drunomics.myjetbrains.com/youtrack/api/reports/83-554/export/csv?&$top=-1' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H "Authorization: Bearer $TKN" > ~/Documents/Drunomics/workhours/2021/21-09.csv~

View File

@ -15,31 +15,37 @@
],
"require": {
"symfony/console": "^5.2",
"guzzlehttp/guzzle": "^7.3",
"guzzlehttp/guzzle": "^6.2",
"php-di/php-di": "^6.3",
"symfony/yaml": "^5.2",
"mpdf/mpdf": "^8.0",
"symfony/mailer": "^5.3",
"symfony/google-mailer": "^5.3"
"symfony/google-mailer": "^5.3",
"cybercog/youtrack-rest-php": "^7.0",
"symfony/finder": "^6.2"
},
"autoload": {
"psr-4": {
"RprtCli\\": "src"
"RprtCli\\": ["src", "tests"]
}
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.7",
"phpunit/phpunit": "^9.5",
"opsway/psr12-strict-coding-standard": "^0.5.0",
"phpcompatibility/php-compatibility": "^9.3"
"opsway/psr12-strict-coding-standard": "^1.0",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.9",
"psy/psysh": "^0.11.10"
},
"scripts": {
"cs": "phpcs --colors --standard=PSR12",
"cbf": "phpcbf"
"cs": "vendor/bin/phpcs --colors",
"cbf": "vendor/bin/phpcbf",
"phpstan": "vendor/bin/phpstan analyze -l 5 src"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
},
"minimum-stability": "alpha"
}

668
app/composer.lock generated
View File

@ -4,8 +4,88 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7b0be09c8f282dfcceb075d1455caa28",
"content-hash": "ca5a759d17cf48faeda14bdaab8f4985",
"packages": [
{
"name": "cybercog/youtrack-rest-php",
"version": "7.0.0",
"source": {
"type": "git",
"url": "https://github.com/cybercog/youtrack-rest-php.git",
"reference": "eb0315133d1d3d161da23d26537201afb253dec9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cybercog/youtrack-rest-php/zipball/eb0315133d1d3d161da23d26537201afb253dec9",
"reference": "eb0315133d1d3d161da23d26537201afb253dec9",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": "^6.2",
"php": "^7.1|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"phpstan/phpstan": "^0.12.32",
"phpunit/phpunit": "^7.0|^8.0|^9.0"
},
"suggest": {
"cybercog/laravel-youtrack-sdk": "Laravel integration with PHP YouTrack SDK.",
"cybercog/youtrack-php-sdk": "PHP YouTrack SDK."
},
"type": "library",
"autoload": {
"psr-4": {
"Cog\\YouTrack\\Rest\\": "src/",
"Cog\\Contracts\\YouTrack\\Rest\\": "contracts/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Anton Komarev",
"email": "anton@komarev.com",
"homepage": "https://komarev.com",
"role": "Developer"
}
],
"description": "YouTrack REST API PHP Client.",
"homepage": "https://komarev.com/sources/php-youtrack-rest",
"keywords": [
"api",
"bugtracker",
"client",
"cog",
"cybercog",
"helpdesk",
"issues",
"jetbrains",
"laravel",
"pm",
"rest",
"tickets",
"youtrack",
"yt"
],
"support": {
"docs": "https://github.com/cybercog/youtrack-rest-php/wiki",
"email": "open@cybercog.su",
"issues": "https://github.com/cybercog/youtrack-rest-php/issues",
"source": "https://github.com/cybercog/youtrack-rest-php",
"wiki": "https://github.com/cybercog/youtrack-rest-php/wiki"
},
"funding": [
{
"url": "https://paypal.me/antonkomarev",
"type": "custom"
}
],
"time": "2022-10-02T18:52:01+00:00"
},
{
"name": "doctrine/lexer",
"version": "1.2.1",
@ -156,68 +236,86 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "7.3.0",
"version": "6.5.7",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "7008573787b430c1c1f650e3722d9bba59967628"
"reference": "724562fa861e21a4071c652c8a159934e4f05592"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/7008573787b430c1c1f650e3722d9bba59967628",
"reference": "7008573787b430c1c1f650e3722d9bba59967628",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/724562fa861e21a4071c652c8a159934e4f05592",
"reference": "724562fa861e21a4071c652c8a159934e4f05592",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.4",
"guzzlehttp/psr7": "^1.7 || ^2.0",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.6.1",
"php": ">=5.5",
"symfony/polyfill-intl-idn": "^1.17.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"ext-curl": "*",
"php-http/client-integration-tests": "^3.0",
"phpunit/phpunit": "^8.5.5 || ^9.3.5",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
"psr/log": "^1.1"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "7.3-dev"
"dev-master": "6.5-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
},
"files": [
"src/functions_include.php"
]
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Jeremy Lindblom",
"email": "jeremeamia@gmail.com",
"homepage": "https://github.com/jeremeamia"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle is a PHP HTTP client library",
@ -228,12 +326,28 @@
"framework",
"http",
"http client",
"psr-18",
"psr-7",
"rest",
"web service"
],
"time": "2021-03-23T11:33:13+00:00"
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/6.5.7"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
"type": "tidelift"
}
],
"time": "2022-06-09T21:36:50+00:00"
},
{
"name": "guzzlehttp/promises",
@ -901,55 +1015,6 @@
},
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
"homepage": "https://github.com/php-fig/http-client",
"keywords": [
"http",
"http-client",
"psr",
"psr-18"
],
"time": "2020-06-29T06:28:15+00:00"
},
{
"name": "psr/http-message",
"version": "1.0.1",
@ -1495,6 +1560,70 @@
],
"time": "2021-03-23T23:28:01+00:00"
},
{
"name": "symfony/finder",
"version": "v6.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "81eefbddfde282ee33b437ba5e13d7753211ae8e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/81eefbddfde282ee33b437ba5e13d7753211ae8e",
"reference": "81eefbddfde282ee33b437ba5e13d7753211ae8e",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
"symfony/filesystem": "^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Finder\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v6.2.3"
},
"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-12-22T17:55:15+00:00"
},
{
"name": "symfony/google-mailer",
"version": "v5.3.0",
@ -2603,27 +2732,27 @@
"packages-dev": [
{
"name": "dealerdirect/phpcodesniffer-composer-installer",
"version": "v0.7.1",
"version": "v0.7.2",
"source": {
"type": "git",
"url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git",
"reference": "fe390591e0241955f22eb9ba327d137e501c771c"
"reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/fe390591e0241955f22eb9ba327d137e501c771c",
"reference": "fe390591e0241955f22eb9ba327d137e501c771c",
"url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db",
"reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0 || ^2.0",
"php": ">=5.3",
"squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0"
"squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0"
},
"require-dev": {
"composer/composer": "*",
"phpcompatibility/php-compatibility": "^9.0",
"sensiolabs/security-checker": "^4.1.0"
"php-parallel-lint/php-parallel-lint": "^1.3.1",
"phpcompatibility/php-compatibility": "^9.0"
},
"type": "composer-plugin",
"extra": {
@ -2644,6 +2773,10 @@
"email": "franck.nijhof@dealerdirect.com",
"homepage": "http://www.frenck.nl",
"role": "Developer / IT Manager"
},
{
"name": "Contributors",
"homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors"
}
],
"description": "PHP_CodeSniffer Standards Composer Installer Plugin",
@ -2655,6 +2788,7 @@
"codesniffer",
"composer",
"installer",
"phpcbf",
"phpcs",
"plugin",
"qa",
@ -2665,7 +2799,11 @@
"stylecheck",
"tests"
],
"time": "2020-12-07T18:04:37+00:00"
"support": {
"issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues",
"source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer"
},
"time": "2022-02-04T12:51:07+00:00"
},
{
"name": "doctrine/instantiator",
@ -2795,30 +2933,29 @@
},
{
"name": "opsway/psr12-strict-coding-standard",
"version": "0.5.0",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/opsway/psr12-strict-modern-standart.git",
"reference": "495d5109079f544ec46ec2f0e2161d48f62a6335"
"reference": "2ce8d92f35ed3c229b1da2668c7c02c490af95f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opsway/psr12-strict-modern-standart/zipball/495d5109079f544ec46ec2f0e2161d48f62a6335",
"reference": "495d5109079f544ec46ec2f0e2161d48f62a6335",
"url": "https://api.github.com/repos/opsway/psr12-strict-modern-standart/zipball/2ce8d92f35ed3c229b1da2668c7c02c490af95f4",
"reference": "2ce8d92f35ed3c229b1da2668c7c02c490af95f4",
"shasum": ""
},
"require": {
"dealerdirect/phpcodesniffer-composer-installer": "~0.6",
"php": "^7.3 || ^8.0",
"slevomat/coding-standard": "^6.1",
"squizlabs/php_codesniffer": "^3.5",
"webimpress/coding-standard": "^1.1"
"dealerdirect/phpcodesniffer-composer-installer": "~0.7",
"php": "^8.0",
"slevomat/coding-standard": "^7.0 || ^8.0",
"squizlabs/php_codesniffer": "^3.6 || ^4.0",
"webimpress/coding-standard": "^1.2"
},
"type": "phpcodesniffer-standard",
"extra": {
"branch-alias": {
"dev-master": "0.0.x-dev",
"dev-develop": "1.0.x-dev"
"dev-develop": "2.0.x-dev"
}
},
"autoload": {
@ -2836,7 +2973,13 @@
"opsway",
"psr12-strict"
],
"time": "2021-01-01T11:19:18+00:00"
"support": {
"docs": "https://github.com/opsway/psr12-strict-modern-standart/tree/master/docs",
"issues": "https://github.com/opsway/psr12-strict-modern-standart/issues",
"rss": "https://github.com/opsway/psr12-strict-modern-standart/releases.atom",
"source": "https://github.com/opsway/psr12-strict-modern-standart"
},
"time": "2022-10-22T12:21:38+00:00"
},
{
"name": "phar-io/manifest",
@ -3240,39 +3383,31 @@
},
{
"name": "phpstan/phpdoc-parser",
"version": "0.4.9",
"version": "1.15.3",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "98a088b17966bdf6ee25c8a4b634df313d8aa531"
"reference": "61800f71a5526081d1b5633766aa88341f1ade76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/98a088b17966bdf6ee25c8a4b634df313d8aa531",
"reference": "98a088b17966bdf6ee25c8a4b634df313d8aa531",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/61800f71a5526081d1b5633766aa88341f1ade76",
"reference": "61800f71a5526081d1b5633766aa88341f1ade76",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
"php": "^7.2 || ^8.0"
},
"require-dev": {
"consistence/coding-standard": "^3.5",
"ergebnis/composer-normalize": "^2.0.2",
"jakub-onderka/php-parallel-lint": "^0.9.2",
"phing/phing": "^2.16.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12.26",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/phpunit": "^6.3",
"slevomat/coding-standard": "^4.7.2",
"symfony/process": "^4.0"
"phpstan/phpstan": "^1.5",
"phpstan/phpstan-phpunit": "^1.1",
"phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^9.5",
"symfony/process": "^5.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.4-dev"
}
},
"autoload": {
"psr-4": {
"PHPStan\\PhpDocParser\\": [
@ -3285,7 +3420,70 @@
"MIT"
],
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"time": "2020-08-03T20:32:43+00:00"
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.15.3"
},
"time": "2022-12-20T20:56:55+00:00"
},
{
"name": "phpstan/phpstan",
"version": "1.9.4",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03bccee595e2146b7c9d174486b84f4dc61b0f2",
"reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0"
},
"conflict": {
"phpstan/phpstan-shim": "*"
},
"bin": [
"phpstan",
"phpstan.phar"
],
"type": "library",
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan - PHP Static Analysis Tool",
"keywords": [
"dev",
"static analysis"
],
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.9.4"
},
"funding": [
{
"url": "https://github.com/ondrejmirtes",
"type": "github"
},
{
"url": "https://github.com/phpstan",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
"type": "tidelift"
}
],
"time": "2022-12-17T13:33:52+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -3708,6 +3906,82 @@
],
"time": "2022-04-01T12:37:26+00:00"
},
{
"name": "psy/psysh",
"version": "v0.11.10",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "e9eadffbed9c9deb5426fd107faae0452bf20a36"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/e9eadffbed9c9deb5426fd107faae0452bf20a36",
"reference": "e9eadffbed9c9deb5426fd107faae0452bf20a36",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-tokenizer": "*",
"nikic/php-parser": "^4.0 || ^3.1",
"php": "^8.0 || ^7.0.8",
"symfony/console": "^6.0 || ^5.0 || ^4.0 || ^3.4",
"symfony/var-dumper": "^6.0 || ^5.0 || ^4.0 || ^3.4"
},
"conflict": {
"symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.2"
},
"suggest": {
"ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)",
"ext-pdo-sqlite": "The doc command requires SQLite to work.",
"ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.",
"ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history."
},
"bin": [
"bin/psysh"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "0.11.x-dev"
}
},
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Psy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"description": "An interactive shell for modern PHP.",
"homepage": "http://psysh.org",
"keywords": [
"REPL",
"console",
"interactive",
"shell"
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.11.10"
},
"time": "2022-12-23T17:47:18+00:00"
},
{
"name": "sebastian/cli-parser",
"version": "1.0.1",
@ -4674,37 +4948,37 @@
},
{
"name": "slevomat/coding-standard",
"version": "6.4.1",
"version": "8.7.1",
"source": {
"type": "git",
"url": "https://github.com/slevomat/coding-standard.git",
"reference": "696dcca217d0c9da2c40d02731526c1e25b65346"
"reference": "c51edb898bebd36aac70a190c6a41a7c056bb5b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slevomat/coding-standard/zipball/696dcca217d0c9da2c40d02731526c1e25b65346",
"reference": "696dcca217d0c9da2c40d02731526c1e25b65346",
"url": "https://api.github.com/repos/slevomat/coding-standard/zipball/c51edb898bebd36aac70a190c6a41a7c056bb5b9",
"reference": "c51edb898bebd36aac70a190c6a41a7c056bb5b9",
"shasum": ""
},
"require": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7",
"php": "^7.1 || ^8.0",
"phpstan/phpdoc-parser": "0.4.5 - 0.4.9",
"squizlabs/php_codesniffer": "^3.5.6"
"php": "^7.2 || ^8.0",
"phpstan/phpdoc-parser": ">=1.15.0 <1.16.0",
"squizlabs/php_codesniffer": "^3.7.1"
},
"require-dev": {
"phing/phing": "2.16.3",
"php-parallel-lint/php-parallel-lint": "1.2.0",
"phpstan/phpstan": "0.12.48",
"phpstan/phpstan-deprecation-rules": "0.12.5",
"phpstan/phpstan-phpunit": "0.12.16",
"phpstan/phpstan-strict-rules": "0.12.5",
"phpunit/phpunit": "7.5.20|8.5.5|9.4.0"
"phing/phing": "2.17.4",
"php-parallel-lint/php-parallel-lint": "1.3.2",
"phpstan/phpstan": "1.4.10|1.9.3",
"phpstan/phpstan-deprecation-rules": "1.1.0",
"phpstan/phpstan-phpunit": "1.0.0|1.3.1",
"phpstan/phpstan-strict-rules": "1.4.4",
"phpunit/phpunit": "7.5.20|8.5.21|9.5.27"
},
"type": "phpcodesniffer-standard",
"extra": {
"branch-alias": {
"dev-master": "6.x-dev"
"dev-master": "8.x-dev"
}
},
"autoload": {
@ -4717,7 +4991,25 @@
"MIT"
],
"description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.",
"time": "2020-10-05T12:39:37+00:00"
"keywords": [
"dev",
"phpcs"
],
"support": {
"issues": "https://github.com/slevomat/coding-standard/issues",
"source": "https://github.com/slevomat/coding-standard/tree/8.7.1"
},
"funding": [
{
"url": "https://github.com/kukulich",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard",
"type": "tidelift"
}
],
"time": "2022-12-14T08:49:18+00:00"
},
{
"name": "squizlabs/php_codesniffer",
@ -4775,6 +5067,94 @@
},
"time": "2022-06-18T07:21:10+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v6.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "fdbadd4803bc3c96ef89238c9c9e2ebe424ec2e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/fdbadd4803bc3c96ef89238c9c9e2ebe424ec2e0",
"reference": "fdbadd4803bc3c96ef89238c9c9e2ebe424ec2e0",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"phpunit/phpunit": "<5.4.3",
"symfony/console": "<5.4"
},
"require-dev": {
"ext-iconv": "*",
"symfony/console": "^5.4|^6.0",
"symfony/process": "^5.4|^6.0",
"symfony/uid": "^5.4|^6.0",
"twig/twig": "^2.13|^3.0.4"
},
"suggest": {
"ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
"ext-intl": "To show region name in time zone dump",
"symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
},
"bin": [
"Resources/bin/var-dump-server"
],
"type": "library",
"autoload": {
"files": [
"Resources/functions/dump.php"
],
"psr-4": {
"Symfony\\Component\\VarDumper\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides mechanisms for walking through any arbitrary PHP variable",
"homepage": "https://symfony.com",
"keywords": [
"debug",
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v6.2.3"
},
"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-12-22T17:55:15+00:00"
},
{
"name": "theseer/tokenizer",
"version": "1.2.1",
@ -4827,24 +5207,24 @@
},
{
"name": "webimpress/coding-standard",
"version": "1.2.1",
"version": "1.2.4",
"source": {
"type": "git",
"url": "https://github.com/webimpress/coding-standard.git",
"reference": "fbeb31ee876b3c493779d3aa717ebb57ef258e6a"
"reference": "cd0c4b0b97440c337c1f7da17b524674ca2f9ca9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webimpress/coding-standard/zipball/fbeb31ee876b3c493779d3aa717ebb57ef258e6a",
"reference": "fbeb31ee876b3c493779d3aa717ebb57ef258e6a",
"url": "https://api.github.com/repos/webimpress/coding-standard/zipball/cd0c4b0b97440c337c1f7da17b524674ca2f9ca9",
"reference": "cd0c4b0b97440c337c1f7da17b524674ca2f9ca9",
"shasum": ""
},
"require": {
"php": "^7.3 || ^8.0",
"squizlabs/php_codesniffer": "^3.5.8"
"squizlabs/php_codesniffer": "^3.6.2"
},
"require-dev": {
"phpunit/phpunit": "^9.4.3"
"phpunit/phpunit": "^9.5.13"
},
"type": "phpcodesniffer-standard",
"extra": {
@ -4868,7 +5248,17 @@
"psr-12",
"webimpress"
],
"time": "2021-01-11T18:13:55+00:00"
"support": {
"issues": "https://github.com/webimpress/coding-standard/issues",
"source": "https://github.com/webimpress/coding-standard/tree/1.2.4"
},
"funding": [
{
"url": "https://github.com/michalbundyra",
"type": "github"
}
],
"time": "2022-02-15T19:52:12+00:00"
},
{
"name": "webmozart/assert",
@ -4930,7 +5320,7 @@
}
],
"aliases": [],
"minimum-stability": "stable",
"minimum-stability": "alpha",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,

View File

@ -20,6 +20,8 @@ use RprtCli\Utils\PdfExport\PdfExportService;
use RprtCli\Utils\TimeTrackingServices\YoutrackInterface;
use RprtCli\Utils\TimeTrackingServices\YoutrackService;
use Psr\Container\ContainerInterface;
use Symfony\Component\Finder\Finder;
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\EntityManager;
# use Symfony\Component\Translation\Translator;
#use Symfony\Component\Translation\Loader\PoFileLoader;
@ -38,9 +40,9 @@ return [
'mpdf' => factory(function (ContainerInterface $c) {
return new Mpdf(['tempDir' => sys_get_temp_dir()]);
}),
// 'mpdf' => function () {
// return new Mpdf(['tempDir' => sys_get_temp_dir()]);
// },
'finder' => factory(function (ContainerInterface $c) {
return new Finder();
}),
ConfigurationInterface::class => get(ConfigurationService::class),
ConfigurationService::class => create()->constructor(
get('config.path'),
@ -73,6 +75,10 @@ return [
get('pdf_export.service')
),
'mailer' => get(MailerInterface::class),
EntityManager::class => create()->constructor(
get('finder')
),
'youtrack.entity_manager' => get(EntityManager::class),
InvoiceCommand::class => create()->constructor(
get('csv.report'),
get('config.service'),

View File

@ -12,9 +12,14 @@
<arg value="p"/>
<!-- Paths to check -->
<file>tests</file>
<file>src</file>
<file>test</file>
<!-- Include all rules from the Zend Coding Standard -->
<rule ref="OpsWayStrictPSR12CodingStandard"/>
<!-- rule ref="OpsWayStrictPSR12CodingStandard"> -->
<rule ref="PSR12">
<exclude name="Generic.NamingConventions.CamelCapsVariableName.Invalid" />
<exclude name="Squiz.NamingConventions.ValidVariableName" />
<!-- <exclude name="WebimpressCodingStandard.NamingConventions.ValidVariableName" /> -->
</rule>
</ruleset>

View File

@ -9,6 +9,7 @@ use DI\ContainerBuilder;
require __DIR__ . '/vendor/autoload.php';
$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/dependencies.php');
$container = $builder->build();
@ -19,4 +20,5 @@ $application->add($invoiceCommand);
$reportCommand = $container->get(ReportCommand::class);
$application->add($reportCommand);
// eval(\Psy\sh());
$application->run();

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
// src/Commands/InvoiceCommand.php;
namespace RprtCli\Commands;
use RprtCli\Utils\Configuration\ConfigurationInterface;
use Symfony\Component\Console\Command\Command;
// use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Abstract class that adds some common services and input options.
*/
class AbstractCliCommand extends Command
{
protected $configuration;
// @TODO Add service factory service.
// protected $trackingServiceFactory;
/**
* Dependency inversion.
*/
public function __construct(
ConfigurationInterface $configuration,
?string $name = null
) {
$this->configuration = $configuration;
parent::__construct($name);
}
}

View File

@ -30,6 +30,7 @@ use function is_string;
use function readline;
use function strpos;
use function strtolower;
use function sys_get_temp_dir;
use function var_dump;
use function var_export;
@ -40,30 +41,34 @@ use function var_export;
*/
class InvoiceCommand extends Command
{
protected $csv;
use SelectReportTrait;
protected $configuration;
protected ReportCsvInterface $csv;
protected $youtrack;
protected ConfigurationInterface $config;
protected $pdfExport;
protected YoutrackInterface $trackingService;
const TYPE_WORK = 1;
const TYPE_EXPENSE = 2;
protected PdfExportInterface $pdfExport;
protected MailerInterface $mailer;
protected const TYPE_WORK = 1;
protected const TYPE_EXPENSE = 2;
public function __construct(
ReportCsvInterface $csv,
ConfigurationInterface $configuration,
YoutrackInterface $youtrack,
YoutrackInterface $trackingService,
PdfExportInterface $pdf_export,
MailerInterface $mailer,
?string $name = null
) {
$this->csv = $csv;
$this->configuration = $configuration;
$this->youtrack = $youtrack;
$this->pdfExport = $pdf_export;
$this->mailer = $mailer;
$this->csv = $csv;
$this->config = $configuration;
$this->trackingService = $trackingService;
$this->pdfExport = $pdf_export;
$this->mailer = $mailer;
parent::__construct($name);
}
@ -82,12 +87,12 @@ class InvoiceCommand extends Command
InputOption::VALUE_REQUIRED,
'Specify the input csv file to generate report from.'
);
$this->addOption(
'youtrack',
'y',
InputOption::VALUE_NONE,
'Use youtrack api to get a report. If this option is used --file does not have any effect..'
);
// $this->addOption(
// 'youtrack',
// 'y',
// InputOption::VALUE_NONE,
// 'Use youtrack api to get a report. If this option is used --file does not have any effect..'
// );
$this->addOption(
'pdf',
'p',
@ -122,6 +127,7 @@ class InvoiceCommand extends Command
'expenses',
'e',
InputOption::VALUE_OPTIONAL,
// phpcs:ignore
'List of additional expenses in format expense1=value1;expenses2=value2... or empty for interactive output.',
false
);
@ -129,6 +135,7 @@ class InvoiceCommand extends Command
'custom',
'c',
InputOption::VALUE_OPTIONAL,
// phpcs:ignore
'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
);
@ -150,73 +157,42 @@ class InvoiceCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output) : int
{
if ($input->getOption('test')) {
$test = $this->youtrack->testYoutrackapi();
$test = $this->trackingService->testYoutrackapi();
$output->writeln($test);
}
if ($input->getOption('list-reports')) {
$list = $this->youtrack->listReports();
$list = $this->trackingService->listReports();
$output->writeln(var_export($list, true));
return Command::SUCCESS;
}
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
if ($report = $input->getOption('report')) {
$this->youtrack->setReportId($report);
} else {
$reports = $this->youtrack->listReports();
$count = 1;
foreach ($reports as $id => $name) {
$output->writeln("[{$count}] {$name} ({$id})");
$count++;
}
$report = readline('Select id of the report: ');
// Asume people are literate.
$this->youtrack->setReportId($report);
}
if ($output->isVerbose()) {
$output->writeln("Setting report: <info>{$report}</info>.");
}
}
if ($youtrack = $input->getOption('youtrack')) {
$report_id = $this->youtrack->getReportId();
$cache_clear_status = $this->youtrack->clearReportCache($report_id);
if ($output->isVerbose()) {
$output->writeln("Report <info>{$report_id}</info> cache cleared, status: {$cache_clear_status}");
}
$file = $this->youtrack->downloadReport($report_id);
}
// Gets report parameter.
$file = $this->getReportCsvFilePath($input, $output, 'tracking_service.youtrack.invoice.report');
$report_name = $this->trackingService->getReportName();
if ($input->hasParameterOption('--expenses') || $input->hasParameterOption('-e')) {
$expenses = $this->getCustomWorkOrExpenses($input->getOption('expenses'), self::TYPE_EXPENSE);
}
if ($input->hasParameterOption('--custom') || $input->hasParameterOption('-c')) {
// @TODO Add option for custom time tracking data.
$custom = $this->getCustomWorkOrExpenses($input->getOption('custom'), self::TYPE_WORK);
}
if ($youtrack || $file = $input->getOption('file')) {
// Youtrack can also provide a file name.
if ($output->isVerbose()) {
$output->writeln("Csv file downloaded to: <info>{$file}</info>");
}
$data = $this->csv->getInvoiceData($file);
if (! empty($expenses)) {
$data = array_merge($data, $expenses);
}
// $table = $this->generateTable($output, $data);
$table = $this->getTable($output, $data);
$table->render();
if ($input->getOption('pdf')) {
$nice_data = $this->csv->arangeDataForDefaultPdfExport($data);
// @TODO method gatherTokens();
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 <info>${output_path}</info>.");
}
// return Command::SUCCESS;
$output->writeln("report: <info>{$report_name}</info>");
$data = $this->csv->getInvoiceData($file);
if (! empty($expenses)) {
$data = array_merge($data, $expenses);
}
if ($input->getOption('send') && $output_path) {
$table = $this->getTable($output, $data);
$table->render();
if ($input->getOption('pdf')) {
$nice_data = $this->csv->arangeDataForDefaultPdfExport($data);
// @TODO method gatherTokens();
if ($out = $input->getOption('output')) {
$this->pdfExport->setOutput($out);
}
$output_path = $this->pdfExport->fromDefaultDataToPdf($nice_data) ?: sys_get_temp_dir();
// Notify the user where the file was generated to.
$output->writeln("The file was generated at <info>${output_path}</info>.");
}
if ($input->getOption('send') && isset($output_path)) {
// @TODO If no output path print an error.
// Send email to configured address.
if ($recipients = $input->getOption('recipients')) {
@ -267,7 +243,7 @@ class InvoiceCommand extends Command
'Price',
]);
[$rows, $totalHours, $totalPrice] = [[], 0, 0];
$projectsConfig = $this->configuration->get('projects');
$projectsConfig = $this->config->get('projects');
foreach ($projectsConfig as $name => $config) {
if (! isset($data[$name])) {
// @TODO Proper error handling.
@ -300,7 +276,7 @@ class InvoiceCommand extends Command
$rows[] = new TableSeparator();
// @TODO Check rate in final result.
// $rows[] = [$this->translator->trans('Sum'), $totalHours, $config['price'], $totalPrice];
$rows[] = ['Sum', $totalHours, $config['price'], $totalPrice];
$rows[] = ['Sum', $totalHours, null, $totalPrice];
$table->setRows($rows);
return $table;
@ -353,7 +329,7 @@ class InvoiceCommand extends Command
return $output;
}
protected function getCustomWorkOrExpenses($custom, $type)
protected function getCustomWorkOrExpenses(mixed $custom, int $type)
{
$output = [];
if (is_string($custom)) {
@ -369,6 +345,8 @@ class InvoiceCommand extends Command
} elseif ($type === self::TYPE_EXPENSE) {
$message_name = 'Enter expenses name or leave empty to stop: ';
$message_value = 'Enter expenses value: ';
} else {
throw new Exception('Unknown type of custom data.');
}
while ($continue) {
$name = readline($message_name);

View File

@ -14,20 +14,21 @@ use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use function array_flip;
use function is_array;
use function is_null;
class ReportCommand extends Command
{
protected $trackingService;
use SelectReportTrait;
protected $config;
protected YoutrackInterface $trackingService;
protected $csv;
protected ConfigurationInterface $config;
protected ReportCsvInterface $csv;
// phpcs:ignore
public function __construct(ConfigurationInterface $configuration, YoutrackInterface $tracking_service, ReportCsvInterface $csv, ?string $name = null)
{
$this->config = $configuration;
@ -45,14 +46,23 @@ class ReportCommand extends Command
'report',
'r',
InputOption::VALUE_OPTIONAL,
'Select a report from list of your reports'
'Select a report from list ofo your reports'
);
// Not supported by by ReportCsv service!
// @TODO Build factory for time tracking service (youtrack-csv-export,
// youtrack-api, jira-api, kimai-api (biro.radiostudent.si)).
$this->addOption(
'time-range',
't',
InputOption::VALUE_REQUIRED,
'Calculates report from tracking service work items directly for time range'
);
$this->addOption(
'file',
'f',
InputOption::VALUE_REQUIRED,
'Specify the input csv file to generate report from.'
);
}
protected function execute(InputInterface $input, OutputInterface $output) : int
@ -61,50 +71,17 @@ class ReportCommand extends Command
// @TODO: Implement time range option:
// - Request workTime items from tracking service
// - Filter them, join by issue, project ...
// This will came in other report service that will gather data
// directly from cache.
if ($output->isVerbose()) {
$output->writeln("Time range: {$timeRange}");
}
$output->writeln('<error>This option is not supported yet.</error>');
return Command::FAILURE;
}
$reports = $this->trackingService->listReports();
// Could just parse a csv file or actually get workItems from youtrack ...
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
if (! $report = $input->getOption('report')) {
// @TODO Make selection nicer.
// QuestionHelper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html
$helper = $this->getHelper('question');
$question = new ChoiceQuestion('Select report:', $reports);
$question
->setErrorMessage('Report %s does not exist!')
->setAutocompleterValues($reports);
$report = $helper->ask($input, $output, $question);
$output->writeln('Report ' . $report . ' selected.');
}
// If parameter option is not recognised check if report name was given.
if (! isset($reports[$report])) {
if (! isset(array_flip($reports)[$report])) {
$output->writeln('Non-existing report ' . $report . '. Exiting.');
return Command::SUCCESS;
}
$report = array_flip($reports)[$report];
}
$this->trackingService->setReportId($report);
} elseif ($report = $this->config->get('tracking_service.youtrack.report.default')) {
$this->trackingService->setReportId($report);
}
// Currently we only support csv download.
$report_id = $this->trackingService->getReportId();
$report_name = $reports[$report_id];
// Code duplication.
$cache_clear_status = $this->trackingService->clearReportCache($report_id);
if ($output->isVerbose()) {
$output->writeln("Report cache cleared, status: {$cache_clear_status}");
}
$file = $this->trackingService->downloadReport($report_id);
if ($output->isVerbose()) {
$output->writeln("Csv file downloaded to: <info>{$file}</info>");
}
$file = $this->getReportCsvFilePath($input, $output);
$report_name = $this->trackingService->getReportName();
$output->writeln("report: <info>{$report_name}</info>");
$data = $this->csv->generateReportTable($file);
$table = $this->buildTable($output, $data);
@ -132,7 +109,10 @@ class ReportCommand extends Command
$rows[$key] = new TableSeparator();
} elseif (is_array($row) && is_null($row[0]) && is_null($row[2])) {
// Check which elements in array are null.
$rows[$key] = [new TableCell($row[1], ['colspan' => 2]), new TableCell((string) $row[3], ['colspan' => 2])];
$rows[$key] = [
new TableCell($row[1], ['colspan' => 2]),
new TableCell((string) $row[3], ['colspan' => 2]),
];
}
}
$table->setRows($rows);

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace RprtCli\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use function array_flip;
/**
* Trait to select report.
*
* To be used on commands with trackingService property and config property.
* Command must have report input option. It is not the most elegant solution
* but it helps avoiding code duplication.
*/
trait SelectReportTrait
{
protected function checkContext()
{
if (! isset($this->trackingService)) {
return false;
}
return $this instanceof Command;
}
protected function getReportParameter(
InputInterface $input,
OutputInterface $output,
$default = 'tracking_service.youtrack.report.report'
) {
if (! $this->checkContext()) {
return Command::FAILURE;
}
$reports = $this->trackingService->listReports();
// Could just parse a csv file or actually get workItems from youtrack ...
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
if (! $report_id = $input->getOption('report')) {
// @TODO Make selection nicer.
// QuestionHelper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html
$helper = $this->getHelper('question');
$question = new ChoiceQuestion('Select report:', $reports);
$question
->setErrorMessage('Report %s does not exist!')
->setAutocompleterValues($reports);
$report_id = $helper->ask($input, $output, $question);
$output->writeln('Report ' . $report_id . ' selected.');
}
// If parameter option is not recognised check if report name was given.
if (! isset($reports[$report_id])) {
if (! isset(array_flip($reports)[$report_id])) {
$output->writeln('Non-existing report ' . $report_id . '. Exiting.');
return Command::FAILURE;
}
$report_id = array_flip($reports)[$report_id];
}
$this->trackingService->setReportId($report_id);
} elseif ($report_id = $this->config->get($default)) {
// If report is not set, try getting default report from configuration.
// This is dependant on the config.
$this->trackingService->setReportId($report_id);
}
$this->trackingService->setReportName();
}
protected function getReportCsvFilePath(
InputInterface $input,
OutputInterface $output,
?string $default = 'tracking_service.youtrack.report.report'
) : ?string {
if (! $file = $input->getOption('file')) {
$this->getReportParameter($input, $output, $default);
$report_id = $this->trackingService->getReportId();
$cache_clear_status = $this->trackingService->clearReportCache($report_id);
if ($output->isVerbose()) {
$output->writeln("Report <info>{$report_id}</info> cache cleared, status: {$cache_clear_status}");
}
$file = $this->trackingService->downloadReport($report_id);
if ($output->isVerbose()) {
$output->writeln("Csv file downloaded to: <info>{$file}</info>");
}
} else {
$this->trackingService->setReportName($file);
}
return $file;
}
}

View File

@ -34,6 +34,7 @@ class TrackCommand extends Command
{
$this->setName('youtrack');
$this->setDescription('Track time into your youtrack service');
// phpcs:ignore
$this->addUsage('rprt-cli youtrack --issue=[issue-name] --minutes=[minutes] --date=[days-ago] --description=[text] --work-type=[work-type]');
// Options or arguments? Technically they are arguments but default value could be provided by config.
// Options are more suitable.

View File

@ -69,7 +69,7 @@ class ConfigurationService implements ConfigurationInterface
return $fullPath;
}
}
// @TODO This should be some kind of error!
// @TODO This should be some kind of error!
var_dump('Config File Not Found!');
return false;
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
// src/Utils/Configuration/TranslationService.php
namespace RprtCli\Utils\Translation;
use RprtCli\Utils\Configuration\ConfigurationInterface;
class TranslationService

View File

@ -111,7 +111,12 @@ class CsvReport implements CsvReportInterface
}
// @TODO Check rate in final result.
// $rows[] = [$this->translator->trans('Sum'), $totalHours, $config['price'], $totalPrice];
$rows[] = ['Gesamt netto', number_format($totalHours, 2, ',', '.'), number_format($config['price'], 2, ',', '.'), number_format($totalPrice, 2, ',', '.')];
$rows[] = [
'Gesamt netto',
number_format($totalHours, 2, ',', '.'),
null,
number_format($totalPrice, 2, ',', '.'),
];
$rows[] = [null, null, 'Gessamt brutto', number_format($totalPrice, 2, ',', '.')];
return $rows;
@ -127,15 +132,15 @@ class CsvReport implements CsvReportInterface
'price' => 26,
// optional specify columns
],
'WV' => [
'name' => 'Wirtschaftsverlag',
'pattern' => 'WV-[0-9]+',
'TNP' => [
'name' => 'Triglav National Park',
'pattern' => 'TNP-[0-9]+',
'price' => 26,
// optional specify columns
],
'Other' => [
'name' => 'Other projects',
'pattern' => '(?!.\bLDP\b)(?!.\bWV\b)',
'pattern' => '(?!.\bLDP\b)(?!.\bTNP\b)',
'price' => 26,
// optional specify columns
],

View File

@ -6,6 +6,8 @@ namespace RprtCli\Utils\CsvReport;
/**
* Handles creating report data from csv file downloaded from youtrack service.
*
* @deprecated Use ReportCsv instead.
*/
interface CsvReportInterface
{

View File

@ -17,7 +17,6 @@ use function explode;
use function fgetcsv;
use function fopen;
use function implode;
use function is_array;
use function is_numeric;
use function number_format;
use function preg_match;
@ -58,7 +57,7 @@ class ReportCsv implements ReportCsvInterface
if ($file = fopen($filePath, 'r')) {
while (($line = fgetcsv($file)) !== false) {
$parsed = $this->parseCsvFile($line);
// $key = reset(array_keys($parsed));
// $key = reset(array_keys($parsed));
$key = array_key_first($parsed);
if (isset($output[$key])) {
$output[$key] += (float) reset($parsed);
@ -98,11 +97,11 @@ class ReportCsv implements ReportCsvInterface
{
[$rows, $totalHours, $totalPrice, $add_separator] = [[], 0, 0, false];
$projectsConfig = $this->configurationService->get('projects');
// @TODO Remove unnecessary header after check.
// $header = $this->configurationService->get('export.labels', null);
$header = null;
if (is_array($header)) {
$rows[] = $header;
}
// if (is_array($header)) {
// $rows[] = $header;
// }
// First only list work invoice elements.
foreach ($data as $key => $invoice_element) {
if ($invoice_element instanceof WorkInvoiceElementInterface) {
@ -127,7 +126,12 @@ class ReportCsv implements ReportCsvInterface
if ($add_separator) {
// @TODO replace separators with constants for normal separating.
$rows[] = null;
$rows[] = ['Gesamt netto', number_format($totalHours, 2, ',', '.'), ' ', number_format($totalPrice, 2, ',', '.')];
$rows[] = [
'Gesamt netto',
number_format($totalHours, 2, ',', '.'),
' ',
number_format($totalPrice, 2, ',', '.'),
];
$add_separator = false;
}
if (empty($data)) {
@ -181,17 +185,27 @@ class ReportCsv implements ReportCsvInterface
return $rows;
}
public function generateReportTable(string $filePath)
/**
* {@inheritdoc}
*/
public function generateReportTable(string $filePath) : array
{
// ticket-id, ticket-name, time-spent
$data = $this->parseReportData($filePath);
if (empty($data)) {
return [];
}
[$previous, $time_sum, $project_time, $table, $all_projects] = [$data[0]['id'], 0, 0, [], []];
$explodeMinus = fn($ticket) => explode('-', $ticket)[0] ?? 'UNKNOWN';
[$previous, $time_sum, $project_time, $table, $all_projects] = [
$data[0]['id'],
0,
0,
[],
[$explodeMinus($data[0]['id'])],
];
foreach ($data as $line) {
$project = explode('-', $line['id'])[0];
$previous_project = explode('-', $previous)[0];
$project = $explodeMinus($line['id']);
$previous_project = $explodeMinus($previous);
if ($project !== $previous_project) {
// When project changes, add a sum of time for that project.
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
@ -241,33 +255,4 @@ class ReportCsv implements ReportCsvInterface
}
return $output;
}
/**
* 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

@ -12,17 +12,17 @@ interface ReportCsvInterface
/**
* Normal separator.
*/
const SEPARATOR_SOFT = false;
public const SEPARATOR_SOFT = false;
/**
* Medium separator. Next line bold.
*/
const SEPARATOR_MEDIUM = null;
public const SEPARATOR_MEDIUM = null;
/**
* Next line should be bold and centered.
*/
const SEPARATOR_HARD = 0;
public const SEPARATOR_HARD = 0;
/**
* Returns array of hours per configured projects.
@ -48,4 +48,6 @@ interface ReportCsvInterface
* Parsed data from csv report.
*/
public function arangeDataForDefaultPdfExport(array $data) : array;
public function generateReportTable(string $filePath) : array;
}

View File

@ -9,4 +9,15 @@ namespace RprtCli\Utils\Mailer;
*/
interface MailerInterface
{
/**
* Recipients for message.
*
* @param string[] $to
*/
public function setRecipients(array $to) : void;
/**
* Sends default mail.
*/
public function sendDefaultMail(string $output) : void;
}

View File

@ -46,6 +46,8 @@ class MailerService implements MailerInterface
protected $email;
protected string $password;
public function __construct(ConfigurationInterface $config, PdfExportInterface $pdf)
{
$this->config = $config;
@ -155,7 +157,12 @@ class MailerService implements MailerInterface
public function sendDefaultMail(string $output) : void
{
$tokens = $this->pdf->gatherTokensForTemplate($this->getEmailTemplatePath(), false, $this->getDefaultTokens(), 'email.tokens');
$tokens = $this->pdf->gatherTokensForTemplate(
$this->getEmailTemplatePath(),
false,
$this->getDefaultTokens(),
'email.tokens'
);
$text = $this->pdf->replaceTokensInTemplate($this->getEmailTemplatePath(), $tokens);
$this->sendMail(
$this->getProperty('from'),

View File

@ -75,5 +75,10 @@ interface PdfExportInterface
*
* Token keys and values array.
*/
public function gatherTokensForTemplate(string $template_path, bool $skip_missing, array $runtime_tokens = [], string $config = 'export.token') : array;
public function gatherTokensForTemplate(
string $template_path,
bool $skip_missing,
array $runtime_tokens = [],
string $config = 'export.token'
) : array;
}

View File

@ -69,6 +69,7 @@ class PdfExportService implements PdfExportInterface
$table = '<table><thead><tr class="tr-header">';
$header = array_shift($data);
$classes = '';
$rows = [];
foreach ($header as $index => $cell) {
$table .= "<th class=\"th-{$index}\">{$cell}</th>";
}
@ -138,8 +139,12 @@ class PdfExportService implements PdfExportInterface
*
* Token keys and values array.
*/
public function gatherTokensForTemplate(string $template_path, bool $skip_missing = false, array $runtime_tokens = [], string $config = 'export.tokens') : array
{
public function gatherTokensForTemplate(
string $template_path,
bool $skip_missing = false,
array $runtime_tokens = [],
string $config = 'export.tokens'
) : array {
[$tokens, $missing] = [[], []];
$token_keys = $this->getTokensInTemplate(file_get_contents($template_path));
$config_tokens = $this->config->get($config);

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices;
use Attribute;
/**
* Define entities and their properties.
*
* This should actually be the attribute extension that defines entity attributes.
*/
#[Attribute]
class EntityDefinition {
public function __construct(
private string $id,
private string $provider,
private string $name,
private array $fields,
private array $filters
) { }
public function getId() {
return $this->id;
}
public function getProvider() {
return $this->provider;
}
public function getName() {
return $this->name;
}
public function getFields() {
return $this->fields;
}
public function getFilters() {
return $this->filters;
}
public function getDefinition() {
return [
'id' => $this->id,
'provider' => $this->provider,
'name' => $this->name,
'fields' => $this->fields,
'filters' => $this->filters,
];
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices;
/**
* Interface for enitities.
*
* Rest api mapping is defined by attributes.
*/
interface EntityInterface {
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices;
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
/**
* Describes entity manager interface.
*/
interface EntityManagerInterface {
/**
* List supported entity types.
*/
public function list(): array;
/**
* Gets entity interface.
*/
public function getDefinition(string $id): ?array;
public function createInstance(string $id, array $values): ?EntityInterface;
}

View File

@ -4,6 +4,11 @@ declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices;
/**
* Youtrack report interface.
*
* @TODO Rename into something more in line with what the class does.
*/
interface YoutrackInterface
{
/**
@ -34,4 +39,15 @@ interface YoutrackInterface
* Array of reports with ids as keys and names as values.
*/
public function listReports() : array;
public function setReportId(string $report_id) : void;
public function setReportName(?string $report_name = null) : void;
public function getReportName() : ?string;
/**
* Clears cache for youtrack report.
*/
public function clearReportCache(string $report_id) : int;
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
use RprtCli\Utils\TimeTrackingServices\EntityManagerInterface;
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
use Symfony\Component\Finder\Finder;
class EntityManager implements EntityManagerInterface {
protected const ENTITIES_DIR = 'YoutrackEntityTypes';
protected const ENTITIES_NAMESPACE = 'RprtCli\\Utils\\TimeTrackingServices\\YoutrackRestApi';
protected ?array $entityDefinitions = null;
public function __construct(protected Finder $finder) {
$this->finder = $finder;
}
/**
* Attributes based discovery from EntityTypes folder.
*/
public function list() :array {
return $this->listEntityDefinitions();
}
/**
* Returns all the entity definitions.
*/
public function listEntityDefinitions() {
if (!$this->entityDefinitions) {
$this->discoverEntities();
}
return $this->entityDefinitions;
}
/**
* Discovers entity definitions.
*/
private function discoverEntities() {
if (!empty($this->entityDefinitions)) {
return $this->entityDefinitions;
}
$path = __DIR__ . '/' . self::ENTITIES_DIR;
$definitions = [];
// @todo create a proxy service for finder.
$this->finder->files()->in($path);
/** @var SplFileInfo $file */
foreach ($this->finder as $file) {
$class = self::ENTITIES_NAMESPACE . '\\' . self::ENTITIES_DIR . '\\' . $file->getBasename('.php');
$reflection = new \ReflectionClass($class);
$attribute = $this->getAttributeOfInstance($reflection, EntityDefinition::class);
// We expect a single attribute of this kind.
if (!$attribute) {
var_dump($file);
continue;
}
$instance = $attribute->newInstance();
$id = $instance->getId();
$content = $instance->getDefinition();
$content['class'] = $class;
$definitions[$id] = $content;
}
$this->entityDefinitions = $definitions;
return $definitions;
}
/**
* Returns an attribute of a given instance or NULL.
*
* @param \ReflectionClass $reflection
* The reflection class.
* @param string $instance
* The instance the attribute should be of.
*
* @return
* The attribute instance.
*/
protected function getAttributeOfInstance(\ReflectionClass $reflection, string $instance) {
$t = $reflection->getAttributes();
var_dump($t);
var_dump($t[0]->getName());
var_dump($t[0]->getArguments());
var_dump($t[0]->newInstance());
$s = $reflection->getAttributes(EntityDefinition::class, \ReflectionAttribute::IS_INSTANCEOF);
var_dump($s);
$attributes = $reflection->getAttributes($instance, \ReflectionAttribute::IS_INSTANCEOF);
var_dump($attributes);
if (empty($attributes)) {
return NULL;
}
return reset($attributes);
}
public function getDefinition(string $id) :?array {
if (!isset($this->entityDefinitions)) {
$this->list();
}
if (!isset($this->entityDefinitions[$id])) {
// @TODO maybe we should throw an error here.
return NULL;
}
return $this->entityDefinitions[$id];
}
public function createInstance(string $id, array $values) :?EntityInterface {
if (!isset($this->entityDefinitions)) {
$this->list();
}
if (!isset($this->entityDefinitions[$id])) {
// @TODO maybe we should throw an error here.
return NULL;
}
$definition = $this->getDefinition($id);
$reflection = new \ReflectionClass($definition['class']);
return new $reflection->newInstanceArgs($values);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
use RprtCli\Utils\TimeTrackingServices\EntityManagerInterface;
class SimpleYoutrackEntityManager implements EntityManagerInterface {
public function list() {
return [
'issue' => [
'fields' => [
'id',
'idReadable',
],
],
'project',
'work_item',
];
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
/**
* Abstract class for youtrack entities.
*/
abstract class YoutrackEntity implements EntityInterface {
}

View File

@ -0,0 +1 @@
lio@spacemacs.22534:1672395843

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
/**
* https://www.jetbrains.com/help/youtrack/devportal/api-entity-Issue.html
*/
#[EntityDefinition(
'issue',
'youtrack_rest_api',
'Issue',
[],
[]
)]
class YoutrackIssue extends YoutrackEntity {
const ID = 'issue';
const NAME = 'name';
const FIELDS = [
'id',
'idReadable',
'created',
'updated',
'project',
'summary',
'description',
'links',
];
// Missing fields
const CUSTOM_FIELDS = [
'assignee',
'state',
];
const FILTERS = [
'project',
'state',
];
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
#[EntityDefinition(
'project',
'youtrack_rest_api',
'Youtrack Project',
[],
[]
)]
class YoutrackProject extends YoutrackEntity {
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
#[EntityDefinition(
'work_item',
'youtrack_rest_api',
'Issue Work Item',
[
'id',
'author',
'text',
'type',
'duration',
'date',
'issue',
],
[
'project',
'issue',
'user',
'date',
]
)]
class YoutrackWorkItem extends YoutrackEntity {
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
// use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
/**
* Youtrack rest api interface.
*
*/
interface YoutrackRestApiInterface
{
/**
* Check if client can sign into youtrack with provided token.
*/
public function testYoutrackapi() : ?string;
/**
* Get the id of the report from configuration.
*
* @TODO more advanced use case with proper data objects.
*/
// public function getItem(string $id) : ?EntityDefinition;
// fields, filters
public function getItem(string $id): array;
/**
* Get a list of items.
*
*
* Array of reports with ids as keys and names as values.
*/
public function listItems(string $id, array $filters = []): array;
}

View File

@ -28,14 +28,15 @@ use function var_dump;
class YoutrackService implements YoutrackInterface
{
protected $ytToken;
protected $ytBaseUrl;
protected string $ytToken;
protected string $ytBaseUrl;
protected $config;
protected ConfigurationInterface $config;
protected $httpClient;
protected ClientInterface $httpClient;
protected $report_id;
protected string $report_id;
protected ?string $reportName = null;
public function __construct(ConfigurationInterface $config, ClientInterface $http_client)
{
@ -106,6 +107,24 @@ class YoutrackService implements YoutrackInterface
$this->report_id = $report_id;
}
public function setReportName(?string $report_name = null) : void
{
if (! $report_name) {
$reports = $this->listReports();
if ($report_id = $this->getReportId()) {
$report_name = $reports[$report_id] ?? null;
} else {
$report_name = null;
}
}
$this->reportName = $report_name;
}
public function getReportName() : ?string
{
return $this->reportName;
}
public function downloadReport(string $report_id) : ?string
{
$path = "youtrack/api/reports/$report_id/export/csv";
@ -203,7 +222,8 @@ class YoutrackService implements YoutrackInterface
{
$path = "/youtrack/api/reports/${report_id}/status";
$query = [
'$top' => -1,
'$top' => -1,
// phpcs:ignore
'fields' => 'calculationInProgress,error(id),errorMessage,isOutdated,lastCalculated,progress,wikifiedErrorMessage',
];
$post = [

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace RprtCli\Tests\Kernel;
use DI\ContainerBuilder;
use PHPUnit\Framework\TestCase;
use RprtCli\Commands\InvoiceCommand;
use RprtCli\Commands\ReportCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use function file_exists;
use function unlink;
/**
* Report and invoice command test.
*
* Does not cover the part of talking to the api, nor does it yet cover email
* and pdf generation. @TODO
*/
class ReportCommandTest extends TestCase
{
protected const INPUT_CSV_FILE = __DIR__ . '/../data/21-03.csv';
protected const REPORT_OUTPUT_FILE = __DIR__ . '/../data/report-21-03.txt';
protected const INVOICE_OUTPUT_FILE = __DIR__ . '/../data/invoice-21-03.txt';
protected const INVOICE_OUTPUT_PDF = __DIR__ . '/../data/output/Invoice-test.pdf';
/**
* Run report and invoice command with file option parameter. Check if pdf was generated.
*/
public function testExecute()
{
$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/../test-dependencies.php');
$container = $builder->build();
$application = new Application('Command Line Tool to process Youtrack Reports', '0.1.0');
$invoiceCommand = $container->get(InvoiceCommand::class);
$application->add($invoiceCommand);
$reportCommand = $container->get(ReportCommand::class);
$application->add($reportCommand);
$reportCommand = $application->find('report');
$reportCommandTester = new CommandTester($reportCommand);
$reportCommandTester->execute([
// pass arguments to the helper
'--file' => __DIR__ . '/../data/21-03.csv',
// prefix the key with two dashes when passing options,
// e.g: '--some-option' => 'option_value',
// use brackets for testing array value,
// e.g: '--some-option' => ['option_value'],
]);
$reportCommandTester->assertCommandIsSuccessful();
// the output of the command in the console
$output = $reportCommandTester->getDisplay();
$this->assertStringContainsString('21-03.csv', $output, 'Check that report name is included in console output.');
$this->assertStringEqualsFile(self::REPORT_OUTPUT_FILE, $output, 'Check that report is still the same as it was.');
if (file_exists(self::INVOICE_OUTPUT_PDF)) {
unlink(self::INVOICE_OUTPUT_PDF);
}
$invoiceCommand = $application->find('invoice');
$invoiceCommandTester = new CommandTester($invoiceCommand);
$invoiceCommandTester->execute([
'--file' => self::INPUT_CSV_FILE,
'--pdf' => true,
'--output' => self::INVOICE_OUTPUT_PDF,
]);
$invoiceCommandTester->assertCommandIsSuccessful();
// the output of the command in the console
$invoice_output = $invoiceCommandTester->getDisplay();
// var_dump($invoice_output);
$this->assertStringContainsString('21-03.csv', $invoice_output, 'Check that invoice command output contains report name.');
$this->assertStringEqualsFile(self::INVOICE_OUTPUT_FILE, $invoice_output, 'Check that invoice command output is the same as it was.');
$this->assertFileExists(self::INVOICE_OUTPUT_PDF, 'Check that pdf file was generated.');
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace RprtCli\Tests\Unit\YoutrackRestApi;
use DI\ContainerBuilder;
use PHPUnit\Framework\TestCase;
/**
* Test the entity manager service and system.
*/
class EntityManagerTest extends TestCase {
/**
* Run report and invoice command with file option parameter. Check if pdf was generated.
*/
public function testExecute()
{
$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/../../test-dependencies.php');
$container = $builder->build();
$entityManagerService = $container->get('youtrack.entity_manager');
// Instance of EntityMangerInterface;
// $this->assertInstanceOf('EntityManagerInterface', $entityManagerService);
$definitions = $entityManagerService->list();
var_dump($definitions);
$this->assertCount(3, $definitions);
$entities = ['project', 'issue', 'work_item'];
foreach ($entities as $entity) {
$this->assertArrayHasKey($entity, $definitions, 'Check that key exists in entity types definitions.');
}
// list method returns definitions: project, issue, worktItem
// Check one definition, compare.
}
}

View File

@ -0,0 +1,4 @@
# YoutrackRestApi Unit tests
1. Check that EntityManager lists defined entity types.
2. Check that EntityTypeManager properly creates entity.

View File

@ -0,0 +1,53 @@
Group name,Item,Item Summary,Estimation time,Spent time
-,DEV-46,"Wochenplanung, Weekly",0,315
-,DEV-56,Developer training,0,60
-,DEV-122,Daily standup,0,255
-,INF-6,"Infrastructure misc, diverse",0,75
-,SND-181,Sprint meetings (PM),240,390
-,SND-507,(TKT-307) Change webhook integration for Slack,0,330
-,SND-552,(TKT-740) Allow kicking users out of collections,780,45
-,SND-554,(TKT-1448) Adaptions in Backend,0,30
-,SND-556,[story] (TKT-1393) Make Paragraph embedding more user friendly,0,75
-,SND-558,(TKT-1467) Full copy of article doesn't update the timestamp,0,15
-,SND-562,(TKT-1441) Implement SEO title in BE,0,45
-,LDP-668,Add json-ld schema.org metadata to articles,0,300
-,LDP-685,"LDP-Contentpool sitemaps. Extend ""simple_sitemap_extended"" setup to support per front-end site sitemaps.",0,15
-,LDP-692,Output json-ld script,0,30
-,LDP-693,Add WebPage schema.org support,120,180
-,LDP-700,LDP onboarding Somebody,0,15
-,LDP-706,Backend FAQ Marketing Block,0,165
-,LDP-707,"Backend ""Hero"" Marketing Block",0,1035
-,LDP-709,"Backend ""Newsletter Signup"" Marketing Block",0,195
-,LDP-711,"Backend ""Call to action"" Marketing Block",0,165
-,LDP-712,"Backend ""Feature section"" Marketing Block",0,300
-,LDP-713,Provide a field type and pre-configuration for choosing icons,300,45
-,LDP-716,Entity browser UX is not as good as expected,0,15
-,LDP-720,Improve display of nodes and add support for article heros,0,45
-,LDP-728,ldp-marketing blocks break the page when they are added to the layout builder,0,195
-,LDP-729,ldp-cp portal entity,0,480
-,LDP-740,CP: Add portal path prefixes,480,30
-,LDP-741,Defects marketing Blocks,0,450
-,LDP-742,[Red Alert] Develop branch fails to build,0,45
-,TES-8,Plan hosting setup on custom hosting provider,0,45
-,TNP-31,"Internal sprint meetings (PM, fixed-price).",0,150
-,TNP-4304,"(ODT-1240) Structured data on article (satellite) pages and overview pages such as the startpage, branch, topic & channel pages.",600,750
-,TNP-4305,(ODT-1202) Add mobile-banner-3 & 4 (advertisement in articles),0,60
-,TNP-4326,(ODT-1174) Make image optional in the Call to Action block,135,60
-,TNP-4355,(ODT-1270) Add article title to breadcrumbs,0,270
-,TNP-4359,"(ODT-1254) Display the text paragraph ""zwischentitel"" in H2",0,90
-,TNP-4372,(ODT-1297) Branch analytics for articles associated with specific branch (GA),135,15
-,TNP-4384,(ODT-1306) Layout-builder rendering in Chrome browser,120,120
-,TNP-4393,TNPsat docker build error,0,90
-,TNP-4410,(ODT-1317) SOLR Search optimization >> suggested articles does NOT affect the relevance for search results.,375,30
-,TNP-4411,Extend simple_sitemap_extensions for dynamic variants & Install and configure simple_sitemap_extensions,600,2070
-,TNP-4412,Implement custom dynamic variants for sitemaps to group by month,0,105
-,TNP-4414,Invalid JSON in the cache_tools composer.json file,0,60
-,TNP-4421,TNP Satellites Hand bau image file entities are not replicated,0,15
-,TNP-4422,"[Red Alert] Behat test fail. ""Demo content loaded..Error pages work.""",0,75
-,TNP-4423,(ODT-1325) Site managers must be able to change article status from Unpublished to Unpublished,0,15
-,TNP-4425,Unpublished Advertorials are returned,0,30
-,TNP-4427,"(ODT-1332) ""Og: image"" is not specified explicitly",120,15
-,TNP-4437,"(ODT-1339) User (id 511) on wv-contentpool is locked and cannot access the system.",0,30
-,TNP-4442,(ODT-1341) Custom HTML block is not rendered,0,120
-,TNP-4478,(ODT-1368) Hosting provider transition of all projects hosted on their OpenShift infrastructure to the new Kuberneets (k8) infrastructure.,0,90
-,TNP-4480,(ODT-1370) Error: Call to a member function getUrl() on null / cannot save an article on stage systems,0,90
1 Group name Item Item Summary Estimation time Spent time
2 - DEV-46 Wochenplanung, Weekly 0 315
3 - DEV-56 Developer training 0 60
4 - DEV-122 Daily standup 0 255
5 - INF-6 Infrastructure misc, diverse 0 75
6 - SND-181 Sprint meetings (PM) 240 390
7 - SND-507 (TKT-307) Change webhook integration for Slack 0 330
8 - SND-552 (TKT-740) Allow kicking users out of collections 780 45
9 - SND-554 (TKT-1448) Adaptions in Backend 0 30
10 - SND-556 [story] (TKT-1393) Make Paragraph embedding more user friendly 0 75
11 - SND-558 (TKT-1467) Full copy of article doesn't update the timestamp 0 15
12 - SND-562 (TKT-1441) Implement SEO title in BE 0 45
13 - LDP-668 Add json-ld schema.org metadata to articles 0 300
14 - LDP-685 LDP-Contentpool sitemaps. Extend "simple_sitemap_extended" setup to support per front-end site sitemaps. 0 15
15 - LDP-692 Output json-ld script 0 30
16 - LDP-693 Add WebPage schema.org support 120 180
17 - LDP-700 LDP onboarding Somebody 0 15
18 - LDP-706 Backend FAQ Marketing Block 0 165
19 - LDP-707 Backend "Hero" Marketing Block 0 1035
20 - LDP-709 Backend "Newsletter Signup" Marketing Block 0 195
21 - LDP-711 Backend "Call to action" Marketing Block 0 165
22 - LDP-712 Backend "Feature section" Marketing Block 0 300
23 - LDP-713 Provide a field type and pre-configuration for choosing icons 300 45
24 - LDP-716 Entity browser UX is not as good as expected 0 15
25 - LDP-720 Improve display of nodes and add support for article heros 0 45
26 - LDP-728 ldp-marketing blocks break the page when they are added to the layout builder 0 195
27 - LDP-729 ldp-cp portal entity 0 480
28 - LDP-740 CP: Add portal path prefixes 480 30
29 - LDP-741 Defects marketing Blocks 0 450
30 - LDP-742 [Red Alert] Develop branch fails to build 0 45
31 - TES-8 Plan hosting setup on custom hosting provider 0 45
32 - TNP-31 Internal sprint meetings (PM, fixed-price). 0 150
33 - TNP-4304 (ODT-1240) Structured data on article (satellite) pages and overview pages such as the startpage, branch, topic & channel pages. 600 750
34 - TNP-4305 (ODT-1202) Add mobile-banner-3 & 4 (advertisement in articles) 0 60
35 - TNP-4326 (ODT-1174) Make image optional in the Call to Action block 135 60
36 - TNP-4355 (ODT-1270) Add article title to breadcrumbs 0 270
37 - TNP-4359 (ODT-1254) Display the text paragraph "zwischentitel" in H2 0 90
38 - TNP-4372 (ODT-1297) Branch analytics for articles associated with specific branch (GA) 135 15
39 - TNP-4384 (ODT-1306) Layout-builder rendering in Chrome browser 120 120
40 - TNP-4393 TNPsat docker build error 0 90
41 - TNP-4410 (ODT-1317) SOLR Search optimization >> suggested articles does NOT affect the relevance for search results. 375 30
42 - TNP-4411 Extend simple_sitemap_extensions for dynamic variants & Install and configure simple_sitemap_extensions 600 2070
43 - TNP-4412 Implement custom dynamic variants for sitemaps to group by month 0 105
44 - TNP-4414 Invalid JSON in the cache_tools composer.json file 0 60
45 - TNP-4421 TNP Satellites Hand bau image file entities are not replicated 0 15
46 - TNP-4422 [Red Alert] Behat test fail. "Demo content loaded..Error pages work." 0 75
47 - TNP-4423 (ODT-1325) Site managers must be able to change article status from Unpublished to Unpublished 0 15
48 - TNP-4425 Unpublished Advertorials are returned 0 30
49 - TNP-4427 (ODT-1332) "Og: image" is not specified explicitly 120 15
50 - TNP-4437 (ODT-1339) User (id 511) on wv-contentpool is locked and cannot access the system. 0 30
51 - TNP-4442 (ODT-1341) Custom HTML block is not rendered 0 120
52 - TNP-4478 (ODT-1368) Hosting provider transition of all projects hosted on their OpenShift infrastructure to the new Kuberneets (k8) infrastructure. 0 90
53 - TNP-4480 (ODT-1370) Error: Call to a member function getUrl() on null / cannot save an article on stage systems 0 90

View File

@ -0,0 +1,10 @@
Hello,
I'm sending you the invoice for [[month]] [[year]].
This email and invoice were generated and sent by (rprt-cli)[https://git.kompot.si/lio/RprtCli] (WIP).
Kind regards
Liopold Novelli

View File

@ -0,0 +1,12 @@
report: /home/lio/Projects/drunomix/rprt-cli/app/tests/Kernel/../data/21-03.csv
+-----------------------------+--------+-------+----------+
| Project | Hours | Rate | Price |
+-----------------------------+--------+-------+----------+
| lupus.digital publishing | 61,75 | 25,60 | 1.580,80 |
| Projektbezogene Entwicklung | 100,50 | 29,10 | 2.924,55 |
+-----------------------------+--------+-------+----------+
| Gesamt netto | 162,25 | | 4.505,35 |
+-----------------------------+--------+-------+----------+
| Gessamt brutto | 4.505,35 |
+-----------------------------+--------+-------+----------+
The file was generated at /home/lio/Projects/drunomix/rprt-cli/app/tests/Kernel/../data/output/Invoice-test.pdf.

View File

@ -0,0 +1,91 @@
<html>
<head>
<style>
body {
font-family: sans-serif;
font-size: 12pt;
}
.table {
width: 100%;
display: flex;
justify-content: space-between;
}
.table table {
border: 1px solid;
border-radius: 5px;
width: 100%;
margin: 0px auto;
float: none;
}
p { margin: 0pt; }
td { vertical-align: top; }
table td {
border-left: 0.1mm solid #000000;
border-right: 0.1mm solid #000000;
}
table tbody td {
background-color: #FFF;
border: 0.1mm solid #000000;
padding-right: 2mm;
padding-left: 2mm;
}
tbody td.td-3,
tbody td.td-1,
.right {
text-align: right;
}
tbody td.td-2,
.center {
text-align: center;
}
table td.colspan.td-2 {
text-align: left;
}
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,
table tbody tr.center td.td-2.colspan {
text-align: center!important;
}
/*
tbody tr.tr-2 td,
tbody tr.tr-3 td {
background-color: #EEE;
border-top: 0.4mm solid #000;
}
tbody tr.tr-3 td {
font-weight: bold;
}
*/
.left {
text-align: left;
}
</style>
</head>
<body>
<p class="center">Spletni razvoj, Liopold Doron Novelli s.p.,<br>ADDRESS<br>NUMBER, liopold@drunomics.com<br>NUMBER</p>
<p><br><br></p>
<p>Company name<br>Company Name<br>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 ACCOUNT NUMBER mit der Bankleitzahl BANK zu überweisen.<br></p>
<p><br>Vielen Dank für den Auftrag,<br>
mit besten Grüßen,</p>
<p><br><br></p>
<p><br><br></p>
<p>Liopold Doron Novelli</p>
</body>
</html>

View File

@ -0,0 +1,76 @@
report: /home/lio/Projects/drunomix/rprt-cli/app/tests/Kernel/../data/21-03.csv
+----------------+--------------------------------------------------------------+------+------------+
| Ticket Id | Name | Time | Estimation |
+----------------+--------------------------------------------------------------+------+------------+
| DEV-46 | Wochenplanung, Weekly | 315 | 0 |
| DEV-56 | Developer training | 60 | 0 |
| DEV-122 | Daily standup | 255 | 0 |
+----------------+--------------------------------------------------------------+------+------------+
| DEV | 10.5 |
+----------------+--------------------------------------------------------------+------+------------+
| INF-6 | Infrastructure misc, diverse | 75 | 0 |
+----------------+--------------------------------------------------------------+------+------------+
| INF | 1.25 |
+----------------+--------------------------------------------------------------+------+------------+
| SND-181 | Sprint meetings (PM) | 390 | 240 |
| SND-507 | (TKT-307) Change webhook integration for Slack | 330 | 0 |
| SND-552 | (TKT-740) Allow kicking users out of collections | 45 | 780 |
| SND-554 | (TKT-1448) Adaptions in Backend | 30 | 0 |
| SND-556 | [story] (TKT-1393) Make Paragraph embedding more user friend | 75 | 0 |
| SND-558 | (TKT-1467) Full copy of article doesn't update the timestamp | 15 | 0 |
| SND-562 | (TKT-1441) Implement SEO title in BE | 45 | 0 |
+----------------+--------------------------------------------------------------+------+------------+
| SND | 15.5 |
+----------------+--------------------------------------------------------------+------+------------+
| LDP-668 | Add json-ld schema.org metadata to articles | 300 | 0 |
| LDP-685 | LDP-Contentpool sitemaps. Extend "simple_sitemap_extended" s | 15 | 0 |
| LDP-692 | Output json-ld script | 30 | 0 |
| LDP-693 | Add WebPage schema.org support | 180 | 120 |
| LDP-700 | LDP onboarding Somebody | 15 | 0 |
| LDP-706 | Backend FAQ Marketing Block | 165 | 0 |
| LDP-707 | Backend "Hero" Marketing Block | 1035 | 0 |
| LDP-709 | Backend "Newsletter Signup" Marketing Block | 195 | 0 |
| LDP-711 | Backend "Call to action" Marketing Block | 165 | 0 |
| LDP-712 | Backend "Feature section" Marketing Block | 300 | 0 |
| LDP-713 | Provide a field type and pre-configuration for choosing icon | 45 | 300 |
| LDP-716 | Entity browser UX is not as good as expected | 15 | 0 |
| LDP-720 | Improve display of nodes and add support for article heros | 45 | 0 |
| LDP-728 | ldp-marketing blocks break the page when they are added to t | 195 | 0 |
| LDP-729 | ldp-cp portal entity | 480 | 0 |
| LDP-740 | CP: Add portal path prefixes | 30 | 480 |
| LDP-741 | Defects marketing Blocks | 450 | 0 |
| LDP-742 | [Red Alert] Develop branch fails to build | 45 | 0 |
+----------------+--------------------------------------------------------------+------+------------+
| LDP | 61.75 |
+----------------+--------------------------------------------------------------+------+------------+
| TES-8 | Plan hosting setup on custom hosting provider | 45 | 0 |
+----------------+--------------------------------------------------------------+------+------------+
| TES | 0.75 |
+----------------+--------------------------------------------------------------+------+------------+
| TNP-31 | Internal sprint meetings (PM, fixed-price). | 150 | 0 |
| TNP-4304 | (ODT-1240) Structured data on article (satellite) pages and | 750 | 600 |
| TNP-4305 | (ODT-1202) Add mobile-banner-3 & 4 (advertisement in article | 60 | 0 |
| TNP-4326 | (ODT-1174) Make image optional in the Call to Action block | 60 | 135 |
| TNP-4355 | (ODT-1270) Add article title to breadcrumbs | 270 | 0 |
| TNP-4359 | (ODT-1254) Display the text paragraph "zwischentitel" in H2 | 90 | 0 |
| TNP-4372 | (ODT-1297) Branch analytics for articles associated with spe | 15 | 135 |
| TNP-4384 | (ODT-1306) Layout-builder rendering in Chrome browser | 120 | 120 |
| TNP-4393 | TNPsat docker build error | 90 | 0 |
| TNP-4410 | (ODT-1317) SOLR Search optimization >> suggested articles do | 30 | 375 |
| TNP-4411 | Extend simple_sitemap_extensions for dynamic variants & Inst | 2070 | 600 |
| TNP-4412 | Implement custom dynamic variants for sitemaps to group by m | 105 | 0 |
| TNP-4414 | Invalid JSON in the cache_tools composer.json file | 60 | 0 |
| TNP-4421 | TNP Satellites Hand bau image file entities are not replicat | 15 | 0 |
| TNP-4422 | [Red Alert] Behat test fail. "Demo content loaded..Error pag | 75 | 0 |
| TNP-4423 | (ODT-1325) Site managers must be able to change article stat | 15 | 0 |
| TNP-4425 | Unpublished Advertorials are returned | 30 | 0 |
| TNP-4427 | (ODT-1332) "Og: image" is not specified explicitly | 15 | 120 |
| TNP-4437 | (ODT-1339) User (id 511) on wv-contentpool is locked and can | 30 | 0 |
| TNP-4442 | (ODT-1341) Custom HTML block is not rendered | 120 | 0 |
| TNP-4478 | (ODT-1368) Hosting provider transition of all projects hoste | 90 | 0 |
| TNP-4480 | (ODT-1370) Error: Call to a member function getUrl() on null | 90 | 0 |
+----------------+--------------------------------------------------------------+------+------------+
| TNP | 72.5 |
+----------------+--------------------------------------------------------------+------+------------+
| DEV, INF, SND, LDP, TES, TNP | 162.25 |
+----------------+--------------------------------------------------------------+------+------------+

View File

@ -0,0 +1,47 @@
tracking_service:
youtrack:
auth_token: 'perm:<secret>'
base_url: 'https://<organization>.myjetbrains.com'
report_id: '83-554'
# report command
report:
report: '83-541'
# invoice command
invoice:
# default value for report parameter
report: '83-554'
export:
template_path: 'tests/data/invoice-template.html'
output: 'tests/data/output/Invoice-Novelli-[[month]]-[[year]].pdf'
labels:
- 'Tätigkeit'
- 'Stunden'
- 'Stundensatz'
- 'EUR'
email:
template_path: 'tests/data/rprt-cli/email-template.txt'
username: '<username>'
tokens:
me: 'Liopold Novelli'
to:
- 'wolfgang.ziegler@drunomics.com'
- 'accounting@drunomics.com'
from: 'liopolddoron@gmail.com'
projects:
LDP:
name: 'lupus.digital publishing'
pattern: 'LDP'
price: 25.6
currency: 'EUR'
project_column: 1
time_column: 4
# time format m - minutes, h - hours
time_format: 'm'
Other:
name: 'Projektbezogene Entwicklung'
pattern: '(?!.\bLDP\b)'
price: 29.1
currency: 'EUR'
project_column: 1
time_column: 4
time_format: 'm'

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
$dependencies = require_once __DIR__ . '/../dependencies.php';
$dependencies['config.path'] = __DIR__ . '/data/';
return $dependencies;

View File

@ -1,5 +1,4 @@
TODO:
- nice report selection
- clean up the config (default reports)
- Improve readme
- abstract baseCommand with some input params
@ -8,16 +7,29 @@ TODO:
- in dependencies.php find subdependencies.php
- Create interface for 3rd party service integration and abstract class
- Create factory to add correct service of a 3rd party service to the Command
- separator constant
------------------------
- add plugin system for time tracking service
- add tests
------------------------
- question helper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html
- psr-12 code check and add phpstan https://phpstan.org/user-guide/getting-started
- phing for build automation
- add ddev for dockerization
- invoice command improvements
- pdf and output option could be combined
TODO (end of 2022):
- DONE fix project list output (first is missing)
- DONE report selection trait
- DONE automated tests
- DONE phpcs
- DONE phpstan (level 5)
OPTIONAL:
- phing build system to run quality assurance
- DONE fix -y parameter in invoice
- time track command
- report from api service and factory:
- youtrack
- jira
- kimai
- build phar without most of the fonts
@ -30,4 +42,8 @@ DONE:
- upload a phar file on repo
- create release
- clear report cache
- nice report selection
- separator constant
- add tests
- psr-12 code check and add phpstan https://phpstan.org/user-guide/getting-started
- question helper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html