Create a workflow statistics page from Jira

This commit is contained in:
JOSEPH LE 2024-05-17 17:26:32 +07:00
parent 306dd78acb
commit 30cc1c3cac
18 changed files with 1645 additions and 43 deletions

View File

@ -0,0 +1,172 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Services\JiraService;
use GuzzleHttp\Promise\Utils;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Http\JsonResponse;
class JiraController extends Controller
{
use HasOrderByRequest;
use HasFilterRequest;
use HasSearchRequest;
protected $jiraService;
public function __construct(JiraService $jiraService)
{
$this->jiraService = $jiraService;
}
public function fetchAllIssues($startAt = 0, $maxResults = 50)
{
try {
$allIssues = [];
$projects = $this->jiraService->getAllProjects();
$promises = [];
foreach ($projects as $project) {
if ($project['key'] !== 'GCT') {
$promises[] = $this->jiraService->getIssuesAsync($project['key'], $startAt, $maxResults)
->then(function ($response) use ($project) {
$issues = [];
$data = json_decode($response->getBody()->getContents(), true);
foreach ($data['issues'] as $issue) {
$issues[] = [
'summary' => $issue['fields']['summary'] ?? null,
'desc' => $issue['fields']['description']['content'][0] ?? null,
'assignee' => $issue['fields']['assignee']['displayName'] ?? null,
'status' => $issue['fields']['status']['name'] ?? null,
'worklogs' => json_encode(array_map(function ($log) {
return [
'author' => $log['author']['displayName'] ?? null,
'timeSpent' => $log['timeSpent'] ?? null,
'started' => $log['started'] ?? null,
'updated' => $log['updated'] ?? null,
];
}, $issue['fields']['worklog']['worklogs'] ?? []), JSON_PRETTY_PRINT),
'originalEstimate' => isset($issue['fields']['timeoriginalestimate']) ? $issue['fields']['timeoriginalestimate'] / 60 / 60 : null,
'timeSpent' => isset($issue['fields']['timespent']) ? $issue['fields']['timespent'] / 60 / 60 : null
];
}
return ['project' => $project['name'], 'issues' => $issues];
});
}
}
$results = Utils::settle($promises)->wait();
foreach ($results as $result) {
if ($result['state'] === 'fulfilled') {
$allIssues[] = $result['value'];
} else {
// Handle the errors if necessary
\Log::error("Error fetching issues: " . $result['reason']->getMessage());
}
}
return response()->json($allIssues);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
public function getAllProject()
{
$projects = $this->jiraService->getAllProjects();
return response()->json([
'data' => $projects,
'status' => true
], 200);
}
public function fetchIssuesByProject(Request $request)
{
$project = ['key'=>$request->key, 'name'=> $request->name];
$allIssues = [];
if ($project['key'] !== 'GCT') {
$startAt = 0;
$issues = [];
$total = 0;
$issueLength = 0;
$checked = true;
// while ($checked) {
$response = $this->jiraService->getIssues($project['key'], $startAt);
$total = $response['total'];
$issueLength = count($response['issues']);
foreach ($response['issues'] as $issue) {
$issues[] = [
'summary' => $issue['fields']['summary'] ?? null,
'desc' => $issue['fields']['description']['content'][0] ?? null,
'assignee' => $issue['fields']['assignee']['displayName'] ?? null,
'status' => $issue['fields']['status']['name'] ?? null,
'worklogs' => json_encode(array_map(function ($log) {
return [
'author' => $log['author']['displayName'] ?? null,
'timeSpent' => $log['timeSpent'] ?? null,
'started' => $log['started'] ?? null,
'updated' => $log['updated'] ?? null,
];
}, $issue['fields']['worklog']['worklogs'] ?? []), JSON_PRETTY_PRINT),
'originalEstimate' => isset($issue['fields']['timeoriginalestimate']) ? $issue['fields']['timeoriginalestimate'] / 60 / 60 : null,
'timeSpent' => isset($issue['fields']['timespent']) ? $issue['fields']['timespent'] / 60 / 60 : null
];
// }
// if (($startAt + $issueLength >= $total && $total !== 0) || $total === 0) {
// $checked = false;
// }
// $startAt += $issueLength;
}
$allIssues[] = [
'project' => $project['name'],
'issues' => $issues
];
return response()->json([
'data' => $allIssues,
'status' => true
], 200);
}
return response()->json([
'data' => $allIssues,
'status' => false
], 500);
}
public function exportToExcel()
{
$allIssues = $this->fetchAllIssues()->original;
$fileName = 'allIssues.xlsx';
Excel::store(new \App\Exports\IssuesExport($allIssues), $fileName);
return response()->download(storage_path("app/{$fileName}"));
}
public function getAllUserWorkLogs(Request $request)
{
try {
$workLogs = $this->jiraService->getAllUserWorkLogs($request->startDate, $request->endDate);
return response()->json([
'data' => $workLogs,
'status' => true
], 200);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
}

View File

@ -7,10 +7,7 @@ use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Modules\Admin\app\Http\Requests\DiscountRequest;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Discount;
use Modules\Admin\app\Models\Tracking;
class TrackingController extends Controller
@ -148,39 +145,4 @@ class TrackingController extends Controller
]);
}
// Delete multiple discounts
public function deletes(DiscountRequest $request)
{
$discounts = $request->get('discounts');
$ids = collect($discounts)->pluck('id');
Discount::whereIn('id', $ids)->delete();
return response()->json([
'data' => $ids,
'status' => true
]);
}
// Update multiple discounts
public function updates(DiscountRequest $request)
{
$discounts = $request->get('discounts');
$ids = collect($discounts)->pluck('id');
foreach ($discounts as $discountRequest) {
// convert to object|array to array
$discountRequest = collect($discountRequest)->toArray();
// handle array
$discount = Discount::find($discountRequest['id']);
if ($discount) {
// exclude id field
unset($discount['id']);
$discount->update($discountRequest);
}
}
return response()->json([
'data' => Discount::whereIn('id', $ids)->get(),
'status' => true
]);
}
}

