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\HasOrderByRequest;
|
||||||
use App\Traits\HasSearchRequest;
|
use App\Traits\HasSearchRequest;
|
||||||
use Illuminate\Http\Request;
|
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\Admin;
|
||||||
use Modules\Admin\app\Models\Discount;
|
|
||||||
use Modules\Admin\app\Models\Tracking;
|
use Modules\Admin\app\Models\Tracking;
|
||||||
|
|
||||||
class TrackingController extends Controller
|
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\DashboardController;
|
||||||
use Modules\Admin\app\Http\Controllers\DiscountController;
|
use Modules\Admin\app\Http\Controllers\DiscountController;
|
||||||
use Modules\Admin\app\Http\Controllers\DiscountTypeController;
|
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\OrderController;
|
||||||
use Modules\Admin\app\Http\Controllers\PackageController;
|
use Modules\Admin\app\Http\Controllers\PackageController;
|
||||||
use Modules\Admin\app\Http\Controllers\SNCheckController;
|
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-search-sn-by-month', [DashboardController::class, 'statisticSearchSNByMonth']);
|
||||||
Route::get('/statistics-revenues-by-month', [DashboardController::class, 'statisticRevenuesByMonth']);
|
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::post('/scan-create', [TrackingController::class, 'create']);
|
||||||
Route::get('/delete', [TrackingController::class, 'delete']);
|
Route::get('/delete', [TrackingController::class, 'delete']);
|
||||||
// Route::get('/clear-cache', [SettingController::class, 'clearCache']);
|
// 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/sanctum": "^3.2",
|
||||||
"laravel/tinker": "^2.8",
|
"laravel/tinker": "^2.8",
|
||||||
"laravel/ui": "^4.3",
|
"laravel/ui": "^4.3",
|
||||||
|
"maatwebsite/excel": "^3.1",
|
||||||
"nwidart/laravel-modules": "^10.0",
|
"nwidart/laravel-modules": "^10.0",
|
||||||
"pion/laravel-chunk-upload": "^1.5",
|
"pion/laravel-chunk-upload": "^1.5",
|
||||||
"predis/predis": "^2.2",
|
"predis/predis": "^2.2",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "879d4b76d94550aedcc3fa47d3db8f96",
|
"content-hash": "4d9f50111be5d1e2be1581ccf00970b6",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "barryvdh/laravel-debugbar",
|
"name": "barryvdh/laravel-debugbar",
|
||||||
|
|
@ -219,6 +219,87 @@
|
||||||
],
|
],
|
||||||
"time": "2023-12-11T17:09:12+00:00"
|
"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",
|
"name": "dflydev/dot-access-data",
|
||||||
"version": "v3.0.2",
|
"version": "v3.0.2",
|
||||||
|
|
@ -642,6 +723,67 @@
|
||||||
],
|
],
|
||||||
"time": "2023-10-06T06:47:41+00:00"
|
"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",
|
"name": "fruitcake/php-cors",
|
||||||
"version": "v1.3.0",
|
"version": "v1.3.0",
|
||||||
|
|
@ -2237,6 +2379,275 @@
|
||||||
],
|
],
|
||||||
"time": "2024-01-28T23:22:08+00:00"
|
"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",
|
"name": "maximebf/debugbar",
|
||||||
"version": "v1.22.3",
|
"version": "v1.22.3",
|
||||||
|
|
@ -2890,6 +3301,111 @@
|
||||||
],
|
],
|
||||||
"time": "2024-01-28T10:04:15+00:00"
|
"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",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.2",
|
"version": "1.9.2",
|
||||||
|
|
|
||||||
|
|
@ -183,8 +183,8 @@
|
||||||
<span class="four"><span class="screen-reader-text">4</span></span>
|
<span class="four"><span class="screen-reader-text">4</span></span>
|
||||||
</section>
|
</section>
|
||||||
<div class="link-container">
|
<div class="link-container">
|
||||||
<a href="{{route('home')}}"
|
<!-- <a href="{{route('home')}}"
|
||||||
class="more-link">Back to home page</a>
|
class="more-link">Back to home page</a> -->
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,4 +53,11 @@ export const statisticRevenuesByMonth = API_URL + 'v1/admin/dashboard/statistics
|
||||||
|
|
||||||
// Tracking
|
// Tracking
|
||||||
export const getListTracking = API_URL + 'v1/admin/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,
|
// IconMail,
|
||||||
IconMoon,
|
IconMoon,
|
||||||
IconPasswordUser,
|
IconPasswordUser,
|
||||||
|
IconReport,
|
||||||
IconScan,
|
IconScan,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
|
IconSubtask,
|
||||||
IconSun
|
IconSun
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
@ -37,6 +39,8 @@ import classes from './NavbarSimpleColored.module.css'
|
||||||
const data = [
|
const data = [
|
||||||
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
|
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
|
||||||
{ link: '/tracking', label: 'Check in/out', icon: IconScan },
|
{ 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: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
|
||||||
{ link: '/general-setting', label: 'General Setting', icon: IconSettings },
|
{ link: '/general-setting', label: 'General Setting', icon: IconSettings },
|
||||||
// { link: '/packages', label: 'Packages', icon: IconPackages },
|
// { 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 CustomTheme from '@/pages/CustomTheme/CustomTheme'
|
||||||
import Dashboard from '@/pages/Dashboard/Dashboard'
|
import Dashboard from '@/pages/Dashboard/Dashboard'
|
||||||
import GeneralSetting from '@/pages/GeneralSetting/GeneralSetting'
|
import GeneralSetting from '@/pages/GeneralSetting/GeneralSetting'
|
||||||
|
import Jira from '@/pages/Jira/Jira'
|
||||||
import PageNotFound from '@/pages/NotFound/NotFound'
|
import PageNotFound from '@/pages/NotFound/NotFound'
|
||||||
import Tracking from '@/pages/Tracking/Tracking'
|
import Tracking from '@/pages/Tracking/Tracking'
|
||||||
import PageWelcome from '@/pages/Welcome/Welcome'
|
import PageWelcome from '@/pages/Welcome/Welcome'
|
||||||
|
import Worklogs from '@/pages/Worklogs/Worklogs'
|
||||||
import { Navigate } from 'react-router-dom'
|
import { Navigate } from 'react-router-dom'
|
||||||
|
|
||||||
const mainRoutes = [
|
const mainRoutes = [
|
||||||
|
|
@ -85,6 +87,34 @@ const mainRoutes = [
|
||||||
</ProtectedRoute>
|
</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',
|
// path: '/packages',
|
||||||
// element: (
|
// element: (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue