Compare commits
7 Commits
master
...
yt-rest-ap
Author | SHA1 | Date |
---|---|---|
|
519c0b074a | |
|
4e3477c4a0 | |
|
bbb22ab502 | |
|
99d16c3a72 | |
|
624b895fd6 | |
|
ff2e35bb93 | |
|
4dcbcb228a |
|
@ -3,3 +3,4 @@
|
|||
/scratch
|
||||
.phpcs-cache
|
||||
*~undo-tree~
|
||||
/app/tests/data/output
|
||||
|
|
27
README.org
27
README.org
|
@ -17,8 +17,7 @@
|
|||
|
||||
** Install/Getting started
|
||||
|
||||
Get ~.phar~ file from r, run configuration wizzard (planned for version 1.0).
|
||||
|
||||
Get ~.phar~ file from r, Run configruration 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.
|
||||
|
@ -46,7 +45,6 @@
|
|||
- hourly wage
|
||||
- folder for reports
|
||||
|
||||
|
||||
** Components
|
||||
- report service - provides a csv of a report
|
||||
- local csv file read (provided via argument)
|
||||
|
@ -77,18 +75,36 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
|
|||
5 days of development.
|
||||
- remove errors from reports
|
||||
|
||||
* Support the work
|
||||
|
||||
If you find this Free Software useful you can consider donating to my
|
||||
[[https://liberapay.com/tehnoklistir/][@tehno-klistir liberapay account.]]
|
||||
|
||||
* 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
|
||||
|
||||
|
@ -126,7 +142,6 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
|
|||
2. configuration wizard
|
||||
|
||||
|
||||
|
||||
** Learning
|
||||
- https://www.youtube.com/watch?v=aCqM9YnjTe0
|
||||
- Choices (~new ChoiceQuestion~)
|
||||
|
@ -134,8 +149,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~
|
||||
|
||||
|
||||
|
|
|
@ -15,25 +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.5",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"opsway/psr12-strict-coding-standard": "^0.5.0"
|
||||
"opsway/psr12-strict-coding-standard": "^1.0",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"psy/psysh": "^0.11.10"
|
||||
},
|
||||
"scripts": {
|
||||
"cs": "phpcs",
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -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": "81050635de2f87c3f7f693ec8cb30645",
|
||||
"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",
|
||||
|
@ -2949,6 +3092,68 @@
|
|||
},
|
||||
"time": "2022-02-21T01:04:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpcompatibility/php-compatibility",
|
||||
"version": "9.3.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPCompatibility/PHPCompatibility.git",
|
||||
"reference": "9fb324479acf6f39452e0655d2429cc0d3914243"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243",
|
||||
"reference": "9fb324479acf6f39452e0655d2429cc0d3914243",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"squizlabs/php_codesniffer": "^2.3 || ^3.0.2"
|
||||
},
|
||||
"conflict": {
|
||||
"squizlabs/php_codesniffer": "2.6.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.",
|
||||
"roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
|
||||
},
|
||||
"type": "phpcodesniffer-standard",
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Wim Godden",
|
||||
"homepage": "https://github.com/wimg",
|
||||
"role": "lead"
|
||||
},
|
||||
{
|
||||
"name": "Juliette Reinders Folmer",
|
||||
"homepage": "https://github.com/jrfnl",
|
||||
"role": "lead"
|
||||
},
|
||||
{
|
||||
"name": "Contributors",
|
||||
"homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.",
|
||||
"homepage": "http://techblog.wimgodden.be/tag/codesniffer/",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"phpcs",
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues",
|
||||
"source": "https://github.com/PHPCompatibility/PHPCompatibility"
|
||||
},
|
||||
"time": "2019-12-27T09:44:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-common",
|
||||
"version": "2.2.0",
|
||||
|
@ -3178,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\\": [
|
||||
|
@ -3223,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",
|
||||
|
@ -3646,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",
|
||||
|
@ -4612,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": {
|
||||
|
@ -4655,20 +4991,38 @@
|
|||
"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",
|
||||
"version": "3.5.8",
|
||||
"version": "3.7.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
|
||||
"reference": "9d583721a7157ee997f235f327de038e7ea6dac4"
|
||||
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4",
|
||||
"reference": "9d583721a7157ee997f235f327de038e7ea6dac4",
|
||||
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
|
||||
"reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -4706,7 +5060,100 @@
|
|||
"phpcs",
|
||||
"standards"
|
||||
],
|
||||
"time": "2020-10-23T02:01:07+00:00"
|
||||
"support": {
|
||||
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
|
||||
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
|
||||
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
|
||||
},
|
||||
"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",
|
||||
|
@ -4760,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": {
|
||||
|
@ -4801,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",
|
||||
|
@ -4863,11 +5320,11 @@
|
|||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"minimum-stability": "alpha",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.1.0"
|
||||
"plugin-api-version": "2.3.0"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackRestApiClient;
|
||||
use function DI\create;
|
||||
use function DI\get;
|
||||
use function DI\factory;
|
||||
|
@ -20,6 +21,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 +41,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'),
|
||||
|
@ -58,6 +61,8 @@ return [
|
|||
get('config.service'),
|
||||
get('mpdf')
|
||||
),
|
||||
YoutrackRestApiClient::class => create()->constructor(get('config.service')),
|
||||
'youtrack_rest_api.client' => get(YoutrackRestApiClient::class),
|
||||
'pdf_export.service' => get(PdfExportInterface::class),
|
||||
// 'locale' => get('config.service')->method('get', 'en'),
|
||||
// Translator::class => create()->constructor('sl')->method('addLoader', 'po', new PoFileLoader),
|
||||
|
@ -73,6 +78,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'),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,6 @@ $application->add($invoiceCommand);
|
|||
$reportCommand = $container->get(ReportCommand::class);
|
||||
$application->add($reportCommand);
|
||||
|
||||
// eval(\Psy\sh());
|
||||
|
||||
$application->run();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace RprtCli\Commands;
|
||||
|
||||
use Exception;
|
||||
use RprtCli\Utils\Configuration\ConfigurationInterface;
|
||||
use RprtCli\Utils\CsvReport\ReportCsvInterface;
|
||||
use RprtCli\Utils\Mailer\MailerInterface;
|
||||
|
@ -15,44 +16,62 @@ 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\Helper\TableSeparator;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
// use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
use function array_merge;
|
||||
use function explode;
|
||||
use function is_array;
|
||||
use function is_null;
|
||||
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;
|
||||
|
||||
// use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Main file - invoice command.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Mailer service.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -71,12 +90,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',
|
||||
|
@ -111,15 +130,17 @@ 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
|
||||
false
|
||||
);
|
||||
$this->addOption(
|
||||
'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
|
||||
false
|
||||
);
|
||||
$this->addOption(
|
||||
'list-reports',
|
||||
|
@ -132,81 +153,52 @@ class InvoiceCommand extends Command
|
|||
'r',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Show time tracked for report.',
|
||||
FALSE
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
$output->writeln(var_export($list, TRUE));
|
||||
$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) {
|
||||
if (!empty($custom)) {
|
||||
$data = array_merge($data, $custom);
|
||||
}
|
||||
$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')) {
|
||||
|
@ -219,17 +211,20 @@ class InvoiceCommand extends Command
|
|||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function getTable(OutputInterface $output, array $data) :Table {
|
||||
$rows = $this->csv->generateTable($data);
|
||||
protected function getTable(OutputInterface $output, array $data) : Table
|
||||
{
|
||||
$rows = $this->csv->generateTable($data);
|
||||
$table = new Table($output);
|
||||
$table->setHeaders([
|
||||
'Project', 'Hours', 'Rate', 'Price',
|
||||
'Project',
|
||||
'Hours',
|
||||
'Rate',
|
||||
'Price',
|
||||
]);
|
||||
foreach ($rows as $key => $row) {
|
||||
if (!$row) {
|
||||
if (! $row) {
|
||||
$rows[$key] = new TableSeparator();
|
||||
}
|
||||
elseif (is_array($row) && is_null($row[1]) && is_null($row[0])) {
|
||||
} 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]];
|
||||
}
|
||||
|
@ -248,10 +243,13 @@ class InvoiceCommand extends Command
|
|||
{
|
||||
$table = new Table($output);
|
||||
$table->setHeaders([
|
||||
'Project', 'Hours', 'Rate', 'Price',
|
||||
'Project',
|
||||
'Hours',
|
||||
'Rate',
|
||||
'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.
|
||||
|
@ -274,9 +272,9 @@ class InvoiceCommand extends Command
|
|||
$totalPrice += $price;
|
||||
unset($data[$name]);
|
||||
}
|
||||
if (!empty($data)) {
|
||||
if (! empty($data)) {
|
||||
foreach ($data as $name => $value) {
|
||||
if (strpos(strtolower($name), 'expanses') !== FALSE) {
|
||||
if (strpos(strtolower($name), 'expanses') !== false) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -284,7 +282,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;
|
||||
|
@ -314,65 +312,68 @@ class InvoiceCommand extends Command
|
|||
*
|
||||
* @return Expenses[]
|
||||
*/
|
||||
protected function getExpenses($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);
|
||||
$output[] = new Expenses($name, (float) $value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$continue = TRUE;
|
||||
} else {
|
||||
$continue = true;
|
||||
while ($continue) {
|
||||
$name = readline('Enter expenses name or leave empty to stop: ');
|
||||
$name = readline('Enter expenses name or leave empty to stop: ');
|
||||
$value = (float) readline('Enter expenses value: ');
|
||||
if (!empty($name)) {
|
||||
if (! empty($name)) {
|
||||
$output[] = new Expenses($name, $value);
|
||||
}
|
||||
else {
|
||||
$continue = FALSE;
|
||||
} else {
|
||||
$continue = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
protected function getCustomWorkOrExpenses($custom, $type) {
|
||||
protected function getCustomWorkOrExpenses(mixed $custom, int $type)
|
||||
{
|
||||
$output = [];
|
||||
if (is_string($custom)) {
|
||||
foreach (explode(';', $custom) as $item) {
|
||||
[$name, $value] = explode('=', $item);
|
||||
$output[] = $this->createInvoiceElement($name, (float) $value, $type);
|
||||
$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: ';
|
||||
$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: ';
|
||||
} 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);
|
||||
$name = readline($message_name);
|
||||
$value = (float) readline($message_value);
|
||||
if (!empty($name)) {
|
||||
if (! empty($name)) {
|
||||
$output[] = $this->createInvoiceElement($name, $value, $type);
|
||||
} else {
|
||||
$continue = FALSE;
|
||||
$continue = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
protected function createInvoiceElement(string $name, float $value, int $type) {
|
||||
if ($type == self::TYPE_WORK) {
|
||||
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) {
|
||||
} elseif ($type === self::TYPE_EXPENSE) {
|
||||
return new Expenses($name, (float) $value);
|
||||
}
|
||||
throw new \Exception('Unkown invoice element type.');
|
||||
throw new Exception('Unkown invoice element type.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,94 +9,81 @@ use RprtCli\Utils\CsvReport\ReportCsvInterface;
|
|||
use RprtCli\Utils\TimeTrackingServices\YoutrackInterface;
|
||||
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\Helper\TableSeparator;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ReportCommand extends Command {
|
||||
use function is_array;
|
||||
use function is_null;
|
||||
|
||||
protected $trackingService;
|
||||
class ReportCommand extends Command
|
||||
{
|
||||
use SelectReportTrait;
|
||||
|
||||
protected $config;
|
||||
protected YoutrackInterface $trackingService;
|
||||
|
||||
protected $csv;
|
||||
protected ConfigurationInterface $config;
|
||||
|
||||
public function __construct(ConfigurationInterface $configuration, YoutrackInterface $tracking_service, ReportCsvInterface $csv, ?string $name = null) {
|
||||
protected ReportCsvInterface $csv;
|
||||
|
||||
// phpcs:ignore
|
||||
public function __construct(ConfigurationInterface $configuration, YoutrackInterface $tracking_service, ReportCsvInterface $csv, ?string $name = null)
|
||||
{
|
||||
$this->config = $configuration;
|
||||
// @TODO generalize tracking service.
|
||||
$this->trackingService = $tracking_service;
|
||||
$this->csv = $csv;
|
||||
$this->csv = $csv;
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
protected function configure() :void {
|
||||
protected function configure() : void
|
||||
{
|
||||
$this->setName('report');
|
||||
$this->setDescription('Get a time-tracking report into command line.');
|
||||
$this->addOption(
|
||||
'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 {
|
||||
protected function execute(InputInterface $input, OutputInterface $output) : int
|
||||
{
|
||||
if ($timeRange = $input->getOption('time-range')) {
|
||||
// @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')) {
|
||||
$this->trackingService->setReportId($report);
|
||||
} else {
|
||||
$count = 1;
|
||||
foreach ($reports as $id => $name) {
|
||||
$output->writeln("[{$count}] {$name} ({$id})");
|
||||
$count++;
|
||||
}
|
||||
$output->writeln("[{$count}] None (null)");
|
||||
$report = readline('Select id of the report: ');
|
||||
// Asume people are literate.
|
||||
if (!in_array($report, array_keys($reports) )) {
|
||||
$output->writeln('Non-existing report. Exiting.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
$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);
|
||||
$data = $this->csv->generateReportTable($file);
|
||||
$table = $this->buildTable($output, $data);
|
||||
$table->render();
|
||||
|
||||
|
@ -108,21 +95,27 @@ class ReportCommand extends Command {
|
|||
*
|
||||
* @TODO: Code duplication with InvoiceCommand::getTable.
|
||||
*/
|
||||
protected function buildTable(OutputInterface $output, array $rows): Table {
|
||||
protected function buildTable(OutputInterface $output, array $rows) : Table
|
||||
{
|
||||
$table = new Table($output);
|
||||
$table->setHeaders([
|
||||
'Ticket Id', 'Name', 'Time', 'Estimation',
|
||||
'Ticket Id',
|
||||
'Name',
|
||||
'Time',
|
||||
'Estimation',
|
||||
]);
|
||||
foreach ($rows as $key => $row) {
|
||||
if (!$row) {
|
||||
if (! $row) {
|
||||
$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);
|
||||
return $table;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -14,8 +14,8 @@ use Symfony\Component\Console\Command\Command;
|
|||
* Later connect this command to the Emacs and have your time tracked directly
|
||||
* from orgmode.
|
||||
*/
|
||||
class TrackCommand extends Command {
|
||||
|
||||
class TrackCommand extends Command
|
||||
{
|
||||
protected $config;
|
||||
|
||||
protected $youtrack;
|
||||
|
@ -25,19 +25,18 @@ class TrackCommand extends Command {
|
|||
YoutrackInterface $youtrack,
|
||||
?string $name = null
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->config = $config;
|
||||
$this->youtrack = $youtrack;
|
||||
parent::__construct($name);
|
||||
}
|
||||
|
||||
protected function configure(): void {
|
||||
protected function configure() : void
|
||||
{
|
||||
$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.
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -4,19 +4,16 @@ declare(strict_types=1);
|
|||
|
||||
// src/Utils/Configuration/TranslationService.php
|
||||
|
||||
use Symfony\Component\Translation\Translator;
|
||||
namespace RprtCli\Utils\Translation;
|
||||
|
||||
use RprtCli\Utils\Configuration\ConfigurationInterface;
|
||||
|
||||
class TranslationService {
|
||||
|
||||
class TranslationService
|
||||
{
|
||||
protected $config;
|
||||
|
||||
public function __construct(ConfigurationInterface $configuration)
|
||||
{
|
||||
$this->config = $configuration;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -10,8 +10,11 @@ use function array_key_first;
|
|||
use function array_keys;
|
||||
use function fgetcsv;
|
||||
use function fopen;
|
||||
use function is_array;
|
||||
use function number_format;
|
||||
use function preg_match;
|
||||
use function reset;
|
||||
use function var_dump;
|
||||
|
||||
/**
|
||||
* Creates a report of projects and hours.
|
||||
|
@ -61,10 +64,9 @@ class CsvReport implements CsvReportInterface
|
|||
/**
|
||||
* 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
|
||||
|
@ -78,10 +80,11 @@ class CsvReport implements CsvReportInterface
|
|||
return [];
|
||||
}
|
||||
|
||||
public function arangeDataForDefaultPdfExport(array $data): array {
|
||||
public function arangeDataForDefaultPdfExport(array $data) : array
|
||||
{
|
||||
[$rows, $totalHours, $totalPrice] = [[], 0, 0];
|
||||
$projectsConfig = $this->configurationService->get('projects');
|
||||
$header = $this->configurationService->get('export.labels', null);
|
||||
$header = $this->configurationService->get('export.labels', null);
|
||||
if (is_array($header)) {
|
||||
$rows[] = $header;
|
||||
}
|
||||
|
@ -108,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;
|
||||
|
@ -124,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
|
||||
],
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
@ -22,9 +24,8 @@ interface CsvReportInterface
|
|||
/**
|
||||
* Data for default drunomics pdf export.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* Parsed data from csv report.
|
||||
*/
|
||||
public function arangeDataForDefaultPdfExport(array $data): array;
|
||||
|
||||
public function arangeDataForDefaultPdfExport(array $data) : array;
|
||||
}
|
||||
|
|
|
@ -11,17 +11,24 @@ use RprtCli\ValueObjects\WorkInvoiceElementInterface;
|
|||
|
||||
use function array_key_first;
|
||||
use function array_keys;
|
||||
use function array_unshift;
|
||||
use function array_values;
|
||||
use function explode;
|
||||
use function fgetcsv;
|
||||
use function fopen;
|
||||
use function implode;
|
||||
use function is_numeric;
|
||||
use function number_format;
|
||||
use function preg_match;
|
||||
use function reset;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* Creates a report of projects and hours.
|
||||
*
|
||||
* Uses value objects instead of arrays.
|
||||
*/
|
||||
class ReportCsv implements ReportCsvInterface
|
||||
class ReportCsv implements ReportCsvInterface
|
||||
{
|
||||
/**
|
||||
* A configuration service.
|
||||
|
@ -50,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);
|
||||
|
@ -67,10 +74,9 @@ class ReportCsv implements ReportCsvInterface
|
|||
/**
|
||||
* 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
|
||||
|
@ -87,68 +93,75 @@ class ReportCsv implements ReportCsvInterface
|
|||
/**
|
||||
* 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');
|
||||
public function generateTable(array $data) : array
|
||||
{
|
||||
[$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) {
|
||||
$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 = [
|
||||
$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['name'] ?? $project,
|
||||
number_format($hours, 2, ',', '.'),
|
||||
number_format($config['price'], 2, ',', '.'),
|
||||
number_format($price, 2, ',', '.'),
|
||||
];
|
||||
$totalHours += $hours;
|
||||
$totalPrice += $price;
|
||||
$rows[] = $row;
|
||||
$totalHours += $hours;
|
||||
$totalPrice += $price;
|
||||
$rows[] = $row;
|
||||
unset($data[$key]);
|
||||
}
|
||||
}
|
||||
if ($add_separator) {
|
||||
// @TODO replace separators with constants for normal separating.
|
||||
$rows[] = null;
|
||||
$rows[] = ['Gesamt netto', number_format($totalHours, 2, ',', '.'), ' ', number_format($totalPrice, 2, ',', '.')];
|
||||
$add_separator = FALSE;
|
||||
$rows[] = null;
|
||||
$rows[] = [
|
||||
'Gesamt netto',
|
||||
number_format($totalHours, 2, ',', '.'),
|
||||
' ',
|
||||
number_format($totalPrice, 2, ',', '.'),
|
||||
];
|
||||
$add_separator = false;
|
||||
}
|
||||
if (empty($data)) {
|
||||
$add_separator = TRUE;
|
||||
$add_separator = true;
|
||||
}
|
||||
foreach ($data as $invoice_element) {
|
||||
if ($invoice_element instanceof ExpensesInterface) {
|
||||
if (!isset($added_expenses)) {
|
||||
if (! isset($added_expenses)) {
|
||||
// Separator 0: Make next line bold and centered.
|
||||
$rows[] = ReportCsvInterface::SEPARATOR_HARD;
|
||||
$rows[] = [
|
||||
null,
|
||||
null,
|
||||
'Kosten',
|
||||
'Extra', // Kosten
|
||||
'EUR',
|
||||
];
|
||||
// Don't make next line bold. See RprtCli\PdfExport\PdfExportService::parsedDataToHtml.
|
||||
$rows[] = ReportCsvInterface::SEPARATOR_SOFT;
|
||||
$added_expenses = TRUE;
|
||||
$rows[] = ReportCsvInterface::SEPARATOR_SOFT;
|
||||
$added_expenses = true;
|
||||
}
|
||||
$add_separator = TRUE;
|
||||
$rows[] = [
|
||||
$add_separator = true;
|
||||
$rows[] = [
|
||||
null,
|
||||
null,
|
||||
$invoice_element->getName(),
|
||||
number_format($invoice_element->getValue(), 2, ',', '.'),
|
||||
];
|
||||
$totalPrice += $invoice_element->getValue();
|
||||
$totalPrice += $invoice_element->getValue();
|
||||
}
|
||||
// @todo Add Extra time as well!
|
||||
}
|
||||
if ($add_separator) {
|
||||
$rows[] = ReportCsvInterface::SEPARATOR_MEDIUM;
|
||||
|
@ -160,105 +173,87 @@ class ReportCsv implements ReportCsvInterface
|
|||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function arangeDataForDefaultPdfExport(array $data) :array {
|
||||
$rows = $this->generateTable($data);
|
||||
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) {
|
||||
if (! $row) {
|
||||
unset($data[$key]);
|
||||
}
|
||||
}
|
||||
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;
|
||||
$table[] = [null, $previous_project, null, $project_time/60];
|
||||
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
|
||||
$time_sum += (float) $project_time;
|
||||
$project_time = 0;
|
||||
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
|
||||
$table[] = [null, $previous_project, null, $project_time / 60];
|
||||
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
|
||||
$time_sum += (float) $project_time;
|
||||
$project_time = 0;
|
||||
$all_projects[] = $project;
|
||||
}
|
||||
$project_time += (float) $line['time'];
|
||||
$previous = $line['id'];
|
||||
$table[] = array_values($line);
|
||||
$previous = $line['id'];
|
||||
$table[] = array_values($line);
|
||||
}
|
||||
// Add sum for the last project.
|
||||
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
|
||||
$table[] = [null, $project, null, $project_time / 60];
|
||||
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
|
||||
$table[] = [null, $project, null, $project_time / 60];
|
||||
$time_sum += (float) $project_time;
|
||||
// $all_projects[] = $project;
|
||||
// Add a sum of time for whole day.
|
||||
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
|
||||
$table[] = [null, implode(', ', $all_projects), null, $time_sum/60];
|
||||
$table[] = [null, implode(', ', $all_projects), null, $time_sum / 60];
|
||||
return $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function parseReportData(string $filePath): array
|
||||
protected function parseReportData(string $filePath) : array
|
||||
{
|
||||
$output = [];
|
||||
// @TODO replace with config service.
|
||||
// $config = $this->dummyConfig()['projects'];
|
||||
if ($file = fopen($filePath, 'r')) {
|
||||
while (($line = fgetcsv($file)) !== false) {
|
||||
if (!is_numeric($line[4])) {
|
||||
if (! is_numeric($line[4])) {
|
||||
// Skip header at least.
|
||||
continue;
|
||||
}
|
||||
// @TODO validate line
|
||||
$output[] = [
|
||||
'id' => $line[1],
|
||||
'name' => substr($line[2], 0, 60),
|
||||
'time' => $line[4],
|
||||
'id' => $line[1],
|
||||
'name' => substr($line[2], 0, 60),
|
||||
'time' => $line[4],
|
||||
'estimation' => $line[3],
|
||||
];
|
||||
}
|
||||
}
|
||||
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
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,21 +9,20 @@ namespace RprtCli\Utils\CsvReport;
|
|||
*/
|
||||
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.
|
||||
|
@ -33,20 +32,22 @@ interface ReportCsvInterface
|
|||
*
|
||||
* Project key as key and number of hours as value.
|
||||
*/
|
||||
public function getInvoiceData(string $filePath): array;
|
||||
public function getInvoiceData(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;
|
||||
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;
|
||||
public function arangeDataForDefaultPdfExport(array $data) : array;
|
||||
|
||||
public function generateReportTable(string $filePath) : array;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,17 @@ namespace RprtCli\Utils\Mailer;
|
|||
/**
|
||||
* Methods for symfony (swift)mailer service.
|
||||
*/
|
||||
interface MailerInterface {
|
||||
interface MailerInterface
|
||||
{
|
||||
/**
|
||||
* Recipients for message.
|
||||
*
|
||||
* @param string[] $to
|
||||
*/
|
||||
public function setRecipients(array $to) : void;
|
||||
|
||||
/**
|
||||
* Sends default mail.
|
||||
*/
|
||||
public function sendDefaultMail(string $output) : void;
|
||||
}
|
||||
|
|
|
@ -6,21 +6,33 @@ declare(strict_types=1);
|
|||
|
||||
namespace RprtCli\Utils\Mailer;
|
||||
|
||||
use Exception;
|
||||
use RprtCli\Utils\Configuration\ConfigurationInterface;
|
||||
use RprtCli\Utils\PdfExport\PdfExportInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use \Exception;
|
||||
use Symfony\Component\Mailer\Transport;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\Mailer\Transport;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
use function date;
|
||||
use function explode;
|
||||
use function fgets;
|
||||
use function file_exists;
|
||||
use function rawurlencode;
|
||||
use function readline;
|
||||
use function strtotime;
|
||||
use function system;
|
||||
use function trim;
|
||||
use function var_dump;
|
||||
|
||||
use const STDIN;
|
||||
|
||||
/**
|
||||
* Send emails with invoices as attachments.
|
||||
*
|
||||
* https://symfony.com/doc/current/mailer.html
|
||||
*/
|
||||
class MailerService implements MailerInterface {
|
||||
|
||||
|
||||
class MailerService implements MailerInterface
|
||||
{
|
||||
protected $config;
|
||||
|
||||
protected $pdf;
|
||||
|
@ -34,31 +46,37 @@ class MailerService implements MailerInterface {
|
|||
|
||||
protected $email;
|
||||
|
||||
public function __construct(ConfigurationInterface $config, PdfExportInterface $pdf) {
|
||||
protected string $password;
|
||||
|
||||
public function __construct(ConfigurationInterface $config, PdfExportInterface $pdf)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->pdf = $pdf;
|
||||
$this->pdf = $pdf;
|
||||
}
|
||||
|
||||
public function setRecipients(array $to): void
|
||||
public function setRecipients(array $to) : void
|
||||
{
|
||||
$this->to = $to;
|
||||
}
|
||||
|
||||
public function setSubject(string $subject): void {
|
||||
public function setSubject(string $subject) : void
|
||||
{
|
||||
$this->subject = $subject;
|
||||
}
|
||||
|
||||
public function setAttachment(string $path): void {
|
||||
public function setAttachment(string $path) : void
|
||||
{
|
||||
// @TODO - add some error handling.
|
||||
$this->attachment = $path;
|
||||
}
|
||||
|
||||
public function getProperty(string $property) {
|
||||
public function getProperty(string $property)
|
||||
{
|
||||
// Only for simple value properies - string and numbers.
|
||||
// from, to, subject.
|
||||
if (!isset($this->{$property})) {
|
||||
$value = $this->config->get('email.' . $property, FALSE);
|
||||
if (!$value) {
|
||||
if (! isset($this->{$property})) {
|
||||
$value = $this->config->get('email.' . $property, false);
|
||||
if (! $value) {
|
||||
$value = readline("Property {$property} is not configured. Enter value: ");
|
||||
}
|
||||
$this->{$property} = $value;
|
||||
|
@ -66,10 +84,11 @@ class MailerService implements MailerInterface {
|
|||
return $this->{$property};
|
||||
}
|
||||
|
||||
protected function getRecipients() {
|
||||
if (!isset($this->to)) {
|
||||
$value = $this->config->get('email.to', FALSE);
|
||||
if (!$value) {
|
||||
protected function getRecipients()
|
||||
{
|
||||
if (! isset($this->to)) {
|
||||
$value = $this->config->get('email.to', false);
|
||||
if (! $value) {
|
||||
$value = explode(',', readline('Provide recipients\' emails separated by a comma: '));
|
||||
}
|
||||
$this->to = $value;
|
||||
|
@ -77,7 +96,8 @@ class MailerService implements MailerInterface {
|
|||
return $this->to;
|
||||
}
|
||||
|
||||
private function readPassword($prompt = "Enter Password:") {
|
||||
private function readPassword($prompt = "Enter Password:")
|
||||
{
|
||||
echo $prompt;
|
||||
system('stty -echo');
|
||||
$password = trim(fgets(STDIN));
|
||||
|
@ -85,10 +105,11 @@ class MailerService implements MailerInterface {
|
|||
return $password;
|
||||
}
|
||||
|
||||
protected function getPasswordProperty() {
|
||||
if (!isset($this->password)) {
|
||||
$value = $this->config->get('email.password', FALSE);
|
||||
if (!$value) {
|
||||
protected function getPasswordProperty()
|
||||
{
|
||||
if (! isset($this->password)) {
|
||||
$value = $this->config->get('email.password', false);
|
||||
if (! $value) {
|
||||
$value = $this->readPassword();
|
||||
}
|
||||
$this->password = $value;
|
||||
|
@ -96,8 +117,8 @@ class MailerService implements MailerInterface {
|
|||
return $this->password;
|
||||
}
|
||||
|
||||
|
||||
public function sendMail(string $from, array $to, string $subject, string $text, array $attachment = []): void {
|
||||
public function sendMail(string $from, array $to, string $subject, string $text, array $attachment = []) : void
|
||||
{
|
||||
$email = new Email();
|
||||
$email->from($from);
|
||||
$email->to(...$to);
|
||||
|
@ -105,21 +126,21 @@ class MailerService implements MailerInterface {
|
|||
// https://github.com/symfony/mailer
|
||||
$email->subject($subject);
|
||||
$email->text($text);
|
||||
if (!empty($attachment)) {
|
||||
if (!isset($attachment['path'])) {
|
||||
if (! empty($attachment)) {
|
||||
if (! isset($attachment['path'])) {
|
||||
var_dump('Attachment path missing!');
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$email->attachFromPath($attachment['path'], $attachment['name'] ?? null, $attachment['type'] ?? null);
|
||||
}
|
||||
}
|
||||
// Not sure whether it would be nicer to use class property instead of variable.
|
||||
$transport = $this->getTransport();
|
||||
$mailer = $this->getMailer($transport);
|
||||
$mailer = $this->getMailer($transport);
|
||||
$mailer->send($email);
|
||||
}
|
||||
|
||||
public function getTransport() {
|
||||
public function getTransport()
|
||||
{
|
||||
// @TODO remove username and password from config.
|
||||
$username = rawurlencode($this->getProperty('username'));
|
||||
$password = rawurlencode($this->getPasswordProperty());
|
||||
|
@ -129,13 +150,20 @@ class MailerService implements MailerInterface {
|
|||
return Transport::fromDsn($mailer_dsn);
|
||||
}
|
||||
|
||||
public function getMailer($transport) {
|
||||
public function getMailer($transport)
|
||||
{
|
||||
return new Mailer($transport);
|
||||
}
|
||||
|
||||
public function sendDefaultMail(string $output): void {
|
||||
$tokens = $this->pdf->gatherTokensForTemplate($this->getEmailTemplatePath(), FALSE, $this->getDefaultTokens(), 'email.tokens');
|
||||
$text = $this->pdf->replaceTokensInTemplate($this->getEmailTemplatePath(), $tokens);
|
||||
public function sendDefaultMail(string $output) : void
|
||||
{
|
||||
$tokens = $this->pdf->gatherTokensForTemplate(
|
||||
$this->getEmailTemplatePath(),
|
||||
false,
|
||||
$this->getDefaultTokens(),
|
||||
'email.tokens'
|
||||
);
|
||||
$text = $this->pdf->replaceTokensInTemplate($this->getEmailTemplatePath(), $tokens);
|
||||
$this->sendMail(
|
||||
$this->getProperty('from'),
|
||||
$this->getProperty('to'),
|
||||
|
@ -145,21 +173,23 @@ class MailerService implements MailerInterface {
|
|||
);
|
||||
}
|
||||
|
||||
public function getDefaultTokens(): array {
|
||||
$tokens = [];
|
||||
$date = strtotime('-1 month');
|
||||
public function getDefaultTokens() : array
|
||||
{
|
||||
$tokens = [];
|
||||
$date = strtotime('-1 month');
|
||||
$tokens['month'] = date('F', $date);
|
||||
$tokens['year'] = date('Y', $date);
|
||||
$tokens['year'] = date('Y', $date);
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
protected function getEmailTemplatePath(): ?string {
|
||||
if (!isset($this->templatePath)) {
|
||||
$template_path = $this->config->get('email.template_path', FALSE);
|
||||
if (!$template_path) {
|
||||
protected function getEmailTemplatePath() : ?string
|
||||
{
|
||||
if (! isset($this->templatePath)) {
|
||||
$template_path = $this->config->get('email.template_path', false);
|
||||
if (! $template_path) {
|
||||
$template_path = readline('Enter template file path: ');
|
||||
}
|
||||
if (!file_exists($template_path)) {
|
||||
if (! file_exists($template_path)) {
|
||||
throw new Exception('Template file not found!');
|
||||
}
|
||||
$this->templatePath = $template_path;
|
||||
|
@ -167,12 +197,12 @@ class MailerService implements MailerInterface {
|
|||
return $this->templatePath;
|
||||
}
|
||||
|
||||
public function setEmailTemplatePath(string $path): void {
|
||||
public function setEmailTemplatePath(string $path) : void
|
||||
{
|
||||
if (file_exists($path)) {
|
||||
$this->templatePath = $path;
|
||||
return;
|
||||
}
|
||||
throw new Exception('Email template file not found!');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,36 +7,35 @@ namespace RprtCli\Utils\PdfExport;
|
|||
/**
|
||||
* Handles exporting parsed csv data to pdf files.
|
||||
*/
|
||||
interface PdfExportInterface {
|
||||
|
||||
interface PdfExportInterface
|
||||
{
|
||||
/**
|
||||
* Retrieves path to template file either from command option, configuration
|
||||
* or from uer input.
|
||||
*/
|
||||
public function getTemplatePath(): ?string;
|
||||
public function getTemplatePath() : ?string;
|
||||
|
||||
public function setTemplatePath(string $path): void;
|
||||
public function setTemplatePath(string $path) : void;
|
||||
|
||||
/**
|
||||
* Creates html table from parsed csv data.
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* First line is header. The rest of them is table body.
|
||||
*/
|
||||
public function parsedDataToHtmlTable(array $data): ?string;
|
||||
public function parsedDataToHtmlTable(array $data) : ?string;
|
||||
|
||||
/**
|
||||
* Reads the template file and replaces token values.
|
||||
*/
|
||||
public function replaceTokensInTemplate(string $template_path, array $tokens): ?string;
|
||||
public function replaceTokensInTemplate(string $template_path, array $tokens) : ?string;
|
||||
|
||||
/**
|
||||
* Creates and export file.
|
||||
*
|
||||
* @param string $html
|
||||
*
|
||||
* Template file with tokens replaced.
|
||||
*
|
||||
* @return bool
|
||||
* True if export was successfull.
|
||||
*/
|
||||
public function pdfExport(string $html) : bool;
|
||||
|
@ -44,39 +43,42 @@ interface PdfExportInterface {
|
|||
/**
|
||||
* Goes through the whole process of creating a pdf for the invoice.
|
||||
*
|
||||
* @param array $nice_data
|
||||
*
|
||||
* Parsed csv report export data.
|
||||
*
|
||||
* @return string
|
||||
* Path of the pdf file.
|
||||
*/
|
||||
public function fromDefaultDataToPdf(array $nice_data, array $tokens = []): string;
|
||||
public function fromDefaultDataToPdf(array $nice_data, array $tokens = []) : string;
|
||||
|
||||
/**
|
||||
* Sets output file override via command paramater.
|
||||
*
|
||||
* @param string $output
|
||||
*
|
||||
* Path of the output pdf file ('/tmp/test.pdf').
|
||||
*/
|
||||
public function setOutput(string $output): void;
|
||||
public function setOutput(string $output) : void;
|
||||
|
||||
// @TODO support multiple templates by adding template id in config.
|
||||
// @TODO implement twig.
|
||||
/**
|
||||
* Get tokens to replace in the template.
|
||||
*
|
||||
* @param string $template_path
|
||||
*
|
||||
* Path to the template file (long term plan is to support multiple templates).
|
||||
* @param bool $skip_missing
|
||||
*
|
||||
*
|
||||
* Just skip missing tokens.
|
||||
* @param array $runtime_tokens
|
||||
*
|
||||
* Provide tokens at runtime of the application (not supported yet). @TODO
|
||||
* @param string $config
|
||||
*
|
||||
* Config path to tokens.
|
||||
*
|
||||
* @return array
|
||||
* 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;
|
||||
}
|
||||
|
|
|
@ -5,12 +5,23 @@ declare(strict_types=1);
|
|||
namespace RprtCli\Utils\PdfExport;
|
||||
|
||||
use Exception;
|
||||
use RprtCli\Utils\Configuration\ConfigurationInterface;
|
||||
use Mpdf\Output\Destination;
|
||||
use RprtCli\Utils\Configuration\ConfigurationInterface;
|
||||
use RprtCli\Utils\CsvReport\ReportCsvInterface;
|
||||
|
||||
class PdfExportService implements PdfExportInterface {
|
||||
use function array_shift;
|
||||
use function date;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function implode;
|
||||
use function mktime;
|
||||
use function preg_match_all;
|
||||
use function readline;
|
||||
use function str_replace;
|
||||
use function strtotime;
|
||||
|
||||
class PdfExportService implements PdfExportInterface
|
||||
{
|
||||
protected $templatePath;
|
||||
protected $output;
|
||||
|
||||
|
@ -18,18 +29,20 @@ class PdfExportService implements PdfExportInterface {
|
|||
|
||||
protected $mpdf;
|
||||
|
||||
public function __construct(ConfigurationInterface $config, $mpdf) {
|
||||
public function __construct(ConfigurationInterface $config, $mpdf)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->mpdf = $mpdf;
|
||||
$this->mpdf = $mpdf;
|
||||
}
|
||||
|
||||
public function getTemplatePath(): ?string {
|
||||
if (!isset($this->templatePath)) {
|
||||
$template_path = $this->config->get('export.template_path', FALSE);
|
||||
if (!$template_path) {
|
||||
public function getTemplatePath() : ?string
|
||||
{
|
||||
if (! isset($this->templatePath)) {
|
||||
$template_path = $this->config->get('export.template_path', false);
|
||||
if (! $template_path) {
|
||||
$template_path = readline('Enter template file path: ');
|
||||
}
|
||||
if (!file_exists($template_path)) {
|
||||
if (! file_exists($template_path)) {
|
||||
throw new Exception('Template file not found!');
|
||||
}
|
||||
$this->templatePath = $template_path;
|
||||
|
@ -37,7 +50,8 @@ class PdfExportService implements PdfExportInterface {
|
|||
return $this->templatePath;
|
||||
}
|
||||
|
||||
public function setTemplatePath(string $path): void {
|
||||
public function setTemplatePath(string $path) : void
|
||||
{
|
||||
if (file_exists($path)) {
|
||||
$this->templatePath = $path;
|
||||
return;
|
||||
|
@ -50,40 +64,40 @@ class PdfExportService implements PdfExportInterface {
|
|||
// @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);
|
||||
public function parsedDataToHtmlTable(array $data) : ?string
|
||||
{
|
||||
$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>";
|
||||
}
|
||||
$table .= '</tr></thead><tbody>';
|
||||
foreach ($data as $row_index => $row) {
|
||||
if (!$row) {
|
||||
if (! $row) {
|
||||
if ($row === ReportCsvInterface::SEPARATOR_MEDIUM) {
|
||||
$classes = 'bold';
|
||||
}
|
||||
elseif ($row === ReportCsvInterface::SEPARATOR_HARD) {
|
||||
} elseif ($row === ReportCsvInterface::SEPARATOR_HARD) {
|
||||
$classes = 'bold center';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
list($cells, $colspan) = [[], 0];
|
||||
[$cells, $colspan] = [[], 0];
|
||||
foreach ($row as $index => $cell) {
|
||||
if (!$cell) {
|
||||
if (! $cell) {
|
||||
$colspan += 1;
|
||||
continue;
|
||||
}
|
||||
if ($colspan) {
|
||||
$colspan += 1;
|
||||
$cells[] = "<td class=\"td-{$index} colspan\" colspan=\"{$colspan}\">{$cell}</td>";
|
||||
$colspan = 0;
|
||||
}
|
||||
else {
|
||||
$cells[] = "<td class=\"td-{$index} colspan\" colspan=\"{$colspan}\">{$cell}</td>";
|
||||
$colspan = 0;
|
||||
} else {
|
||||
$cells[] = "<td class=\"td-{$index}\">{$cell}</td>";
|
||||
}
|
||||
}
|
||||
$rows[] = "<tr class=\"tr-{$row_index} {$classes}\">" . implode($cells) . '</tr>';
|
||||
$rows[] = "<tr class=\"tr-{$row_index} {$classes}\">" . implode($cells) . '</tr>';
|
||||
$classes = '';
|
||||
}
|
||||
$table .= implode('', $rows);
|
||||
|
@ -92,7 +106,7 @@ class PdfExportService implements PdfExportInterface {
|
|||
return $table;
|
||||
}
|
||||
|
||||
public function replaceTokensInTemplate(string $template_path, array $tokens): ?string
|
||||
public function replaceTokensInTemplate(string $template_path, array $tokens) : ?string
|
||||
{
|
||||
$template = file_get_contents($template_path);
|
||||
foreach ($tokens as $key => $value) {
|
||||
|
@ -102,7 +116,8 @@ class PdfExportService implements PdfExportInterface {
|
|||
}
|
||||
|
||||
// @TODO write a method to gather tokens.
|
||||
public function getTokensInTemplate(string $template): array {
|
||||
public function getTokensInTemplate(string $template) : array
|
||||
{
|
||||
// @TODO find substrings of type [[key]]
|
||||
preg_match_all('/\[\[([a-z0-9-_]+)\]\]/', $template, $match);
|
||||
return $match[1];
|
||||
|
@ -112,41 +127,44 @@ class PdfExportService implements PdfExportInterface {
|
|||
/**
|
||||
* Get tokens to replace in the template.
|
||||
*
|
||||
* @param string $template_path
|
||||
*
|
||||
* Path to the template file (long term plan is to support multiple templates).
|
||||
* @param bool $skip_missing
|
||||
*
|
||||
*
|
||||
* Just skip missing tokens.
|
||||
* @param array $runtime_tokens
|
||||
*
|
||||
* Provide tokens at runtime of the application (not supported yet). @TODO
|
||||
* @param string $config
|
||||
*
|
||||
* Config path to tokens.
|
||||
*
|
||||
* @return array
|
||||
* Token keys and values array.
|
||||
*/
|
||||
public function gatherTokensForTemplate(string $template_path, bool $skip_missing = FALSE, array $runtime_tokens = [], string $config = 'export.tokens'): array {
|
||||
list($tokens, $missing) = [[], []];
|
||||
$token_keys = $this->getTokensInTemplate(file_get_contents($template_path));
|
||||
$config_tokens = $this->config->get($config);
|
||||
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);
|
||||
foreach ($token_keys as $token_key) {
|
||||
if (isset($runtime_tokens[$token_key])) {
|
||||
$tokens[$token_key] = $runtime_tokens[$token_key];
|
||||
}
|
||||
elseif (!isset($config_tokens[$token_key]) && !$skip_missing) {
|
||||
} elseif (! isset($config_tokens[$token_key]) && ! $skip_missing) {
|
||||
$tokens[$token_key] = readline("Enter value to replace [[{$token_key}]] in template: ");
|
||||
}
|
||||
elseif (isset($config_tokens[$token_key])) {
|
||||
} elseif (isset($config_tokens[$token_key])) {
|
||||
$tokens[$token_key] = $config_tokens[$token_key];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$missing[] = $token_key;
|
||||
}
|
||||
}
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
public function pdfExport(string $html, $output = null): bool {
|
||||
$this->mpdf->SetProtection(array('print'));
|
||||
public function pdfExport(string $html, $output = null) : bool
|
||||
{
|
||||
$this->mpdf->SetProtection(['print']);
|
||||
// @TODO make configurable.
|
||||
$this->mpdf->SetTitle("Invoice");
|
||||
$this->mpdf->SetAuthor("rprt-cli");
|
||||
|
@ -156,29 +174,32 @@ class PdfExportService implements PdfExportInterface {
|
|||
return file_exists($this->output);
|
||||
}
|
||||
|
||||
protected function getOutput() {
|
||||
protected function getOutput()
|
||||
{
|
||||
if (isset($this->output)) {
|
||||
return $this->output;
|
||||
}
|
||||
$output = $this->config->get('export.output', NULL) ?? readline('Enter output file path: ');
|
||||
$date = strtotime("-1 month");
|
||||
$output = str_replace('[[month]]', date('F', $date), $output);
|
||||
$output = str_replace('[[year]]', date('Y', $date), $output);
|
||||
$output = $this->config->get('export.output', null) ?? readline('Enter output file path: ');
|
||||
$date = strtotime("-1 month");
|
||||
$output = str_replace('[[month]]', date('F', $date), $output);
|
||||
$output = str_replace('[[year]]', date('Y', $date), $output);
|
||||
$this->output = $output;
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function setOutput(string $path): void {
|
||||
public function setOutput(string $path) : void
|
||||
{
|
||||
$this->output = $path;
|
||||
}
|
||||
|
||||
public function fromDefaultDataToPdf(array $data, array $tokens = []): string {
|
||||
$template_path = $this->getTemplatePath();
|
||||
$tokens = $this->defaultTokens();
|
||||
public function fromDefaultDataToPdf(array $data, array $tokens = []) : string
|
||||
{
|
||||
$template_path = $this->getTemplatePath();
|
||||
$tokens = $this->defaultTokens();
|
||||
$tokens['table'] = $this->parsedDataToHtmlTable($data);
|
||||
$tokens = $this->gatherTokensForTemplate($template_path, FALSE, $tokens);
|
||||
$html = $this->replaceTokensInTemplate($template_path, $tokens);
|
||||
$success = $this->pdfExport($html);
|
||||
$tokens = $this->gatherTokensForTemplate($template_path, false, $tokens);
|
||||
$html = $this->replaceTokensInTemplate($template_path, $tokens);
|
||||
$success = $this->pdfExport($html);
|
||||
if ($success) {
|
||||
return $this->getOutput();
|
||||
}
|
||||
|
@ -188,14 +209,14 @@ class PdfExportService implements PdfExportInterface {
|
|||
/**
|
||||
* Get default tokens.
|
||||
*/
|
||||
protected function defaultTokens(): array {
|
||||
$tokens = [];
|
||||
$tokens['today'] = date('j. m. y');
|
||||
$month_ago = strtotime('1 month ago');
|
||||
$tokens['number'] = date('m/y', $month_ago);
|
||||
protected function defaultTokens() : array
|
||||
{
|
||||
$tokens = [];
|
||||
$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("d. m. Y", mktime(0, 0, 0, (int) date("m"), 0));
|
||||
$tokens['date_end'] = date("d. m. Y", mktime(0, 0, 0, (int) date("m"), 0));
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
<?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 {
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param string $provider
|
||||
* @param string $name
|
||||
* @param array $fields
|
||||
* @param array $filters
|
||||
* @param array $resources
|
||||
*/
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private string $provider,
|
||||
private string $name,
|
||||
private array $fields,
|
||||
private array $filters,
|
||||
private array $resources
|
||||
) { }
|
||||
|
||||
public function getId() :string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getProvider() :string {
|
||||
return $this->provider;
|
||||
}
|
||||
|
||||
public function getName() :string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getFields() :array {
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
public function getFilters() :array {
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be of type ResourceInterface.
|
||||
*/
|
||||
public function getResources() :array {
|
||||
return $this->resources;
|
||||
}
|
||||
|
||||
public function getResource(string $key) :?Resource {
|
||||
return $this->resources[$key] ?? NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get definition as array.
|
||||
*
|
||||
* @todo I'm not really sure why I implemented this method. Definition
|
||||
* is a definition already.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDefinition() :array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'provider' => $this->provider,
|
||||
'name' => $this->name,
|
||||
'fields' => $this->fields,
|
||||
'filters' => $this->filters,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices;
|
||||
|
||||
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
||||
|
||||
/**
|
||||
* Describes entity manager interface.
|
||||
*/
|
||||
interface EntityManagerInterface {
|
||||
|
||||
/**
|
||||
* List supported entity types.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function list(): array;
|
||||
|
||||
/**
|
||||
* Gets entity interface.
|
||||
*/
|
||||
public function getDefinition(string $id): ?array;
|
||||
|
||||
public function createInstance(string $id, array $values): ?EntityInterface;
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices;
|
||||
|
||||
|
||||
/**
|
||||
* Defines resources for entity type.
|
||||
*
|
||||
* This class could be readonly after php82.
|
||||
*/
|
||||
final class Resource {
|
||||
|
||||
public const GET = 'GET';
|
||||
public const POST = 'POST';
|
||||
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private string $method,
|
||||
private string $path,
|
||||
private string $description = ''
|
||||
) { }
|
||||
|
||||
public function getId() :string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getPath() :string {
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function getMethod() :string {
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
public function getDescription() :string {
|
||||
return $this->description;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/**
|
||||
|
@ -19,12 +24,30 @@ interface YoutrackInterface
|
|||
/**
|
||||
* Downloads report and returns file path.
|
||||
*
|
||||
* @param string $report_id
|
||||
*
|
||||
* Youtrack internal report id.
|
||||
*
|
||||
* @return NULL|string
|
||||
*
|
||||
* If fetch was unsuccssefull return false, otherwise the file path.
|
||||
*/
|
||||
public function downloadReport(string $report_id) : ?string;
|
||||
|
||||
/**
|
||||
* Get a list of reports.
|
||||
*
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
|
||||
|
||||
use ReflectionAttribute;
|
||||
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.
|
||||
*
|
||||
* @return array
|
||||
* List of entity definitions keyed with their id.
|
||||
*/
|
||||
public function listEntityDefinitions() :array {
|
||||
if (!$this->entityDefinitions) {
|
||||
$this->discoverEntities();
|
||||
}
|
||||
return $this->entityDefinitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers entity definitions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function discoverEntities() :?array {
|
||||
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);
|
||||
foreach ($this->finder as $file) {
|
||||
/** @var SplFileInfo $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 = [
|
||||
'definition' => $instance,
|
||||
'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 ?ReflectionAttribute
|
||||
* The attribute instance.
|
||||
*/
|
||||
protected function getAttributeOfInstance(\ReflectionClass $reflection, string $instance) :?ReflectionAttribute {
|
||||
$attributes = $reflection->getAttributes($instance, \ReflectionAttribute::IS_INSTANCEOF);
|
||||
if (empty($attributes)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return reset($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity definition by id.
|
||||
*
|
||||
* @param string $id
|
||||
* Id of an entity definition (work_item, issue, project, comment).
|
||||
*
|
||||
* @return ?EntityDefinition
|
||||
* Entity definition (maybe even EntityDefinition object).
|
||||
*/
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create entity instance.
|
||||
*
|
||||
* This method should be used for transfering the data between report cli
|
||||
* tools and the youtrack rest api.
|
||||
*
|
||||
* @param string $id
|
||||
* Instance id (issue, project, work_item, comment ...).
|
||||
* @param array $values
|
||||
* Time, description ...
|
||||
*
|
||||
* @return ?EntityInterface
|
||||
*
|
||||
*/
|
||||
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);
|
||||
// This is not ok. definition is of type EntityDefinition but we want
|
||||
// EntityInterface. This is where $class would come in handy.
|
||||
$reflection = new \ReflectionClass($definition['class']);
|
||||
return $reflection->newInstanceArgs($values);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices;
|
||||
|
||||
use Attribute;
|
||||
|
||||
/**
|
||||
* Define filter properties.
|
||||
*/
|
||||
#[Attribute]
|
||||
class FilterAttribute {
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param string $provider
|
||||
* @param string $name
|
||||
* @param array $allowedFields
|
||||
* @param array $allowedEntities
|
||||
* @param array $resources
|
||||
*/
|
||||
public function __construct(
|
||||
private string $id,
|
||||
private string $provider,
|
||||
private string $name,
|
||||
private array $allowedFields = [],
|
||||
private array $allowedEntities = [],
|
||||
) { }
|
||||
|
||||
public function getId() :string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getProvider() :string {
|
||||
return $this->provider;
|
||||
}
|
||||
|
||||
public function getName() :string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getAllowedFields() :array {
|
||||
return $this->allowedFields;
|
||||
}
|
||||
|
||||
public function getAllowedEntities() :array {
|
||||
return $this->allowedEntities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get definition as array.
|
||||
*
|
||||
* @todo I'm not really sure why I implemented this method. Definition
|
||||
* is a definition already.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDefinition() :array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'provider' => $this->provider,
|
||||
'name' => $this->name,
|
||||
'allowedFields' => $this->allowedFields,
|
||||
'allowedEntities' => $this->allowedEntities,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\Filters;
|
||||
|
||||
/**
|
||||
* Defines a filter attribute and some constants.
|
||||
*/
|
||||
interface YoutrackFilterInterface {
|
||||
|
||||
const OPTIONAL = 0;
|
||||
const REQUIRED = 1;
|
||||
|
||||
}
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
|
||||
|
||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
||||
|
||||
/**
|
||||
* Abstract class for youtrack entities.
|
||||
*/
|
||||
abstract class YoutrackEntity implements EntityInterface {
|
||||
|
||||
protected function getDefintion() :?EntityDefinition {
|
||||
$reflection = new \ReflectionClass(self::class);
|
||||
$attributes = $reflection->getAttributes(EntityDefinition::class, \ReflectionAttribute::IS_INSTANCEOF);
|
||||
if (empty($attributes)) {
|
||||
return NULL;
|
||||
}
|
||||
return reset($attributes);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
lio@spacemacs.22534:1672395843
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
|
||||
|
||||
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||
use RprtCli\Utils\TimeTrackingServices\Resource;
|
||||
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\Filters\YoutarckFilterInterface;
|
||||
|
||||
/**
|
||||
* https://www.jetbrains.com/help/youtrack/devportal/api-entity-IssueComment.html
|
||||
* https://www.jetbrains.com/help/youtrack/devportal/resource-api-issues-issueID-comments.html
|
||||
*/
|
||||
#[EntityDefinition(
|
||||
id: 'issue_comment',
|
||||
provider: 'youtrack_rest_api',
|
||||
name: 'IssueComment',
|
||||
fields: [
|
||||
'id',
|
||||
'text',
|
||||
'textPreview',
|
||||
'created',
|
||||
'author' => [
|
||||
'id',
|
||||
'fullName',
|
||||
'email',
|
||||
],
|
||||
'issue' => [
|
||||
'id',
|
||||
'idReadable',
|
||||
'project' => [
|
||||
'id',
|
||||
'shortName',
|
||||
],
|
||||
'summary',
|
||||
],
|
||||
],
|
||||
filters: [
|
||||
'issue' => [
|
||||
'required' => YoutrackFilterInterface::REQUIRED,
|
||||
],
|
||||
],
|
||||
resources: [
|
||||
'list' => new Resource(
|
||||
id: 'list',
|
||||
method: Resource::GET,
|
||||
path: '/api/issues/{issueID}/comments',
|
||||
description: 'Lists comments of issue.'
|
||||
),
|
||||
]
|
||||
)]
|
||||
class YoutrackComment extends YoutrackEntity {
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
|
||||
|
||||
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||
use RprtCli\Utils\TimeTrackingServices\Resource;
|
||||
|
||||
/**
|
||||
* https://www.jetbrains.com/help/youtrack/devportal/api-entity-Issue.html
|
||||
* https://www.jetbrains.com/help/youtrack/devportal/resource-api-issues.html
|
||||
*/
|
||||
#[EntityDefinition(
|
||||
id: 'issue',
|
||||
provider: 'youtrack_rest_api',
|
||||
name: 'Issue',
|
||||
fields: [
|
||||
'id',
|
||||
'idReadable',
|
||||
'project' => [
|
||||
'id',
|
||||
'shortName',
|
||||
],
|
||||
'summary',
|
||||
],
|
||||
filters: [
|
||||
'state',
|
||||
'project',
|
||||
'assignee',
|
||||
],
|
||||
resources: [
|
||||
'list' => new Resource(
|
||||
id: 'list',
|
||||
method: Resource::GET,
|
||||
path: '/api/issues',
|
||||
description: 'Lists issues.'
|
||||
),
|
||||
]
|
||||
)]
|
||||
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',
|
||||
];
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
|
||||
|
||||
use RprtCli\Utils\TimeTrackingServices\Resource;
|
||||
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||
|
||||
/**
|
||||
* https://www.jetbrains.com/help/youtrack/devportal/resource-api-admin-projects.html
|
||||
*/
|
||||
#[EntityDefinition(
|
||||
id: 'project',
|
||||
provider: 'youtrack_rest_api',
|
||||
name: 'Youtrack Project',
|
||||
fields: [
|
||||
'id',
|
||||
'shortName',
|
||||
'description',
|
||||
'leader' => ['id', 'fullName'],
|
||||
],
|
||||
filters: [],
|
||||
resources: [
|
||||
'list' => new Resource(
|
||||
id: 'list',
|
||||
method: Resource::GET,
|
||||
path: '/api/admin/projects',
|
||||
description: 'List projects'
|
||||
),
|
||||
'read' => new Resource(
|
||||
id: 'read',
|
||||
method: Resource::GET,
|
||||
path: '/api/admin/projects/{projectId}',
|
||||
description: 'Sprecific project'
|
||||
),
|
||||
]
|
||||
)]
|
||||
class YoutrackProject extends YoutrackEntity {
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntityTypes;
|
||||
|
||||
use RprtCli\Utils\TimeTrackingServices\Resource;
|
||||
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackEntity;
|
||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||
|
||||
/**
|
||||
* https://www.jetbrains.com/help/youtrack/devportal/api-entity-IssueWorkItem.html
|
||||
* https://www.jetbrains.com/help/youtrack/devportal/resource-api-workItems.html
|
||||
*/
|
||||
#[EntityDefinition(
|
||||
id: 'work_item',
|
||||
provider: 'youtrack_rest_api',
|
||||
name: 'Issue Work Item',
|
||||
fields: [
|
||||
'id',
|
||||
'author' => [
|
||||
'id',
|
||||
'fullName',
|
||||
'email'
|
||||
],
|
||||
'text',
|
||||
'type',
|
||||
'duration' => [
|
||||
'id',
|
||||
'minutes',
|
||||
'presentation'
|
||||
],
|
||||
'date',
|
||||
'created',
|
||||
'issue' => [
|
||||
'id',
|
||||
'idReadable',
|
||||
'project' => [
|
||||
'id',
|
||||
'shortName'
|
||||
],
|
||||
'summary',
|
||||
],
|
||||
],
|
||||
filters: [
|
||||
'project',
|
||||
'issue',
|
||||
'user',
|
||||
'date',
|
||||
],
|
||||
resources: [
|
||||
'list' => new Resource(
|
||||
id: 'list',
|
||||
method: Resource::GET,
|
||||
path: '/api/workItems',
|
||||
description: 'List workItems'
|
||||
),
|
||||
]
|
||||
)]
|
||||
class YoutrackWorkItem extends YoutrackEntity {
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
|
||||
|
||||
use Cog\YouTrack\Rest\Client\YouTrackClient;
|
||||
use Cog\YouTrack\Rest\Authorizer\TokenAuthorizer;
|
||||
use Cog\YouTrack\Rest\HttpClient\GuzzleHttpClient;
|
||||
use RprtCli\Utils\Configuration\ConfigurationInterface;
|
||||
|
||||
|
||||
/**
|
||||
* YouTrack php sdk abstraction service.
|
||||
*
|
||||
* Returns the client that takes care of authentication.
|
||||
*/
|
||||
class YoutrackRestApiClient {
|
||||
|
||||
protected ConfigurationInterface $config;
|
||||
protected YouTrackClient $client;
|
||||
|
||||
// config
|
||||
public function __construct(ConfigurationInterface $config) {
|
||||
$this->config = $config;
|
||||
$this->client = $this->createYoutrackClient();
|
||||
}
|
||||
|
||||
protected function createYoutrackClient() :YouTrackClient {
|
||||
// Could this all go into __construct method?
|
||||
$apiBaseUri = $this->config->get('tracking_service.youtrack.base_url', false);
|
||||
$apiToken = $this->config->get('tracking_service.youtrack.auth_token', false);
|
||||
// Instantiate PSR-7 HTTP Client
|
||||
$psrHttpClient = new \GuzzleHttp\Client([
|
||||
'base_uri' => $apiBaseUri,
|
||||
'debug' => true,
|
||||
]);
|
||||
// Instantiate YouTrack API HTTP Client
|
||||
$httpClient = new GuzzleHttpClient($psrHttpClient);
|
||||
// Instantiate YouTrack API Token Authorizer
|
||||
$authorizer = new TokenAuthorizer($apiToken);
|
||||
// Instantiate YouTrack API Client
|
||||
$client = new YouTrackClient($httpClient, $authorizer);
|
||||
return $client;
|
||||
}
|
||||
|
||||
public function getClient() :YouTrackClient {
|
||||
return $this->client;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Utils\TimeTrackingServices\YoutrackRestApi;
|
||||
|
||||
use RprtCli\Utils\TimeTrackingServices\EntityManagerInterface;
|
||||
use RprtCli\Utils\TimeTrackingServices\YoutrackRestApi\YoutrackRestApiClient;
|
||||
|
||||
class YoutrackRestApiRequestBuilder {
|
||||
|
||||
protected EntityManagerInterface $entityManager;
|
||||
protected YoutrackRestApiClient $clientFactory;
|
||||
|
||||
public function __construct(EntityManagerInterface $entity_manager, YoutrackRestApiClient $clientFactory) {
|
||||
$this->entityManager = $entity_manager;
|
||||
$this->clientFactory = $clientFactory;
|
||||
// EntityMangerInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that creates path to youtrack entity resource.
|
||||
*
|
||||
* Examples:
|
||||
* list issue => GET '/api/issues?fields=id,idReadable,summary,..'
|
||||
* create issue => POST '/api/issues'
|
||||
* but create action is only allowed on worktItems.
|
||||
* list work_item => GET '/api/workItem?fields=...'
|
||||
*
|
||||
* First impression seems simple. We need path and method.
|
||||
* What bothers me whether it would be better to create a new attribute
|
||||
* wehere these "mappings" would live: allowed actions and then method and path.
|
||||
*
|
||||
* @TODO Add filter system?
|
||||
*
|
||||
* @param string $action
|
||||
* Name of the action: list, create, read, update, delete.
|
||||
* @param string $entity_id
|
||||
* Name of the entity id (project, issue, comment, work_item).
|
||||
*
|
||||
* @return ?string
|
||||
* Path to the resource.
|
||||
*/
|
||||
public function buildPath(string $action, string $entity_id) :?string {
|
||||
$defintion = $this->entityManager->getDefinition($entity_id);
|
||||
if (!isset($defintion['definition'])) {
|
||||
// Missing entity definition!
|
||||
return NULL;
|
||||
}
|
||||
else {
|
||||
$defintion = $defintion['definition'];
|
||||
}
|
||||
$fields = $definition->getFields();
|
||||
$fieldsQuery = $this->fieldsQuery($fields);
|
||||
/** @var RprtCli\Utils\TimeTrackingServices\Resource $resource */
|
||||
$resource = $defintion->getResource($action);
|
||||
if (!$resource) {
|
||||
// Missing resource!
|
||||
return NULL;
|
||||
}
|
||||
$path = $resource->getPath();
|
||||
// @TODO add query, create client request.
|
||||
// @see https://github.com/cybercog/youtrack-rest-php/blob/eb0315133d1d3d161da23d26537201afb253dec9/src/Client/YouTrackClient.php#L89
|
||||
if ($fieldsQuery) {
|
||||
$path .= '?' . $fieldsQuery;
|
||||
}
|
||||
// @TODO create request and handle request.
|
||||
|
||||
}
|
||||
|
||||
protected function fieldsQuery(array $fields) :?string {
|
||||
$query = implode(',', $this->stringifyFields($fields));
|
||||
if (!empty($query)) {
|
||||
return "fields=${query}";
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function stringifyFields(array $fields) :array {
|
||||
foreach ($fields as $key => &$field) {
|
||||
if (is_array($field)) {
|
||||
$field = $key . '(' . implode(',', $this->stringifyFields($field)) . ')';
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
}
|
|
@ -6,63 +6,81 @@ declare(strict_types=1);
|
|||
|
||||
namespace RprtCli\Utils\TimeTrackingServices;
|
||||
|
||||
use Exception;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use RprtCli\Utils\Configuration\ConfigurationInterface;
|
||||
use Throwable;
|
||||
|
||||
use function array_combine;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function file_put_contents;
|
||||
use function floor;
|
||||
use function json_decode;
|
||||
use function microtime;
|
||||
use function readline;
|
||||
use function sleep;
|
||||
use function sys_get_temp_dir;
|
||||
use function tempnam;
|
||||
use function trim;
|
||||
use function var_dump;
|
||||
|
||||
class YoutrackService implements YoutrackInterface
|
||||
{
|
||||
protected string $ytToken;
|
||||
protected string $ytBaseUrl;
|
||||
|
||||
protected $ytToken;
|
||||
protected $ytBaseUrl;
|
||||
protected ConfigurationInterface $config;
|
||||
|
||||
protected $config;
|
||||
protected ClientInterface $httpClient;
|
||||
|
||||
protected $httpClient;
|
||||
|
||||
protected $report_id;
|
||||
protected string $report_id;
|
||||
protected ?string $reportName = null;
|
||||
|
||||
public function __construct(ConfigurationInterface $config, ClientInterface $http_client)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->config = $config;
|
||||
$this->httpClient = $http_client;
|
||||
}
|
||||
|
||||
public function testYoutrackapi(): ?string
|
||||
public function testYoutrackapi() : ?string
|
||||
{
|
||||
// Get base url from config or add input.
|
||||
// Get token or add input.
|
||||
$path = 'youtrack/api/admin/users/me';
|
||||
$yt_url = $this->getYtUrl($path);
|
||||
$yt_token = $this->getYtToken();
|
||||
$query = ['fields' => 'id,email,fullName'];
|
||||
$headers = [
|
||||
$path = 'youtrack/api/admin/users/me';
|
||||
$yt_url = $this->getYtUrl($path);
|
||||
$yt_token = $this->getYtToken();
|
||||
$query = ['fields' => 'id,email,fullName'];
|
||||
$headers = [
|
||||
"Authorization" => "Bearer $yt_token",
|
||||
'Cache-Control' =>'no-cache',
|
||||
'Cache-Control' => 'no-cache',
|
||||
];
|
||||
$me_response = $this->httpClient->request('GET', $yt_url, [
|
||||
'query' => $query,
|
||||
'headers' => $headers
|
||||
'query' => $query,
|
||||
'headers' => $headers,
|
||||
]);
|
||||
$test = (string) $me_response->getBody()->getContents();
|
||||
$me_json = (array) json_decode($test);
|
||||
$test = (string) $me_response->getBody()->getContents();
|
||||
$me_json = (array) json_decode($test);
|
||||
if ($me_json && isset($me_json['fullName'])) {
|
||||
return $me_json['fullName'];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function requestYoutrackPath(string $path, array $query) {
|
||||
$yt_url = $this->getYtUrl($path);
|
||||
protected function requestYoutrackPath(string $path, array $query)
|
||||
{
|
||||
$yt_url = $this->getYtUrl($path);
|
||||
$headers = $this->getHeaders();
|
||||
return $this->httpClient->request('GET', $yt_url, [
|
||||
'query' => $query,
|
||||
'headers' => $headers
|
||||
'query' => $query,
|
||||
'headers' => $headers,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaders() {
|
||||
protected function getHeaders()
|
||||
{
|
||||
$yt_token = $this->getYtToken();
|
||||
return [
|
||||
"Authorization" => "Bearer $yt_token",
|
||||
|
@ -70,7 +88,7 @@ class YoutrackService implements YoutrackInterface
|
|||
];
|
||||
}
|
||||
|
||||
public function getReportId(): ?string
|
||||
public function getReportId() : ?string
|
||||
{
|
||||
// --report option value should take precedence.
|
||||
// @TODO error handling.
|
||||
|
@ -78,132 +96,154 @@ class YoutrackService implements YoutrackInterface
|
|||
return $this->report_id;
|
||||
}
|
||||
$yt_report_id = $this->config->get('tracking_service.youtrack.report_id');
|
||||
if (!$yt_report_id) {
|
||||
if (! $yt_report_id) {
|
||||
$yt_report_id = readline('Enter the report id: ');
|
||||
}
|
||||
return $yt_report_id;
|
||||
}
|
||||
|
||||
public function setReportId(string $report_id) :void {
|
||||
public function setReportId(string $report_id) : void
|
||||
{
|
||||
$this->report_id = $report_id;
|
||||
}
|
||||
|
||||
public function downloadReport(string $report_id): ?string
|
||||
public function setReportName(?string $report_name = null) : void
|
||||
{
|
||||
$path = "youtrack/api/reports/$report_id/export/csv";
|
||||
$query = ['$top' => -1];
|
||||
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";
|
||||
$query = ['$top' => -1];
|
||||
$yt_token = $this->getYtToken();
|
||||
$headers = [
|
||||
$headers = [
|
||||
'Accept' => 'application/json, text/plain, */*',
|
||||
// 'Accept-Encoding' => 'gzip, deflate, br',
|
||||
// 'Connection' => 'keep-alive',
|
||||
'Accept-Language' => 'en-US,en;q=0.5',
|
||||
"Authorization" => "Bearer $yt_token",
|
||||
"Authorization" => "Bearer $yt_token",
|
||||
];
|
||||
try {
|
||||
$csv_response = $this->httpClient->request('GET', $this->getYtUrl($path), [
|
||||
'headers' => $headers,
|
||||
'query' => $query,
|
||||
'query' => $query,
|
||||
]);
|
||||
// Write csv response test into temporary file.
|
||||
$csv_file = tempnam(sys_get_temp_dir(), "rprt-csv-{$report_id}");
|
||||
file_put_contents($csv_file, $csv_response->getBody());
|
||||
return $csv_file;
|
||||
}
|
||||
catch (ClientException $e) {
|
||||
} catch (ClientException $e) {
|
||||
$status = $e->getResponse()->getStatusCode();
|
||||
if ($status == 409) {
|
||||
if ($status === 409) {
|
||||
sleep(3);
|
||||
// @TODO Find a way to break of of loop if necessary!
|
||||
var_dump("409 response status during download of report {$report_id}. Sleep 3 and try again.");
|
||||
return $this->downloadReport($report_id);
|
||||
}
|
||||
}
|
||||
catch (\Throwable $t) {
|
||||
} catch (Throwable $t) {
|
||||
$status = $t->getMessage();
|
||||
var_dump($status);
|
||||
}
|
||||
throw new \Exception("Unable to download report {$report_id}!");
|
||||
throw new Exception("Unable to download report {$report_id}!");
|
||||
}
|
||||
|
||||
protected function getYtToken(): string {
|
||||
protected function getYtToken() : string
|
||||
{
|
||||
if (isset($this->ytToken)) {
|
||||
return $this->ytToken;
|
||||
}
|
||||
$yt_token = $this->config->get('tracking_service.youtrack.auth_token', FALSE);
|
||||
if (!$yt_token) {
|
||||
$yt_token = $this->config->get('tracking_service.youtrack.auth_token', false);
|
||||
if (! $yt_token) {
|
||||
$yt_token = readline('Enter your youtrack authentication token: ');
|
||||
}
|
||||
return $yt_token;
|
||||
}
|
||||
|
||||
public function setYtToken(string $token): void {
|
||||
public function setYtToken(string $token) : void
|
||||
{
|
||||
$this->ytToken = $token;
|
||||
}
|
||||
|
||||
protected function getYtUrl(string $path = ''): ?string {
|
||||
protected function getYtUrl(string $path = '') : ?string
|
||||
{
|
||||
if (isset($this->ytBaseUrl)) {
|
||||
$yt_base_url = $this->ytBaseUrl;
|
||||
}
|
||||
else {
|
||||
$yt_base_url = $this->config->get('tracking_service.youtrack.base_url', FALSE);
|
||||
} else {
|
||||
$yt_base_url = $this->config->get('tracking_service.youtrack.base_url', false);
|
||||
}
|
||||
if (empty($yt_base_url)) {
|
||||
$yt_base_url = readline('Enter base url for of the youtrack service: ');
|
||||
}
|
||||
if (!empty($path)) {
|
||||
$yt_base_url = $yt_base_url . '/' . trim($path, '/');
|
||||
if (! empty($path)) {
|
||||
$yt_base_url .= '/' . trim($path, '/');
|
||||
}
|
||||
return $yt_base_url;
|
||||
}
|
||||
|
||||
public function setYtUrl(string $base_url) {
|
||||
public function setYtUrl(string $base_url)
|
||||
{
|
||||
$this->ytBaseUrl = $base_url;
|
||||
}
|
||||
|
||||
public function listReports() {
|
||||
public function listReports() : array
|
||||
{
|
||||
// Now filter results by own = true;
|
||||
$url = '/youtrack/api/reports';
|
||||
$query = [
|
||||
'$top' => -1,
|
||||
$url = '/youtrack/api/reports';
|
||||
$query = [
|
||||
'$top' => -1,
|
||||
'fields' => 'id,name,own,owner(login,name)',
|
||||
];
|
||||
$response = $this->requestYoutrackPath($url, $query);
|
||||
$body = (string) $response->getBody()->getContents();
|
||||
$body_json = (array) json_decode($body);
|
||||
$response = $this->requestYoutrackPath($url, $query);
|
||||
$body = (string) $response->getBody()->getContents();
|
||||
$body_json = (array) json_decode($body);
|
||||
$my_reports = array_filter($body_json, fn($report) => $report->own);
|
||||
$reports = array_combine(
|
||||
$reports = array_combine(
|
||||
array_map(fn($r) => $r->id, $my_reports),
|
||||
array_map(fn ($r) => $r->name, $my_reports)
|
||||
);
|
||||
return $reports;
|
||||
}
|
||||
|
||||
public function clearReportCache(string $report_id) :int {
|
||||
$path = "/youtrack/api/reports/${report_id}/status";
|
||||
$query = [
|
||||
public function clearReportCache(string $report_id) : int
|
||||
{
|
||||
$path = "/youtrack/api/reports/${report_id}/status";
|
||||
$query = [
|
||||
'$top' => -1,
|
||||
'fields' => 'calculationInProgress,error(id),errorMessage,isOutdated,lastCalculated,progress,wikifiedErrorMessage'
|
||||
// phpcs:ignore
|
||||
'fields' => 'calculationInProgress,error(id),errorMessage,isOutdated,lastCalculated,progress,wikifiedErrorMessage',
|
||||
];
|
||||
$post = [
|
||||
'lastCalculated' => floor(microtime(true) * 1000),
|
||||
$post = [
|
||||
'lastCalculated' => floor(microtime(true) * 1000),
|
||||
'calculationInProgress' => true,
|
||||
'wikifiedErrorMessage' => '',
|
||||
'isOutdated' => false,
|
||||
'progress' => -1,
|
||||
'error' => null,
|
||||
'errorMessage' => null,
|
||||
'$type' => 'ReportStatus'
|
||||
'wikifiedErrorMessage' => '',
|
||||
'isOutdated' => false,
|
||||
'progress' => -1,
|
||||
'error' => null,
|
||||
'errorMessage' => null,
|
||||
'$type' => 'ReportStatus',
|
||||
];
|
||||
$yt_url = $this->getYtUrl($path);
|
||||
$yt_url = $this->getYtUrl($path);
|
||||
$response = $this->httpClient->request('POST', $yt_url, [
|
||||
'query' => $query,
|
||||
'query' => $query,
|
||||
'headers' => $this->getHeaders(),
|
||||
'json' => $post,
|
||||
'json' => $post,
|
||||
]);
|
||||
// $body = (string) $response->getBody()->getContents();
|
||||
// var_dump($body);
|
||||
return $response->getStatusCode();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace RprtCli\ValueObjects;
|
||||
|
||||
class Expenses implements ExpensesInterface {
|
||||
|
||||
class Expenses implements ExpensesInterface
|
||||
{
|
||||
/**
|
||||
* Expenses in current currency.
|
||||
*/
|
||||
|
@ -16,17 +16,18 @@ class Expenses implements ExpensesInterface {
|
|||
*/
|
||||
private string $name;
|
||||
|
||||
public function __construct(string $name, float $value) {
|
||||
$this->name = $name;
|
||||
public function __construct(string $name, float $value)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public function getValue(): float
|
||||
public function getValue() : float
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
public function getName() : string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace RprtCli\ValueObjects;
|
||||
|
||||
interface ExpensesInterface extends InvoiceElementInterface {
|
||||
|
||||
public function getValue() :float;
|
||||
|
||||
interface ExpensesInterface extends InvoiceElementInterface
|
||||
{
|
||||
public function getValue() : float;
|
||||
}
|
||||
|
|
|
@ -7,11 +7,10 @@ namespace RprtCli\ValueObjects;
|
|||
/**
|
||||
* Main interface for invoice elements.
|
||||
*/
|
||||
interface InvoiceElementInterface {
|
||||
|
||||
interface InvoiceElementInterface
|
||||
{
|
||||
/**
|
||||
* Project or expenses name.
|
||||
*/
|
||||
public function getName() :string;
|
||||
|
||||
public function getName() : string;
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace RprtCli\ValueObjects;
|
||||
|
||||
class WorkInvoiceElement implements WorkInvoiceElementInterface {
|
||||
|
||||
class WorkInvoiceElement implements WorkInvoiceElementInterface
|
||||
{
|
||||
private float $time;
|
||||
|
||||
/**
|
||||
|
@ -13,17 +13,19 @@ class WorkInvoiceElement implements WorkInvoiceElementInterface {
|
|||
*/
|
||||
private string $name;
|
||||
|
||||
public function __construct(string $name, float $time) {
|
||||
public function __construct(string $name, float $time)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->time = $time;
|
||||
}
|
||||
|
||||
public function getTime() :float {
|
||||
public function getTime() : float
|
||||
{
|
||||
return $this->time;
|
||||
}
|
||||
|
||||
public function getName() :string {
|
||||
public function getName() : string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@ declare(strict_types=1);
|
|||
|
||||
namespace RprtCli\ValueObjects;
|
||||
|
||||
interface WorkInvoiceElementInterface extends InvoiceElementInterface {
|
||||
|
||||
public function getTime() :float ;
|
||||
interface WorkInvoiceElementInterface extends InvoiceElementInterface
|
||||
{
|
||||
public function getTime() : float;
|
||||
|
||||
/**
|
||||
* Get project name.
|
||||
*/
|
||||
public function getName() :string ;
|
||||
public function getName() : string;
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Tests\Unit\YoutrackRestApi;
|
||||
|
||||
use DI\ContainerBuilder;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RprtCli\Utils\TimeTrackingServices\EntityDefinition;
|
||||
use RprtCli\Utils\TimeTrackingServices\EntityInterface;
|
||||
|
||||
/**
|
||||
* 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_id) {
|
||||
$this->assertArrayHasKey($entity_id, $definitions, 'Check that key exists in entity types definitions.');
|
||||
var_dump($definitions[$entity_id]);
|
||||
|
||||
$this->assertInstanceOf(EntityDefinition::class, $definitions[$entity_id]['definition'], 'Check that definiton is of type entity definition');
|
||||
$definition = $entityManagerService->getDefinition($entity_id);
|
||||
$entity = $entityManagerService->createInstance($entity_id, []);
|
||||
$this->assertArrayHasKey('definition', $definition);
|
||||
$this->assertArrayHasKey('class', $definition);
|
||||
$this->assertInstanceOf(EntityDefinition::class, $definition['definition']);
|
||||
$this->assertInstanceOf(EntityInterface::class, $entity);
|
||||
}
|
||||
// list method returns definitions: project, issue, worktItem
|
||||
// Check one definition, compare.
|
||||
// Test creating the instance
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# YoutrackRestApi Unit tests
|
||||
|
||||
1. Check that EntityManager lists defined entity types.
|
||||
2. Check that EntityTypeManager properly creates entity.
|
|
@ -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
|
|
|
@ -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
|
|
@ -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.
|
|
@ -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>
|
|
@ -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 |
|
||||
+----------------+--------------------------------------------------------------+------+------------+
|
|
@ -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'
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$dependencies = require_once __DIR__ . '/../dependencies.php';
|
||||
|
||||
$dependencies['config.path'] = __DIR__ . '/data/';
|
||||
|
||||
return $dependencies;
|
35
todo.txt
35
todo.txt
|
@ -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,36 @@ 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: (start of 2023)
|
||||
- youtrack rest api:
|
||||
- resources class and parameter in EntityDefinition
|
||||
- compose fields method
|
||||
- create filter system
|
||||
- connect filter system to youtrack rest api command
|
||||
|
||||
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 +49,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
|
||||
|
|
Loading…
Reference in New Issue