View File

@ -10,6 +10,7 @@ use Modules\Admin\app\Http\Controllers\CustomThemeController;
use Modules\Admin\app\Http\Controllers\DashboardController;
use Modules\Admin\app\Http\Controllers\DiscountController;
use Modules\Admin\app\Http\Controllers\DiscountTypeController;
use Modules\Admin\app\Http\Controllers\JiraController;
use Modules\Admin\app\Http\Controllers\OrderController;
use Modules\Admin\app\Http\Controllers\PackageController;
use Modules\Admin\app\Http\Controllers\SNCheckController;
@ -142,6 +143,16 @@ Route::middleware('api')
Route::get('/statistics-search-sn-by-month', [DashboardController::class, 'statisticSearchSNByMonth']);
Route::get('/statistics-revenues-by-month', [DashboardController::class, 'statisticRevenuesByMonth']);
});
Route::group([
'prefix' => 'jira',
], function () {
Route::get('/fetch-issues', [JiraController::class, 'fetchAllIssues']);
Route::get('/export-issues', [JiraController::class, 'exportToExcel']);
Route::get('/all-project', [JiraController::class, 'getAllProject']);
Route::get('/all-issue-by-project', [JiraController::class, 'fetchIssuesByProject']);
Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs']);
});
});
});
@ -152,4 +163,5 @@ Route::middleware('api')
Route::post('/scan-create', [TrackingController::class, 'create']);
Route::get('/delete', [TrackingController::class, 'delete']);
// Route::get('/clear-cache', [SettingController::class, 'clearCache']);
});
});

View File

