Реализация выгрузки задач по командам

master
an.nechaev 2026-05-27 17:19:12 +04:00
commit fe6255deb1
11 changed files with 1190 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/vendor/
/.idea/
/local/

13
composer.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "nechaev/reports",
"type": "project",
"require": {
"guzzlehttp/guzzle": "^7.10",
"ext-json": "*"
},
"autoload": {
"psr-4": {
"Nechaev\\Reports\\": "src/"
}
}
}

621
composer.lock generated Normal file
View File

@ -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"
}

13
configs/config.php Normal file
View File

@ -0,0 +1,13 @@
<?php
// config.php
return [
'jira' => [
'url' => 'https://your-domain.atlassian.net', // Ваш домен Jira
'email' => 'your-email@example.com', // Ваш email
'apiToken' => 'your-api-token', // Ваш API токен
],
'export' => [
'maxResults' => 1000, // Максимальное количество задач за один запрос
],
];

96
init.php Normal file
View File

@ -0,0 +1,96 @@
<?php
use Nechaev\Reports\RolesEnum;
// Подключаем автозагрузчик Composer и файл конфигурации
require __DIR__ . '/vendor/autoload.php';
// готовим конфиги
$baseConfig = require __DIR__ . '/configs/config.php';
$config = require __DIR__ . '/local/configs/config.php';
$config = array_replace_recursive($baseConfig, $config);
// готовим команды
$teams = [
'ts' => [
'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,
],
],
];

67
src/DateHelper.php Normal file
View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Nechaev\Reports;
use DateInterval;
use DatePeriod;
use DateTime;
/**
* Class DateHelper
* @package Nechaev\Reports
*/
class DateHelper
{
/**
* Подсчитывает количество рабочих дней между двумя датами.
*
* @param string $startDate Дата начала периода (формат 'Y-m-d').
* @param string $endDate Дата окончания периода (формат 'Y-m-d').
* @throws \DateMalformedStringException
*/
public static function countWorkingDays(string $startDate, string $endDate): int {
// Преобразуем строки дат в объекты DateTime
$start = new DateTime($startDate);
$end = new DateTime($endDate);
$interval = new DateInterval('P1D'); // Интервал — 1 день
$period = new DatePeriod($start, $interval, $end);
$workingDays = 0;
foreach ($period as $day) {
$dayStr = $day->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 января
];
}
}

18
src/RolesEnum.php Normal file
View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Nechaev\Reports;
/**
* Class RolesEnum
* @package Nechaev\Reports
*/
class RolesEnum
{
public const ANALYTIC = 'analytic';
public const BACKEND = 'backend';
public const DESIGNER = 'designer';
public const FRONTEND = 'frontend';
public const QA = 'qa';
}

48
src/StatusHelper.php Normal file
View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Nechaev\Reports;
/**
* Class StatusHelper
* @package Nechaev\Reports
* @author Bryansk_team <bryansk_team@b2b-center.ru>
*/
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;
}
}

83
src/Task.php Normal file
View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Nechaev\Reports;
use DateTime;
/**
* Class Task
* @package Nechaev\Reports
*/
class Task
{
private string $type; // Тип таски: bug, backend...
private string $key; // Номер задачи
private string $description; // Заголовок задачи
private string $assignee; // Исполнитель
private int $estimate; // Время оценки в секундах
private int $totalTime; // суммарное время потраченное на работу в секундах
private string $dateTakeInWork; // дата-время взятия в работу
private string $resolutionDate; // Дата-время завершения задачи
private int $leadTime; // System Lead Time в рабочих днях
private float $recast; // Переработка
private float $efficiency; // КПД
private int $countReview; // Количество заходов на ревью
private int $countTest; // Количество заходов на тест
public function __construct(
string $type,
string $key,
string $description,
string $assignee,
?int $estimate,
int $totalTime,
string $dateTakeInWork,
string $resolutionDate,
?int $countReview,
?int $countTest,
) {
$this->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";
}
}

86
src/functions.php Normal file
View File

@ -0,0 +1,86 @@
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
/**
* Получает количество раз нахождения задачи в каждом статусе.
*
* @param string $issueKey Ключ задачи (например, PROJ-123)
* @param Client $client Экземпляр клиента Guzzle с авторизацией
* @return array Ассоциативный массив ['In Progress' => [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;
}

142
src/jira_export.php Normal file
View File

@ -0,0 +1,142 @@
global$config; #!/usr/bin/env php
<?php
/**
* @var array $config
* @var array $teams
* @var string[] $holidays
*/
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Nechaev\Reports\StatusHelper;
require __DIR__ . '/../init.php';
require __DIR__ . '/functions.php';
// --- 1. Обработка параметров командной строки ---
$options = getopt('', ['start-date:', 'end-date:']);
if (empty($options['start-date']) || empty($options['end-date'])) {
echo "Использование: php jira_export.php --start-date=ГГГГ-ММ-ДД --end-date=ГГГГ-ММ-ДД\n";
exit(1);
}
$startDate = $options['start-date'];
$endDate = $options['end-date'];
// Валидация формата дат
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
die("Ошибка: даты должны быть в формате ГГГГ-ММ-ДД\n");
}
// Извлекаем настройки из конфига
$jiraUrl = $config['jira']['url'];
$apiToken = $config['jira']['apiToken'];
$maxResults = $config['export']['maxResults'];
// --- 2. Инициализация клиента Guzzle ---
$client = new Client([
'base_uri' => $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";
}
}