commit fe6255deb1d372ffccf3bd046c88ea361d4e8059 Author: an.nechaev Date: Wed May 27 17:19:12 2026 +0400 Реализация выгрузки задач по командам diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..226804e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +/.idea/ +/local/ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e7a52b3 --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "name": "nechaev/reports", + "type": "project", + "require": { + "guzzlehttp/guzzle": "^7.10", + "ext-json": "*" + }, + "autoload": { + "psr-4": { + "Nechaev\\Reports\\": "src/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..e82ceb2 --- /dev/null +++ b/composer.lock @@ -0,0 +1,621 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "b8dad7f6207ecd5e6111703a662d2426", + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "7.10.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "47ba23c7a55247e2e1b7407aca90e9bbed0d9d86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/47ba23c7a55247e2e1b7407aca90e9bbed0d9d86", + "reference": "47ba23c7a55247e2e1b7407aca90e9bbed0d9d86", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.3.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "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": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "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://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.3" + }, + "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": "2026-05-20T22:59:19+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/09e8a212562fb1fb6a512c4156ed71525969d6c2", + "reference": "09e8a212562fb1fb6a512c4156ed71525969d6c2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "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": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.4.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-05-20T22:57:30+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.10.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "73ab136360b5dfd858006eae9795e8fe43c80361" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/73ab136360b5dfd858006eae9795e8fe43c80361", + "reference": "73ab136360b5dfd858006eae9795e8fe43c80361", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "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": "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://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.10.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-05-20T09:27:36+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.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": "https://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" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "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": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/configs/config.php b/configs/config.php new file mode 100644 index 0000000..634ba80 --- /dev/null +++ b/configs/config.php @@ -0,0 +1,13 @@ + [ + 'url' => 'https://your-domain.atlassian.net', // Ваш домен Jira + 'email' => 'your-email@example.com', // Ваш email + 'apiToken' => 'your-api-token', // Ваш API токен + ], + 'export' => [ + 'maxResults' => 1000, // Максимальное количество задач за один запрос + ], +]; diff --git a/init.php b/init.php new file mode 100644 index 0000000..c2ba33a --- /dev/null +++ b/init.php @@ -0,0 +1,96 @@ + [ + 'projects' => 'TS', + 'executors' => [ + 't.vafin' => RolesEnum::BACKEND, + 'v.kulikov' => RolesEnum::BACKEND, + 'v.yuzkiv' => RolesEnum::BACKEND, + 'e.kozyrev' => RolesEnum::BACKEND, + 's.kudryavcev' => RolesEnum::BACKEND, + 'a.khokhryakov' => RolesEnum::BACKEND, + 'v.kozachenko' => RolesEnum::BACKEND, + 'k.benner' => RolesEnum::FRONTEND, + 'p.aksenov' => RolesEnum::FRONTEND, + 'ar.markov' => RolesEnum::FRONTEND, + 'i.zhurkov' => RolesEnum::QA, + 'e.zyumkina' => RolesEnum::QA, + 'a.nelyubov' => RolesEnum::QA, + 'n.merkulov' => RolesEnum::QA, + 'o.kirichik' => RolesEnum::ANALYTIC, + 't.baskakova' => RolesEnum::ANALYTIC, + 'm.larin' => RolesEnum::ANALYTIC, + 'k.goshko' => RolesEnum::DESIGNER, + 'e.kazakova' => RolesEnum::DESIGNER, + ], + ], + 'altis' => [ + 'projects' => 'b2b-center', + 'executors' => [ + 'd.ivashov' => RolesEnum::BACKEND, + 'm.boytsova' => RolesEnum::ANALYTIC, + 'm.shabanova' => RolesEnum::ANALYTIC, + 'm.azarenkova' => RolesEnum::DESIGNER, + 'a.taranenko' => RolesEnum::BACKEND, + 'd.zhugel' => RolesEnum::BACKEND, + 'e.podmarev' => RolesEnum::BACKEND, + 'e.sokolova' => RolesEnum::BACKEND, + 'd.pavlov' => RolesEnum::BACKEND, + 'e.paranyuk' => RolesEnum::FRONTEND, + 'a.golubev' => RolesEnum::FRONTEND, + 'a.donnik' => RolesEnum::QA, + 'e.borkunov' => RolesEnum::QA, + 'd.androsova' => RolesEnum::QA, + 'e.plotinskaya' => RolesEnum::QA, + + 'a.kirin' => RolesEnum::BACKEND, + 'v.lushnikov' => RolesEnum::ANALYTIC, + 'a.dozhun' => RolesEnum::ANALYTIC, + 'd.borisov' => RolesEnum::BACKEND, + 'r.schekin' => RolesEnum::BACKEND, + 'a.fatov' => RolesEnum::BACKEND, + 'm.arzamastsev' => RolesEnum::BACKEND, + 'a.kuzin' => RolesEnum::BACKEND, + 'i.grigorev' => RolesEnum::FRONTEND, + 'a.drozdov' => RolesEnum::FRONTEND, + 'n.pilyavets' => RolesEnum::FRONTEND, + 'r.novikov' => RolesEnum::QA, + 'k.khayrullina' => RolesEnum::QA, + 'o.rudakov' => RolesEnum::QA, + 'e.motygullin' => RolesEnum::QA, + ] + ], + 'srm' => [ + 'projects' => '"SRM (Мои поставщики)", CRM, Bryansk', + 'executors' => [ + 'a.abramchuk' => RolesEnum::DESIGNER, + 'i.chikurov' => RolesEnum::ANALYTIC, + 'a.belyavcova' => RolesEnum::ANALYTIC, + 'a.afanasenkov' => RolesEnum::BACKEND, + 'v.evstegneev' => RolesEnum::BACKEND, + 'm.epikhin' => RolesEnum::BACKEND, + 'k.suvorov' => RolesEnum::BACKEND, + 'v.utyuganov' => RolesEnum::BACKEND, + 'a.shitov' => RolesEnum::BACKEND, + 'a.filatov' => RolesEnum::BACKEND, + 'i.monastyrskiy' => RolesEnum::FRONTEND, + 'd.nasyrov' => RolesEnum::FRONTEND, + 'm.miklushov' => RolesEnum::QA, + 'd.mikhaylov' => RolesEnum::QA, + ], + ], +]; diff --git a/src/DateHelper.php b/src/DateHelper.php new file mode 100644 index 0000000..aca32d6 --- /dev/null +++ b/src/DateHelper.php @@ -0,0 +1,67 @@ +format('Y-m-d'); + // Проверяем, не является ли день субботой или воскресеньем + $isWeekend = in_array($dayStr, self::getHolidays()) || in_array($day->format('N'), [6, 7]); + if (!$isWeekend) { + $workingDays++; + } + } + + return $workingDays; + } + + public static function getHolidays(): array { + return [ + '2026-01-01', // Новый год + '2026-01-02', // Новогодние каникулы + '2026-01-03', // Новогодние каникулы + '2026-01-04', // Новогодние каникулы + '2026-01-05', // Новогодние каникулы + '2026-01-06', // Новогодние каникулы + '2026-01-07', // Рождество Христово + '2026-01-08', // Новогодние каникулы + '2026-01-09', // Перенос выходного с 3 января + '2026-01-11', // Перенос выходного с 4 января + '2026-02-23', // День защитника Отечества + '2026-03-09', // Международный женский день + '2026-05-01', // Праздник Весны и Труда + '2026-05-11', // День Победы + '2026-06-12', // День России + '2026-11-04', // День народного единства + '2026-12-31' // Перенос выходного с 4 января + ]; + } +} diff --git a/src/RolesEnum.php b/src/RolesEnum.php new file mode 100644 index 0000000..3baa58b --- /dev/null +++ b/src/RolesEnum.php @@ -0,0 +1,18 @@ + + */ +class StatusHelper +{ + public static function getInWorkStatuses(): array { + return [ + 'Разработка', + "В работе", + ]; + } + + public static function getInReviewStatuses(): array { + return [ + 'On code-review', + "Review", + ]; + } + + public static function getInTestingStatuses(): array { + return [ + "Тестирование", + ]; + } + + /** + * @param array $data ["status" => "data"] + * @param string[] $statuses + * @return mixed|null + */ + public static function getDataByStatus(array $data, array $statuses) { + foreach ($statuses as $status) { + if (isset($data[$status])) { + return $data[$status]; + } + } + + return null; + } +} diff --git a/src/Task.php b/src/Task.php new file mode 100644 index 0000000..2b47757 --- /dev/null +++ b/src/Task.php @@ -0,0 +1,83 @@ +type = $type; + $this->key = $key; + $this->description = $description; + $this->assignee = $assignee; + $this->estimate = $estimate ?? 0; + $this->totalTime = $totalTime; + $this->dateTakeInWork = $dateTakeInWork; + $this->resolutionDate = $resolutionDate; + + $this->countReview = $countReview ?? 0; + $this->countTest = $countTest ?? 0; + + $this->leadTime = DateHelper::countWorkingDays($dateTakeInWork, $resolutionDate); + $this->recast = ($this->estimate == 0) ? 0: round($totalTime/$estimate,4)-1; + // totalTime в секундах, leadTime в днях + $this->efficiency = round(($totalTime/3600)/$this->leadTime*8, 2); + } + + public function toCSV(): string { + return implode(";", [ + $this->type, + $this->key, + $this->description, + $this->assignee, + new DateTime($this->dateTakeInWork)->format('Y-m-d'), + new DateTime($this->resolutionDate)->format('Y-m-d'), + $this->leadTime, + round($this->estimate/3600,2), // переводим в часы + round($this->totalTime/3600, 2), // переводим в часы + $this->recast, + $this->efficiency, + $this->countReview, + $this->countTest, + ]); + } + + public function __toString(): string { + return $this->toCSV()."\n"; + } +} diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 0000000..6360dda --- /dev/null +++ b/src/functions.php @@ -0,0 +1,86 @@ + [3,"2026-04-15T17:00:49.000+0300"], 'Done' => [1,"2026-04-15T17:00:49.000+0300"]] + * @throws Exception + */ +function getStatusForIssue(string $issueKey, Client $client): array { + $statusCounts = []; + $statusDates = []; + + try { + $response = $client->get("rest/api/2/issue/{$issueKey}", [ + 'query' => [ + 'expand' => 'changelog', + ], + ]); + + $data = json_decode($response->getBody(), true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception("Ошибка декодирования JSON для задачи {$issueKey}."); + } + + // Обрабатываем текущую "пачку" записей истории + foreach ($data['changelog']['histories'] as $historyItem) { + foreach ($historyItem['items'] as $item) { + // Ищем только изменения поля 'status' + //var_dump($item); die(); + if ($item['field'] === 'status') { + $dateStr = $historyItem['created']; + + $statusName = $item['toString']; + if (!isset($statusCounts[$statusName])) { + $statusCounts[$statusName] = 0; + } + $statusCounts[$statusName]++; + if (!isset($statusDates[$statusName])) { + $statusDates[$statusName] = $dateStr; + } + } + } + } + + // Если вернули меньше, чем запрошено, или дошли до конца - выходим + } catch (GuzzleException $e) { + echo "⚠️ Ошибка при получении истории для {$issueKey}: " . $e->getMessage() . "\n"; + } + + return array_merge_recursive($statusCounts, $statusDates); +} + +function filterByResolution(array $data): array { + $result = []; + + foreach($data as $issue) { + if ($issue['fields']['resolution']['id'] == "1") { + $result[] = $issue; + } else { + echo "Выкинуто из отчета ".$issue["key"]." из-за статуса ".$issue['fields']['resolution']['id']."\n"; + } + } + + return $result; +} + + +/** + * @param string[] $executors + */ +function filterByExecutors(array $data, array $executors): array { + $result = []; + foreach($data as $issue) { + if (in_array($issue["fields"]["assignee"]["name"], $executors)) { + $result[] = $issue; + } else { + echo "Выкинуто из отчета ".$issue["key"]." из-за исполнителя ".$issue["fields"]["assignee"]["name"]."\n"; + } + } + return $result; +} diff --git a/src/jira_export.php b/src/jira_export.php new file mode 100644 index 0000000..0464d0f --- /dev/null +++ b/src/jira_export.php @@ -0,0 +1,142 @@ +global$config; #!/usr/bin/env php + $jiraUrl, + 'headers' => [ + 'Authorization' => 'Bearer ' . $apiToken, + 'Accept' => 'application/json', + ], +]); + +foreach ($teams as $team => $teamData) { + echo "Расчет для команды {$team}\n"; + $filename = "{$team}_tasks_{$startDate}_to_{$endDate}.csv"; + + // --- 3. Подготовка JQL-запроса --- + // Запрос ищет задачи, у которых статус изменился на "Done" в указанный период. + $jql = "project in (".$teamData['projects'].") AND issuetype in standardIssueTypes() AND issuetype not in (Epic, \"Admin work\") AND resolutiondate > \"{$startDate}\" AND resolutiondate < \"{$endDate}\""; + + $params = [ + 'jql' => $jql, + 'maxResults' => $maxResults, + 'fields' => 'issuetype,key,summary,assignee,resolution,aggregatetimeoriginalestimate,aggregateprogress,resolutiondate', + ]; + + try { + // --- 4. Обработка пагинации --- + // Если задач больше, чем maxResults, Jira вернет только часть. + // Мы будем запрашивать страницы до тех пор, пока не получим все. + $allIssues = []; + $startAt = 0; + $totalFetched = 0; + + do { + $response = $client->get('rest/api/2/search', [ + 'query' => $params + ['startAt' => $startAt] + ]); + + $data = json_decode($response->getBody(), true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception("Ошибка декодирования JSON-ответа от Jira."); + } + + $issues = $data['issues'] ?? []; + $allIssues = array_merge($allIssues, $issues); + + // Обновляем счетчики для следующей итерации + $totalFetched += count($issues); + $startAt += count($issues); + + echo "Получено задач: {$totalFetched} из {$data['total']}\n"; + + } while ($startAt < ($data['total'] ?? 0)); + + if (!empty($allIssues)) { + // Убираем дубликаты, там где изменения не требуются + $allIssues = filterByResolution($allIssues); + + // Убираем задачи, исполнители на которых не исполнители + $allIssues = filterByExecutors($allIssues, array_keys($teamData['executors'])); + + //file_put_contents($filename.".json", json_encode($allIssues, JSON_PRETTY_PRINT));die(); + + echo "Убрали лишнее, осталось ".count($allIssues)."\n"; + + $task = null; + foreach ($allIssues as $i => $issue) { + $statuses = getStatusForIssue($issue['key'], $client); + //$allIssues[$i]['countStatuses'] = $statuses; + $firstDateInStatus = array_map(function($value) { + return is_array($value) && count($value) > 1 ? $value[1] : null; + }, $statuses); + + $countStatus = array_map(function($value) { + return is_array($value) && count($value) > 1 ? $value[0] : null; + }, $statuses); + + $task = new \Nechaev\Reports\Task( + $issue["fields"]["issuetype"]["name"], + $issue['key'], + $issue["fields"]["summary"], + $issue["fields"]["assignee"]["name"], + $issue["fields"]["aggregatetimeoriginalestimate"], + $issue["fields"]["aggregateprogress"]["progress"], + StatusHelper::getDataByStatus($firstDateInStatus, StatusHelper::getInWorkStatuses()), + $issue["fields"]["resolutiondate"], + StatusHelper::getDataByStatus($countStatus, StatusHelper::getInReviewStatuses()), + StatusHelper::getDataByStatus($countStatus, StatusHelper::getInTestingStatuses()), + ); + file_put_contents($filename, $task, FILE_APPEND | LOCK_EX); + } + + + + // --- 5. Сохранение в файл --- + + + echo "Данные успешно сохранены в файл: {$filename}\n"; + } else { + echo "За указанный период завершенных задач не найдено.\n"; + } + + } catch (GuzzleException | Exception $e) { + echo "Ошибка: " . $e->getMessage() . "\n"; + } + +}