@ -0,0 +1,27 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class IssuesExport implements WithMultipleSheets {
use Exportable;
protected $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function sheets(): array
{
$sheets = [];
foreach ($this->data as $projectData) {
$sheets[] = new ProjectSheet($projectData);
}
return $sheets;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithTitle;
class ProjectSheet implements FromArray, WithHeadings, WithTitle
{
protected $projectData;
public function __construct(array $projectData)
{
$this->projectData = $projectData;
}
public function array(): array
{
return $this->projectData['issues'];
}
public function headings(): array
{
return ['summary', 'desc', 'assignee', 'status', 'worklogs', 'originalEstimate', 'timeSpent'];
}
public function title(): string
{
// Return the project name or any other string based on $projectData
return $this->projectData['project'];
}
}

View File

@ -0,0 +1,203 @@
<?php
namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Promise\Utils;
class JiraService
{
protected $client;
protected $baseUrl;
protected $authHeader;
public function __construct()
{
$this->baseUrl = env('JIRA_BASE_URL');
$this->authHeader = 'Basic ' . base64_encode(env('JIRA_USERNAME') . ':' . env('JIRA_API_TOKEN'));
$this->client = new Client([
'base_uri' => $this->baseUrl,
'headers' => [
'Authorization' => $this->authHeader,
'Accept' => 'application/json',
'Content-Type' => 'application/json'
]
]);
}
public function getAllProjects()
{
$response = $this->client->get('/rest/api/3/project');
return json_decode($response->getBody()->getContents(), true);
}
public function getIssuesAsync($projectKey, $startAt = 0, $maxResults = 50)
{
$body = [
'expand' => ['names', 'schema', 'operations'],
'fields' => ['summary', 'status', 'description', 'timeoriginalestimate', 'timespent', 'worklog', 'assignee'],
'jql' => "project = '{$projectKey}' ORDER BY created DESC",
'maxResults' => $maxResults,
'startAt' => $startAt
];
return $this->client->postAsync($this->baseUrl . '/rest/api/3/search', [
'body' => json_encode($body),
'headers' => [
'Authorization' => $this->authHeader,
'Accept' => 'application/json',
'Content-Type' => 'application/json'
]
]);
}
public function getIssues($projectKey, $startAt = 0)
{
$body = [
'expand' => ['names', 'schema', 'operations'],
'fields' => ['summary', 'status', 'description', 'timeoriginalestimate', 'timespent', 'worklog', 'assignee'],
'jql' => "project = '{$projectKey}' ORDER BY created DESC",
'maxResults' => 100,
'startAt' => $startAt
];
$response = $this->client->post('/rest/api/3/search', [
'body' => json_encode($body)
]);
return json_decode($response->getBody()->getContents(), true);
}
public function getAllUsers()
{
$response = $this->client->get('/rest/api/3/users/search', [
'headers' => [
'Authorization' => $this->authHeader,
'Accept' => 'application/json'
]
]);
return json_decode($response->getBody()->getContents(), true);
}
public function getUserWorkLogs($accountId, $startDate, $endDate)
{
$body = [
'jql' => "worklogAuthor = '{$accountId}'AND worklogDate >= '{$startDate}' AND worklogDate <= '{$endDate}'",
'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'project'],
'maxResults' => 50
];
$response = $this->client->post('/rest/api/3/search', [
'body' => json_encode($body),
'headers' => [
'Authorization' => $this->authHeader,
'Accept' => 'application/json',
'Content-Type' => 'application/json'
]
]);
$data_response = json_decode($response->getBody()->getContents(), true);
if ($data_response['total'] == 0) {
return $data_response;
}
$promises = [];
foreach ($data_response['issues'] as $index => $issue) {
$issueId = $issue['id'];
// Get the initial worklog data to determine total number of worklogs
$promises[$index] = $this->client->getAsync("/rest/api/3/issue/{$issueId}/worklog", [
'query' => [
'startAt' => 0,
'maxResults' => 1
]
])->then(function ($checkApiResponse) use ($issueId, $index) {
$checkApi = json_decode($checkApiResponse->getBody()->getContents(), true);
$maxResults = 50;
$totalWorklogs = $checkApi['total'];
return $this->client->getAsync("/rest/api/3/issue/{$issueId}/worklog", [
'query' => [
'startAt' => $totalWorklogs - $maxResults,
'maxResults' => $totalWorklogs
]
])->then(function ($worklogResponse) use ($index) {
return [
'index' => $index,
'worklogs' => json_decode($worklogResponse->getBody()->getContents(), true)
];
});
});
}
// Wait for all promises to complete
$results = Utils::settle($promises)->wait();
// Attach worklogs to issues
foreach ($results as $result) {
if ($result['state'] === 'fulfilled') {
$data_response['issues'][$result['value']['index']]["fields"]['worklog'] = $result['value']['worklogs'];
}
}
return $data_response;
}
// public function getUserWorkLogs($accountId, $startDate, $endDate)
// {
// $body = [
// 'jql' => "worklogAuthor = '{$accountId}'AND worklogDate >= '{$startDate}' AND worklogDate <= '{$endDate}'",
// 'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'project'],
// 'maxResults' => 50
// ];
// $response = $this->client->post('/rest/api/3/search', [
// 'body' => json_encode($body),
// 'headers' => [
// 'Authorization' => $this->authHeader,
// 'Accept' => 'application/json',
// 'Content-Type' => 'application/json'
// ]
// ]);
// $data_response = json_decode($response->getBody()->getContents(), true);
// // $allRespones = [];
// if ($data_response['total'] != 0) {
// foreach ($data_response['issues'] as $index => $issue) {
// $maxResults = 10;
// $check_api = $this->client->get("/rest/api/3/issue/{$issue['id']}/worklog", [
// 'query' => [
// 'startAt' => 0,
// 'maxResults' => 1
// ]
// ]);
// $check_api = json_decode($check_api->getBody()->getContents(), true);
// $response = $this->client->get("/rest/api/3/issue/{$issue['id']}/worklog", [
// 'query' => [
// 'startAt' => $check_api['total'] - $maxResults,
// 'maxResults' => $check_api['total']
// ]
// ]);
// $data_response['issues'][$index]["fields"]['worklogs'] = json_decode($response->getBody()->getContents(), true);
// }
// }
// return $data_response;
// }
public function getAllUserWorkLogs($startDate, $endDate)
{
$users = $this->getAllUsers();
$workLogs = [];
foreach ($users as $user) {
$userWorkLogs = $this->getUserWorkLogs($user['accountId'], $startDate, $endDate);
$workLogs[] = ['username' => $user['displayName'], 'information' => $userWorkLogs];
}
return $workLogs;
}
}

View File

@ -13,6 +13,7 @@
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8",
"laravel/ui": "^4.3",
"maatwebsite/excel": "^3.1",
"nwidart/laravel-modules": "^10.0",
"pion/laravel-chunk-upload": "^1.5",
"predis/predis": "^2.2",

518
BACKEND/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "879d4b76d94550aedcc3fa47d3db8f96",
"content-hash": "4d9f50111be5d1e2be1581ccf00970b6",
"packages": [
{
"name": "barryvdh/laravel-debugbar",
@ -219,6 +219,87 @@
],
"time": "2023-12-11T17:09:12+00:00"
},
{
"name": "composer/semver",
"version": "3.4.0",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "35e8d0af4486141bc745f23a29cc2091eb624a32"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32",
"reference": "35e8d0af4486141bc745f23a29cc2091eb624a32",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.4",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.0"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2023-08-31T09:50:34+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.2",
@ -642,6 +723,67 @@
],
"time": "2023-10-06T06:47:41+00:00"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.17.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c",
"reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c",
"shasum": ""
},
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0"
},
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
"type": "library",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
],
"psr-0": {
"HTMLPurifier": "library/"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"support": {
"issues": "https://github.com/ezyang/htmlpurifier/issues",
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0"
},
"time": "2023-11-17T15:01:25+00:00"
},
{
"name": "fruitcake/php-cors",
"version": "v1.3.0",
@ -2237,6 +2379,275 @@
],
"time": "2024-01-28T23:22:08+00:00"
},
{
"name": "maatwebsite/excel",
"version": "3.1.55",
"source": {
"type": "git",
"url": "https://github.com/SpartnerNL/Laravel-Excel.git",
"reference": "6d9d791dcdb01a9b6fd6f48d46f0d5fff86e6260"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/6d9d791dcdb01a9b6fd6f48d46f0d5fff86e6260",
"reference": "6d9d791dcdb01a9b6fd6f48d46f0d5fff86e6260",
"shasum": ""
},
"require": {
"composer/semver": "^3.3",
"ext-json": "*",
"illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0",
"php": "^7.0||^8.0",
"phpoffice/phpspreadsheet": "^1.18",
"psr/simple-cache": "^1.0||^2.0||^3.0"
},
"require-dev": {
"laravel/scout": "^7.0||^8.0||^9.0||^10.0",
"orchestra/testbench": "^6.0||^7.0||^8.0||^9.0",
"predis/predis": "^1.1"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Maatwebsite\\Excel\\ExcelServiceProvider"
],
"aliases": {
"Excel": "Maatwebsite\\Excel\\Facades\\Excel"
}
}
},
"autoload": {
"psr-4": {
"Maatwebsite\\Excel\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Patrick Brouwers",
"email": "patrick@spartner.nl"
}
],
"description": "Supercharged Excel exports and imports in Laravel",
"keywords": [
"PHPExcel",
"batch",
"csv",
"excel",
"export",
"import",
"laravel",
"php",
"phpspreadsheet"
],
"support": {
"issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
"source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.55"
},
"funding": [
{
"url": "https://laravel-excel.com/commercial-support",
"type": "custom"
},
{
"url": "https://github.com/patrickbrouwers",
"type": "github"
}
],
"time": "2024-02-20T08:27:10+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "b8174494eda667f7d13876b4a7bfef0f62a7c0d1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/b8174494eda667f7d13876b4a7bfef0f62a7c0d1",
"reference": "b8174494eda667f7d13876b4a7bfef0f62a7c0d1",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.1"
},
"require-dev": {
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^10.0",
"vimeo/psalm": "^5.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.0"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
},
{
"url": "https://opencollective.com/zipstream",
"type": "open_collective"
}
],
"time": "2023-06-21T14:59:35+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{
"name": "maximebf/debugbar",
"version": "v1.22.3",
@ -2890,6 +3301,111 @@
],
"time": "2024-01-28T10:04:15+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.29.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fde2ccf55eaef7e86021ff1acce26479160a0fa0",
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": "^7.4 || ^8.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0 || ^10.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.0"
},
"time": "2023-06-14T22:48:31+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.2",

