Create a workflow statistics page from Jira
This commit is contained in:
parent
306dd78acb
commit
30cc1c3cac
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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: (
|
||||
|
|
|
|||
Loading…
Reference in New Issue