View File

@ -183,8 +183,8 @@
<span class="four"><span class="screen-reader-text">4</span></span>
</section>
<div class="link-container">
<a href="{{route('home')}}"
class="more-link">Back to home page</a>
<!-- <a href="{{route('home')}}"
class="more-link">Back to home page</a> -->
</div>
</body>

View File

@ -53,4 +53,11 @@ export const statisticRevenuesByMonth = API_URL + 'v1/admin/dashboard/statistics
// Tracking
export const getListTracking = API_URL + 'v1/admin/tracking'
export const deleteTracking = API_URL + 'v1/admin/tracking/delete'
export const deleteTracking = API_URL + 'v1/admin/tracking/delete'
// // Tracking
export const fetchAllIssues = API_URL + 'v1/admin/jira/fetch-issues'
export const exportIssues = API_URL + 'v1/admin/jira/export-issues'
export const getAllProjects = API_URL + 'v1/admin/jira/all-project'
export const getAllIssuesByProject = API_URL + 'v1/admin/jira/all-issue-by-project'
export const getAllUserWorklogs = API_URL + 'v1/admin/jira/worklogs'

View File

@ -25,8 +25,10 @@ import {
// IconMail,
IconMoon,
IconPasswordUser,
IconReport,
IconScan,
IconSettings,
IconSubtask,
IconSun
} from '@tabler/icons-react'
import { useCallback, useEffect, useState } from 'react'
@ -37,6 +39,8 @@ import classes from './NavbarSimpleColored.module.css'
const data = [
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
{ link: '/tracking', label: 'Check in/out', icon: IconScan },
{ link: '/worklogs', label: 'Worklogs', icon: IconReport },
{ link: '/jira', label: 'Jira', icon: IconSubtask },
{ link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
{ link: '/general-setting', label: 'General Setting', icon: IconSettings },
// { link: '/packages', label: 'Packages', icon: IconPackages },

View File

@ -0,0 +1,40 @@
.root {
--link-height: rem(38px);
--indicator-size: rem(10px);
--indicator-offset: calc((var(--link-height) - var(--indicator-size)) / 2);
position: relative;
/* padding-left: 20px; */
}
.link {
display: flex;
text-decoration: none;
align-items: center;
color: var(--mantine-color-text);
line-height: var(--link-height);
font-size: var(--mantine-font-size-sm);
height: var(--link-height);
border-top-right-radius: var(--mantine-radius-sm);
border-bottom-right-radius: var(--mantine-radius-sm);
border-left: rem(2px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}
}
.linkActive {
font-weight: 500;
color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-4));
}
.indicator {
transition: transform 150ms ease;
border: rem(2px) solid light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-4));
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
height: var(--indicator-size);
width: var(--indicator-size);
border-radius: var(--indicator-size);
position: absolute;
left: calc(var(--indicator-size) / -2 + rem(1));
}

View File

@ -0,0 +1,47 @@
import { Avatar, Box } from '@mantine/core';
import cx from 'clsx';
import { useState } from 'react';
import classes from './TableOfContentsFloating.module.css';
type TLink = {
label: string
link: string
order: number
avartar: string
key: string
}
export function TableOfContentsFloating({links, defaultActive, setCurrentProject, }
:{links:TLink[],defaultActive:number, setCurrentProject:(name:string)=>void}) {
const [active, setActive] = useState(defaultActive);
const items = links.map((item, index) => (
<Box<'a'>
component="a"
href={item.link}
onClick={(event) => {
event.preventDefault();
setCurrentProject(item.label)
setActive(index);
}}
key={item.label}
className={cx(classes.link, { [classes.linkActive]: active === index })}
style={{ paddingLeft: `calc(${item.order} * var(--mantine-spacing-md))`}}
>
<Avatar src={item.avartar} size={'xs'} mr={5}/>{item.label}
</Box>
));
return (
<div className={classes.root}>
<div className={classes.links}>
<div
className={classes.indicator}
style={{
transform: `translateY(calc(${active} * var(--link-height) + var(--indicator-offset)))`
}}
/>
{items}
</div>
</div>
);
}

View File

@ -0,0 +1,10 @@
.title {
background-color: light-dark(var(white), var(--mantine-color-dark-7));
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-lg)
var(--mantine-spacing-sm);
border-bottom: solid rgba(201, 201, 201, 0.377) 1px;
}

View File

@ -0,0 +1,155 @@
import { getAllIssuesByProject, getAllProjects } from '@/api/Admin'
import { TableOfContentsFloating } from '@/components/TableOfContentsFloating/TableOfContentsFloating'
import { get } from '@/rtk/helpers/apiService'
import { Box, Loader, Text } from '@mantine/core'
import { useEffect, useState } from 'react'
import classes from './Jira.module.css'
type TProject = {
id: string
name: string
key: string
avatarUrls: {
'48x48': string
'24x24': string
'16x16': string
'32x32': string
}
}
type TIssue = {
summary: string
desc: string | null
assignee: string | null
status: string
worklogs: any[]
originalEstimate: number | null
timeSpent: number | null
}
const Jira = () => {
const [loader, setLoader] = useState(true)
const [data, setData] = useState([])
const [issuesInProject, setIssuesInProject] = useState<TIssue[]>([])
const [listProject, setListProject] = useState<TProject[]>([])
const [listStatus, setListStatus] = useState<string[]>([])
const [currentProject, setCurrentProject] = useState<string>('Summary')
const getUniqueStatuses = (issues: TIssue[]) => {
const statuses = issues.map((issue) => issue.status)
const uniqueStatuses = [...new Set(statuses)]
return uniqueStatuses
}
const getAllIssuesInProject = async (key: string, name: string) => {
try {
if (name !== 'Summary') {
const res = await get(getAllIssuesByProject, { key: key, name: name })
if (res.status) {
var statusArray:string[] = getUniqueStatuses(res.data[0].issues)
setListStatus(statusArray)
setIssuesInProject(res.data[0].issues)
}
}
} catch (error) {
console.log(error)
}
}
const getAllProject = async () => {
try {
const res = await get(getAllProjects)
if (res.status) {
var list: TProject[] = [
{
id: '',
name: 'Summary',
key: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
},
]
setListProject(list.concat(res.data))
setLoader(false)
}
} catch (error) {
console.log(error)
}
}
console.log(listStatus)
useEffect(() => {
getAllProject()
}, [])
useEffect(() => {
getAllIssuesInProject(
listProject.find((p) => p.name === currentProject)?.key!,
currentProject,
)
}, [currentProject])
return loader ? (
<Box ta={'center'}>
<Loader size={40} mt={'15%'} />
</Box>
) : (
<div>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>Jira
</h3>
</div>
<Box display={'flex'}>
<Box w={'20%'} p={'md'}>
<Text fw={700} mb={'md'}>
Projects
</Text>
<Box
h={'70vh'}
style={{
display: 'flex',
flexFlow: 'column',
maxHeight: '70vh',
overflow: 'auto',
paddingLeft: '20px',
}}
>
<TableOfContentsFloating
links={listProject.map((p) => {
return {
label: p.name,
link: '#',
order: 1,
avartar: p.avatarUrls['16x16'],
key: p.key,
}
})}
setCurrentProject={setCurrentProject}
defaultActive={0}
/>
</Box>
</Box>
<Box w={'80%'} h={'100vh'} display={"flex"} style={{overflowX:"auto"}}>
{
listStatus?.map((sta)=>(
<Box p={'sm'} style={{border:"solid 1px gray"}}>
<Text ta={'center'} style={{border:"solid 1px gray", borderRadius:"8px"}} mb={"md"} fw={600}>{sta}</Text>
<Box style={{maxHeight:"90vh", overflowX:"hidden"}}>
{
issuesInProject.filter((iss)=>iss.status === sta)?.map((iss) => (
<Text w={"200px"} fz={15} style={{border:"solid 1px gray", padding:"4px", marginBottom:"5px"}}>{iss.summary}</Text>
))
}
</Box>
</Box>
))
}
</Box>
</Box>
</div>
)
}
export default Jira

View File

@ -0,0 +1,10 @@
.title {
background-color: light-dark(var(white), var(--mantine-color-dark-7));
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-lg)
var(--mantine-spacing-sm);
border-bottom: solid rgba(201, 201, 201, 0.377) 1px;
}

View File

@ -0,0 +1,373 @@
import { Avatar, Box, Button, Loader, Text } from '@mantine/core'
import React, { useEffect, useState } from 'react'
import classes from './Worklogs.module.css'
import { get } from '@/rtk/helpers/apiService'
import { getAllUserWorklogs } from '@/api/Admin'
import moment from 'moment'
import { DateInput, DatePicker } from '@mantine/dates'
interface WorkLog {
self: string
author: UserInfo
updateAuthor: UserInfo
created: string
updated: string
started: string
timeSpent: string
timeSpentSeconds: number
id: string
issueId: string
}
interface UserInfo {
self: string
accountId: string
avatarUrls: AvatarUrls
displayName: string
active: boolean
timeZone: string
accountType: string
}
interface AvatarUrls {
'48x48': string
'24x24': string
'16x16': string
'32x32': string
}
interface Issue {
expand: string
id: string
self: string
key: string
fields: IssueFields
}
interface IssueFields {
summary: string
timespent: number
timeoriginalestimate: number
project: ProjectInfo
worklog: WorkLogList
status: StatusInfo
}
interface ProjectInfo {
self: string
id: string
key: string
name: string
projectTypeKey: string
simplified: boolean
avatarUrls: AvatarUrls
}
interface WorkLogList {
startAt: number
maxResults: number
total: number
worklogs: WorkLog[]
}
interface StatusInfo {
self: string
description: string
iconUrl: string
name: string
id: string
statusCategory: StatusCategory
}
interface StatusCategory {
self: string
id: number
key: string
colorName: string
name: string
}
interface Information {
startAt: number
maxResults: number
total: number
issues: Issue[]
}
interface UserWorklog {
username: string
information: Information
}
const Worklogs = () => {
const [loader, setLoader] = useState(true)
const [updating, setUpdating] = useState(true)
const [worklogs, setWorklogs] = useState<UserWorklog[]>([])
const [date, setDate] = useState({
startDate: moment(Date.now()).format('YYYY-MM-DD'),
endDate: moment(Date.now()).format('YYYY-MM-DD'),
})
const getAllWorklogs = async (
startDate = date.startDate,
endDate = date.endDate,
) => {
try {
setUpdating(false)
const res = await get(getAllUserWorklogs, {
startDate: startDate,
endDate: endDate,
})
if (res.status) {
const data = res.data.filter(
(user: UserWorklog) => user.information.issues.length > 0,
)
setWorklogs(data)
localStorage.setItem(
'data',
JSON.stringify({ time: Date.now(), data: data, date: date }),
)
setLoader(false)
}
setUpdating(true)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
const localData = localStorage.getItem('data')
if (localData !== null) {
setWorklogs(JSON.parse(localData!).data)
setDate(JSON.parse(localData!).date)
setLoader(false)
if (Date.now() - JSON.parse(localData!).time > 300000) {
getAllWorklogs()
}
} else {
getAllWorklogs()
}
}, [])
return loader ? (
<Box ta={'center'}>
<Loader size={40} mt={'15%'} />
</Box>
) : (
<div>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>Worklogs
{!updating ? (
<Text fs={'italic'} fz={'xs'} c={'gray'}>
Updating data in the background ...
</Text>
) : (
''
)}
</h3>
</div>
<Box
display={'flex'}
w={'30%'}
style={{
float: 'right',
margin: '10px',
alignItems: 'end',
justifyContent: 'space-between',
}}
>
<DateInput
size="xs"
label="From date:"
value={new Date(date.startDate)}
w={'40%'}
clearable
onChange={(e) => {
setDate({ ...date, startDate: moment(e).format('YYYY-MM-DD') })
}}
/>
<DateInput
size="xs"
label="To date:"
value={new Date(date.endDate)}
clearable
w={'40%'}
onChange={(e) => {
setDate({ ...date, endDate: moment(e).format('YYYY-MM-DD') })
}}
/>
<Button
size="xs"
onClick={() => {
getAllWorklogs()
}}
>
Search
</Button>
</Box>
<Box
w={'100%'}
h={'85vh'}
display={'flex'}
style={{ overflowX: 'auto', flexFlow: 'column' }}
>
{worklogs?.map((user, index) => (
<Box
p={'sm'}
style={{
border: 'solid 1px gray',
borderColor: '#afafaf',
marginBottom: '10px',
backgroundColor:
index % 2 === 0 ? 'rgb(201 201 201 / 28%)' : 'white',
}}
>
<Text ta={'left'} mb={'xs'} fw={800}>
{user.username}
({user?.information.issues.reduce(
(total: number, issue: Issue) => {
var totalSpent = issue.fields.worklog.worklogs?.reduce(
(accumulator: number, currentValue: WorkLog) => {
if (
parseInt(moment(date.startDate).format('YYYYMMDD')) <=
parseInt(
moment(currentValue.started).format('YYYYMMDD'),
) &&
parseInt(
moment(currentValue.started).format('YYYYMMDD'),
) <= parseInt(moment(date.endDate).format('YYYYMMDD')) && currentValue.updateAuthor.displayName === user.username
) {
return accumulator + currentValue.timeSpentSeconds
}
return accumulator
},
0,
)
return total + totalSpent
},
0,
) /
60 /
60}
h)
</Text>
{user.information.issues.map((iss) => {
if (
iss.fields.worklog.worklogs.filter(
(w) =>
parseInt(moment(date.startDate).format('YYYYMMDD')) <=
parseInt(moment(w.started).format('YYYYMMDD')) &&
parseInt(moment(date.endDate).format('YYYYMMDD')) >=
parseInt(moment(w.started).format('YYYYMMDD')) && w.updateAuthor.displayName === user.username
).length > 0
) {
return (
<Box
style={{
border: 'solid 1px gray',
padding: '10px',
marginBottom: '5px',
maxHeight: '90vh',
overflowX: 'hidden',
color: '#412d2d',
}}
>
<Box>
<Text
fz={14}
display={'flex'}
style={{ alignItems: 'center' }}
>
<b>Summary:</b>{' '}
<Avatar
src={iss.fields.project.avatarUrls['16x16']}
size={'xs'}
m={'0 5px'}
/>
{iss.fields.project.name} -{' '}
<a
href={`https://apactechvn.atlassian.net/browse/${iss.key}`}
target="_blank"
>
{iss.fields.summary}
</a>
</Text>
<Text fz={14}>
<b>Estimate:</b>{' '}
{iss.fields.timeoriginalestimate / 60 / 60}h
</Text>
<Text fz={14}>
<b>Total time spent:</b>{' '}
{iss.fields.timespent / 60 / 60}h
</Text>
<Text fz={14}>
<b>Time spent/day:</b>{' '}
{iss.fields.worklog.worklogs?.reduce(
(accumulator: number, currentValue: WorkLog) => {
if (
(parseInt(
moment(date.startDate).format('YYYYMMDD'),
) <=
parseInt(
moment(currentValue.started).format(
'YYYYMMDD',
),
)) &&
(parseInt(
moment(currentValue.started).format('YYYYMMDD'),
) <=
parseInt(
moment(date.endDate).format('YYYYMMDD')
)) && currentValue.updateAuthor.displayName === user.username
) {
return accumulator + currentValue.timeSpentSeconds
}
return accumulator
},
0,
) /
60 /
60}
h
</Text>
</Box>
{iss.fields.worklog.worklogs?.map((log) => {
if (
moment(date.startDate).format('YYYYMMDD') <=
moment(log.started).format('YYYYMMDD') &&
moment(log.started).format('YYYYMMDD') <=
moment(date.endDate).format('YYYYMMDD') && log.updateAuthor.displayName === user.username
) {
return (
<Box
style={{
padding: '4px 8px',
marginBottom: '5px',
marginLeft: '10px',
backgroundColor: '#d9d9d9',
}}
>
<Text fz={13}>
<b>Start date:</b>{' '}
{moment(log.started).format('HH:mm YYYY/MM/DD')}
</Text>
<Text fz={13}>
<b>Time spent:</b> {log.timeSpent}
</Text>
</Box>
)
}
})}
</Box>
)
}
})}
</Box>
))}
</Box>
</div>
)
}
export default Worklogs

View File

@ -6,9 +6,11 @@ import PageLogin from '@/pages/Auth/Login/Login'
import CustomTheme from '@/pages/CustomTheme/CustomTheme'
import Dashboard from '@/pages/Dashboard/Dashboard'
import GeneralSetting from '@/pages/GeneralSetting/GeneralSetting'
import Jira from '@/pages/Jira/Jira'
import PageNotFound from '@/pages/NotFound/NotFound'
import Tracking from '@/pages/Tracking/Tracking'
import PageWelcome from '@/pages/Welcome/Welcome'
import Worklogs from '@/pages/Worklogs/Worklogs'
import { Navigate } from 'react-router-dom'
const mainRoutes = [
@ -85,6 +87,34 @@ const mainRoutes = [
</ProtectedRoute>
),
},
{
path: '/jira',
element: (
<ProtectedRoute mode="route">
<BasePage
main={
<>
<Jira />
</>
}
></BasePage>
</ProtectedRoute>
),
},
{
path: '/worklogs',
element: (
<ProtectedRoute mode="route">
<BasePage
main={
<>
<Worklogs />
</>
}
></BasePage>
</ProtectedRoute>
),
},
// {
// path: '/packages',
// element: (