master #57

Merged
joseph merged 17 commits from master into dev 2024-09-21 10:34:09 +10:00
21 changed files with 6935 additions and 2566 deletions

View File

@ -0,0 +1,266 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\JiraService;
use Carbon\Carbon;
use Modules\Admin\app\Models\TechnicalUser;
use Illuminate\Http\Request;
use Modules\Admin\app\Models\UserCriteria;
use PhpOffice\PhpWord\IOFactory;
use PhpOffice\PhpWord\PhpWord;
class EvaluationController extends Controller
{
protected $jiraService;
public function __construct(JiraService $jiraService)
{
$this->jiraService = $jiraService;
}
public function sprintReview(Request $request)
{
$request->validate([
'userID' => 'required|exists:users,id',
'fromDate' => 'nullable|date',
'toDate' => 'nullable|date',
]);
$userEmail = User::findOrFail($request->input('userID'))->email;
$projects = $this->jiraService->getAllProjects();
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$userCriterias = UserCriteria::with([
'sprint' => function ($query) use ($startDate, $endDate) {
if ($startDate && $endDate) {
$query->whereBetween('complete_date', [$startDate, $endDate]);
} elseif ($startDate) {
$query->where('complete_date', '>=', $startDate);
} elseif ($endDate) {
$query->where('complete_date', '<=', $endDate);
}
},
'criteria',
])->where('user_email', $userEmail)
->whereHas('sprint', function ($query) use ($startDate, $endDate) {
if ($startDate && $endDate) {
$query->whereBetween('complete_date', [$startDate, $endDate]);
} elseif ($startDate) {
$query->where('complete_date', '>=', $startDate);
} elseif ($endDate) {
$query->where('complete_date', '<=', $endDate);
}
})
->get();
// dd($userCriterias);
// Xử lý dữ liệu thành cấu trúc mong muốn
$projectsData = $userCriterias->groupBy('sprint.project_id')->map(function ($userCriteriasByProject, $projectId) use ($projects) {
$result = self::getProjectById($projects, $projectId);
return [
'name' => $result['name'],
'sprints' => $userCriteriasByProject->groupBy('sprint.id')->map(function ($userCriteriasBySprint) {
$sprint = $userCriteriasBySprint->first()->sprint;
return [
'name' => $sprint->name,
'complete_date' => $sprint->complete_date ?? '',
'criterias' => $userCriteriasBySprint->map(function ($userCriteria) {
$criteria = $userCriteria->criteria;
return [
'criteria' => $criteria->name,
'note' => $userCriteria->note ?? '',
'createdBy' => $userCriteria->created_by ?? '', // Lấy tên user từ auth
'point' => $userCriteria->point ?? '',
];
})
];
})->values()
];
})->values();
// Trả về kết quả
return AbstractController::ResultSuccess($projectsData);
}
public function getProjectById($projects, $inputId)
{
$filteredProjects = array_filter($projects, function ($project) use ($inputId) {
return $project['id'] == $inputId;
});
return array_values($filteredProjects) ? array_values($filteredProjects)[0] : array_values($filteredProjects);
}
public function technical(Request $request)
{
$request->validate([
'user_id' => 'required|exists:users,id',
'keyword' => 'nullable|string'
]);
$user = User::findOrFail($request->input('user_id'));
$query = TechnicalUser::query();
$query = TechnicalUser::where('user_id', $user->id);
if ($request->filled('keyword')) {
$keyword = '%' . $request->input('keyword') . '%';
$query->whereHas('technical', fn($q) => $q->where('name', 'like', $keyword))
->with('technical');
} else {
$query->with('technical');
}
$technical_users = $query->where('point', '>', 0)->get();
return AbstractController::ResultSuccess(
$technical_users
->map(function ($user) {
return [
'id' => $user->id,
'point' => $user->point,
'updated_at' => $user->updated_at,
'technical' => [
'id' => $user->technical->id,
'name' => $user->technical->name,
'level' => $user->technical->level,
]
];
})
);
}
public function report(Request $request)
{
$request->validate([
'userID' => 'required|exists:users,id',
'fromDate' => 'nullable|date',
'toDate' => 'nullable|date',
]);
// Query User
$user = User::findOrFail($request->input('userID'));
$userEmail = $user->email;
// Query User Criteria
$query = UserCriteria::where('user_email', $userEmail);
if ($request->filled('fromDate')) {
$fromDate = Carbon::parse($request->input('fromDate'));
$query->where('created_at', '>=', $fromDate);
}
if ($request->filled('toDate')) {
$toDate = Carbon::parse($request->input('toDate'));
$query->where('created_at', '<=', $toDate);
}
$userCriterias = $query->with(['sprint', 'criteria'])->get();
// Structure projects data
$projects = $this->jiraService->getAllProjects();
$projectsData = $userCriterias->groupBy('sprint.project_id')->map(function ($userCriteriasByProject, $projectId) use ($projects) {
$result = self::getProjectById($projects, $projectId);
return [
'name' => $result['name'],
'sprints' => $userCriteriasByProject->groupBy('sprint.id')->map(function ($userCriteriasBySprint) {
$sprint = $userCriteriasBySprint->first()->sprint;
return [
'name' => $sprint->name,
'criterias' => $userCriteriasBySprint->map(function ($userCriteria) {
$criteria = $userCriteria->criteria;
return [
'criteria' => $criteria->name,
'note' => $userCriteria->note ?? '',
'createdBy' => $userCriteria->created_by ?? '',
'point' => $userCriteria->point ?? '',
];
})
];
})->values()
];
})->values();
// Generate Word document
$phpWord = new PhpWord();
$phpWord->setDefaultFontName('Times New Roman');
$phpWord->setDefaultFontSize(12);
$section = $phpWord->addSection();
$section->addText('Staff Evaluation', [
'bold' => true,
'size' => 20,
'color' => '000000',
], [
'align' => 'center',
'spaceAfter' => 600,
]);
if ($request->filled('fromDate')) {
$fromDate = Carbon::parse($request->input('fromDate'));
$section->addText("From: " . $fromDate->format('d-m-Y'), ['name' => 'Times New Roman', 'size' => 12], ['align' => 'end']);
}
if ($request->filled('toDate')) {
$toDate = Carbon::parse($request->input('toDate'));
$section->addText("To: " . $toDate->format('d-m-Y'), ['name' => 'Times New Roman', 'size' => 12], ['align' => 'end']);
}
$section->addText("{$user->name}", [
'bold' => true,
'size' => 14,
'color' => '000000',
], [
'spaceAfter' => 400,
]);
foreach ($projectsData as $project) {
$section->addText("Project: {$project['name']}", ['bold' => true, 'size' => 14, 'color' => '000080']);
foreach ($project['sprints'] as $sprint) {
$section->addText("Sprint: {$sprint['name']}", ['bold' => true, 'italic' => true, 'size' => 12, 'color' => '000000']);
$table = $section->addTable([
'borderColor' => '000000',
'borderSize' => 6,
'cellMargin' => 80,
]);
$table->addRow();
$table->addCell(2000)->addText('Criteria', ['bold' => true]);
$table->addCell(2000)->addText('Note', ['bold' => true]);
$table->addCell(2000)->addText('Created By', ['bold' => true]);
$table->addCell(2000)->addText('Points', ['bold' => true]);
foreach ($sprint['criterias'] as $criteria) {
$table->addRow();
$table->addCell(2000)->addText($criteria['criteria']);
$table->addCell(2000)->addText($criteria['note']);
$table->addCell(2000)->addText($criteria['createdBy']);
$table->addCell(2000)->addText($criteria['point']);
}
$section->addText(' ', [], [
'spaceAfter' => 600,
]);
}
$section->addText(' ', [], [
'spaceAfter' => 600,
]);
}
$tempFile = tempnam(sys_get_temp_dir(), 'word');
$objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
$objWriter->save($tempFile);
return response()->download($tempFile, "$user->name.docx")->deleteFileAfterSend(true);
}
}

View File

@ -28,18 +28,39 @@ class ProfileController extends Controller
$this->jiraService = $jiraService;
}
public function getProfilesData()
public function getProfilesData(Request $request)
{
$user = auth('admins')->user();
$userEmail = $user->email;
$projects = $this->jiraService->getAllProjects();
$userCriterias = UserCriteria::with([
'sprint', // Join với bảng sprint
'criteria', // Join với bảng criteria
])->where('user_email', $userEmail)->get();
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$userCriterias = UserCriteria::with([
'sprint' => function ($query) use ($startDate, $endDate) {
if ($startDate && $endDate) {
$query->whereBetween('complete_date', [$startDate, $endDate]);
} elseif ($startDate) {
$query->where('complete_date', '>=', $startDate);
} elseif ($endDate) {
$query->where('complete_date', '<=', $endDate);
}
},
'criteria',
])->where('user_email', $userEmail)
->whereHas('sprint', function ($query) use ($startDate, $endDate) {
if ($startDate && $endDate) {
$query->whereBetween('complete_date', [$startDate, $endDate]);
} elseif ($startDate) {
$query->where('complete_date', '>=', $startDate);
} elseif ($endDate) {
$query->where('complete_date', '<=', $endDate);
}
})
->get();
// dd($userCriterias);
// Xử lý dữ liệu thành cấu trúc mong muốn
$projectsData = $userCriterias->groupBy('sprint.project_id')->map(function ($userCriteriasByProject, $projectId) use ($projects) {
$result = self::getProjectById($projects, $projectId);
@ -49,6 +70,7 @@ class ProfileController extends Controller
$sprint = $userCriteriasBySprint->first()->sprint;
return [
'name' => $sprint->name,
'complete_date' => $sprint->complete_date ?? '',
'criterias' => $userCriteriasBySprint->map(function ($userCriteria) {
$criteria = $userCriteria->criteria;
return [

View File

@ -0,0 +1,191 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Admin\app\Models\Technical;
use Modules\Admin\app\Models\TechnicalUser;
use App\Traits\AnalyzeData;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Modules\Admin\app\Models\Admin;
class TechnicalController extends Controller
{
use HasOrderByRequest;
use HasFilterRequest;
use HasSearchRequest;
use AnalyzeData;
public function getAllUsers()
{
$users = Admin::where('permission', 'like', '%staff%')->get()->toArray();
return AbstractController::ResultSuccess($users);
}
public function getAllTechnical(Request $request)
{
$technicals = new Technical;
// Order by
$this->orderByRequest($technicals, $request);
$technicals->orderBy('name', 'asc');
// Filter
$this->filterRequest(
builder: $technicals,
request: $request,
filterKeys: [
'name' => self::F_TEXT,
'level' => self::F_TEXT,
]
);
$this->searchRequest(
builder: $technicals,
value: $request->get('search'),
fields: [
'name',
'level'
]
);
$responseData = $technicals->get();
return AbstractController::ResultSuccess($responseData);
}
public function createTechnical(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'level' => 'nullable|integer|min:1|max:3',
]);
$technical = Technical::create([
'name' => $request->name,
'level' => $request->level,
]);
return AbstractController::ResultSuccess($technical, "Technical created successfully!");
}
public function deleteTechnical(Request $request)
{
$id = $request->input('id');
$technical = Technical::find($id);
if (!$technical) {
return AbstractController::ResultError("Technical not found");
}
$technical->delete();
return AbstractController::ResultSuccess("Technical deleted successfully!");
}
public function getTechnicalsByUserId($userId)
{
$technicals = TechnicalUser::with('technical')
->where('user_id', $userId)
->orderBy('point', 'desc')
->get();
// Lấy tất cả các technical
$allTechnicals = Technical::all();
if ($technicals->isEmpty() && $allTechnicals->isEmpty()) {
return AbstractController::ResultError("No technicals found.");
}
// Chuẩn bị dữ liệu để trả về
$technicalData = $allTechnicals->map(function ($technical) use ($technicals) {
// Tìm kiếm kỹ thuật từ bảng technical_users
$technicalUser = $technicals->firstWhere('technical_id', $technical->id);
return [
'id' => $technical->id,
'name' => $technical->name,
'level' => $technical->level,
'point' => $technicalUser ? $technicalUser->point : 0, // Nếu không tồn tại, điểm mặc định là 0
'updated_at' => $technicalUser ? $technicalUser->updated_at : null // Nếu không tồn tại, updated_at là null
];
});
$sortedTechnicalData = $technicalData->sortByDesc('point')->values(); // values() để giữ lại chỉ số
return AbstractController::ResultSuccess($sortedTechnicalData);
}
public function getTechnicalsOfUser()
{
$userInfo = auth('admins')->user();
$userId = $userInfo->id;
$technicals = TechnicalUser::with('technical')
->where('user_id', $userId)
->get();
if ($technicals->isEmpty()) {
return AbstractController::ResultError("No technicals found for this user.");
}
// Chuẩn bị dữ liệu để trả về
$technicalData = $technicals->map(function ($technicalUser) {
return [
'id' => $technicalUser->technical->id,
'name' => $technicalUser->technical->name,
'level' => $technicalUser->technical->level,
'point' => $technicalUser->point,
'updated_at' => $technicalUser->updated_at
];
});
return AbstractController::ResultSuccess($technicalData);
}
public function getListUserByTechnicalId($technicalId)
{
$users = TechnicalUser::with('user')
->where('technical_id', $technicalId)
->get();
if ($users->isEmpty()) {
return AbstractController::ResultError("No users found for this technical.");
}
$userData = $users->map(function ($technicalUser) {
if ($technicalUser->user) {
return [
'user_id' => $technicalUser->user->id ?? "",
'name' => $technicalUser->user->name,
'email' => $technicalUser->user->email,
'point' => $technicalUser->point
];
}
});
return AbstractController::ResultSuccess($userData);
}
public function updateTechnicalsUser(Request $request)
{
$userInfo = auth('admins')->user();
$validatedData = $request->validate([
'technicals' => 'required|array',
'technicals.*.technical_id' => 'required|exists:technicals,id', // Phải tồn tại trong bảng technicals
'technicals.*.point' => 'required|integer|min:0|max:3', // Điểm trong khoảng [0-3]
]);
// Xóa hết các bản ghi hiện có của user trong bảng technical_users
TechnicalUser::where('user_id', $userInfo->id)->delete();
// Duyệt qua mảng technicals và thêm mới dữ liệu
foreach ($validatedData['technicals'] as $technical) {
TechnicalUser::create([
'user_id' => $userInfo->id,
'technical_id' => $technical['technical_id'],
'point' => $technical['point']
]);
}
return AbstractController::ResultSuccess('Technicals for user updated successfully.');
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Technical extends Model
{
use HasFactory;
protected $fillable = ['name', 'level'];
}

View File

@ -0,0 +1,32 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TechnicalUser extends Model
{
use HasFactory;
protected $table = 'technicals_users';
// Các trường có thể được fill
protected $fillable = [
'user_id',
'technical_id',
'point',
];
// Quan hệ với bảng users
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
// Quan hệ với bảng technicals
public function technical()
{
return $this->belongsTo(Technical::class, 'technical_id');
}
}

View File

@ -16,7 +16,9 @@ use Modules\Admin\app\Http\Controllers\TicketController;
use Modules\Admin\app\Http\Controllers\TimekeepingController;
use Modules\Admin\app\Http\Controllers\TrackingController;
use Modules\Admin\app\Http\Controllers\CriteriasController;
use Modules\Admin\app\Http\Controllers\EvaluationController;
use Modules\Admin\app\Http\Controllers\ProfileController;
use Modules\Admin\app\Http\Controllers\TechnicalController;
use Modules\Admin\app\Http\Controllers\TestCaseForSprintController;
use Modules\Admin\app\Http\Middleware\AdminMiddleware;
@ -173,6 +175,27 @@ Route::middleware('api')
Route::get('/profiles-data', [ProfileController::class, 'getProfilesData'])->middleware('check.permission:admin.hr.staff.tester');
Route::post('/profiles-data/update', [ProfileController::class, 'updateProfilesData'])->middleware('check.permission:admin.hr.staff.tester');
});
Route::group([
'prefix' => 'evaluation',
], function () {
Route::get('/sprint-review', [EvaluationController::class, 'sprintReview'])->middleware('check.permission:admin');
Route::get('/technical', [EvaluationController::class, 'technical'])->middleware('check.permission:admin');
Route::get('/report', [EvaluationController::class, 'report'])->middleware('check.permission:admin');
});
Route::group([
'prefix' => 'technical',
], function () {
Route::get('/get-all-user', [TechnicalController::class, 'getAllUsers'])->middleware('check.permission:admin');
Route::get('/get-all', [TechnicalController::class, 'getAllTechnical']);
Route::post('/create', [TechnicalController::class, 'createTechnical'])->middleware('check.permission:admin');
Route::get('/delete', [TechnicalController::class, 'deleteTechnical'])->middleware('check.permission:admin');
Route::get('/get-tech-of-user', [TechnicalController::class, 'getTechnicalsOfUser']);
Route::get('/get-tech-by-user-id/{userId}', [TechnicalController::class, 'getTechnicalsByUserId'])->middleware('check.permission:admin');
Route::get('/get-list-user-by-tech-id/{technicalId}', [TechnicalController::class, 'getListUserByTechnicalId'])->middleware('check.permission:admin');
Route::post('/technicals-user/update', [TechnicalController::class, 'updateTechnicalsUser']);
});
});
});

View File

@ -0,0 +1,18 @@
<?php
namespace App\Exports;
use App\Models\Technical;
use Maatwebsite\Excel\Concerns\FromCollection;
use Modules\Admin\app\Models\Technical as ModelsTechnical;
class TechnicalsExport implements FromCollection
{
/**
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return ModelsTechnical::all();
}
}

View File

@ -15,6 +15,7 @@
"laravel/ui": "^4.3",
"maatwebsite/excel": "^3.1",
"nwidart/laravel-modules": "^10.0",
"phpoffice/phpword": "^1.3",
"pion/laravel-chunk-upload": "^1.5",
"predis/predis": "^2.2",
"simplesoftwareio/simple-qrcode": "^4.2",

163
BACKEND/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6bac35902964ded629f238acceaf2c81",
"content-hash": "3c46ab46834a4aa1178208be43c9e064",
"packages": [
{
"name": "bacon/bacon-qr-code",
@ -3389,6 +3389,58 @@
],
"time": "2024-01-28T10:04:15+00:00"
},
{
"name": "phpoffice/math",
"version": "0.2.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/Math.git",
"reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc2eb6d1a61b058d5dac77197059db30ee3c8329",
"reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xml": "*",
"php": "^7.1|^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.88 || ^1.0.0",
"phpunit/phpunit": "^7.0 || ^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\Math\\": "src/Math/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Progi1984",
"homepage": "https://lefevre.dev"
}
],
"description": "Math - Manipulate Math Formula",
"homepage": "https://phpoffice.github.io/Math/",
"keywords": [
"MathML",
"officemathml",
"php"
],
"support": {
"issues": "https://github.com/PHPOffice/Math/issues",
"source": "https://github.com/PHPOffice/Math/tree/0.2.0"
},
"time": "2024-08-12T07:30:45+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.29.1",
@ -3494,6 +3546,115 @@
},
"time": "2024-09-03T00:55:32+00:00"
},
{
"name": "phpoffice/phpword",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PHPWord.git",
"reference": "8392134ce4b5dba65130ba956231a1602b848b7f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/8392134ce4b5dba65130ba956231a1602b848b7f",
"reference": "8392134ce4b5dba65130ba956231a1602b848b7f",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-json": "*",
"ext-xml": "*",
"php": "^7.1|^8.0",
"phpoffice/math": "^0.2"
},
"require-dev": {
"dompdf/dompdf": "^2.0",
"ext-gd": "*",
"ext-libxml": "*",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.3",
"mpdf/mpdf": "^8.1",
"phpmd/phpmd": "^2.13",
"phpstan/phpstan-phpunit": "@stable",
"phpunit/phpunit": ">=7.0",
"symfony/process": "^4.4 || ^5.0",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Allows writing PDF",
"ext-gd2": "Allows adding images",
"ext-xmlwriter": "Allows writing OOXML and ODF",
"ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template",
"ext-zip": "Allows writing OOXML and ODF"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpWord\\": "src/PhpWord"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0"
],
"authors": [
{
"name": "Mark Baker"
},
{
"name": "Gabriel Bull",
"email": "me@gabrielbull.com",
"homepage": "http://gabrielbull.com/"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net/blog/"
},
{
"name": "Ivan Lanin",
"homepage": "http://ivan.lanin.org"
},
{
"name": "Roman Syroeshko",
"homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/"
},
{
"name": "Antoine de Troostembergh"
}
],
"description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)",
"homepage": "https://phpoffice.github.io/PHPWord/",
"keywords": [
"ISO IEC 29500",
"OOXML",
"Office Open XML",
"OpenDocument",
"OpenXML",
"PhpOffice",
"PhpWord",
"Rich Text Format",
"WordprocessingML",
"doc",
"docx",
"html",
"odf",
"odt",
"office",
"pdf",
"php",
"reader",
"rtf",
"template",
"template processor",
"word",
"writer"
],
"support": {
"issues": "https://github.com/PHPOffice/PHPWord/issues",
"source": "https://github.com/PHPOffice/PHPWord/tree/1.3.0"
},
"time": "2024-08-30T18:03:42+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.3",

File diff suppressed because it is too large Load Diff

View File

@ -73,4 +73,12 @@ export const createTestCase = API_URL + 'v1/admin/criterias/test-cases'
//Profile
export const getProfilesData = API_URL + 'v1/admin/criterias/profiles-data'
export const updateProfilesData = API_URL + 'v1/admin/criterias/profiles-data/update'
export const updateProfilesData =
API_URL + 'v1/admin/criterias/profiles-data/update'
export const getAllUser = API_URL + 'v1/admin/technical/get-all-user'
export const getAllTechByUserId =
API_URL + 'v1/admin/technical/get-tech-by-user-id'
export const evaluation = API_URL + 'v1/admin/evaluation/report'
export const sprintReview = API_URL + 'v1/admin/evaluation/sprint-review'

View File

@ -27,6 +27,15 @@
justify-items: center;
}
.totalBox {
width: 60%;
display: flex;
flex-flow: row;
align-items: end;
justify-content: right;
justify-items: center;
}
.headers {
height: 6vh;
z-index: 100;

View File

@ -83,6 +83,7 @@ export const DataTableAll = ({
searchInput,
checkBox,
size,
infoTotal,
}: {
data: any[]
columns: Column[]
@ -90,6 +91,7 @@ export const DataTableAll = ({
searchInput?: boolean
checkBox?: boolean
size: string
infoTotal?: React.ReactNode // Set the type to ReactNode to allow JSX elements
}) => {
const [Tdata, setTData] = useState<any[]>(data)
// const [tempData, setTempData] = useState<any[]>([])
@ -323,7 +325,14 @@ export const DataTableAll = ({
</Text>
)}
</Box>
<Box
className={classes.totalBox}
display={infoTotal ? 'flex' : 'none'}
>
<Text fz={'sm'} ta={'right'}>
{infoTotal}
</Text>
</Box>
<Box
className={classes.paginationBox}
display={pagination ? 'flex' : 'none'}
@ -682,7 +691,8 @@ export const DataTablePagination = ({
search: urlParams.toString(),
})
}
statusSort.status === 'clear' && removeParam(`order_by_${statusSort.name}`)
statusSort.status === 'clear' &&
removeParam(`order_by_${statusSort.name}`)
} catch (error) {
console.warn(error)
}

View File

@ -24,6 +24,7 @@ import {
IconBinaryTree2,
IconCalendar,
IconCalendarClock,
IconChartDots2,
IconDevices,
IconLayoutSidebarLeftExpand,
IconLayoutSidebarRightExpand,
@ -37,7 +38,7 @@ import {
IconSun,
IconTicket,
IconUsersGroup,
IconZoomExclamation,
IconZoomExclamation
} from '@tabler/icons-react'
import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
@ -107,6 +108,12 @@ const data = [
icon: IconZoomExclamation,
group: 'hidden',
},
{
link: '/staff-avaluation',
label: 'Staff evaluation',
icon: IconChartDots2,
group: 'admin',
},
// { link: '/jira', label: 'Jira', icon: IconSubtask },
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
// { link: '/general-setting', label: 'General Setting', icon: IconSettings },

View File

@ -0,0 +1,58 @@
.projectHeader {
display: flex;
align-items: center;
cursor: pointer;
padding: 5px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 4px;
margin-top: 10px;
background-color: #457bad;
color: white;
transition: background-color 0.3s;
}
.projectHeader:hover {
background-color: var(--hover-background);
color: #2d353c;
}
.sprintList {
padding-left: 20px; /* Indentation for sprints under the project */
}
.sprintHeader2 {
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.1);
width: 100%;
padding-left: 5px;
padding-top: 5px;
padding-bottom: 5px;
margin-left: 20px;
align-items: center;
cursor: pointer;
background-color: rgba(162, 162, 162, 0.1);
}
.sprintHeader {
margin-top: 5px;
display: flex;
align-items: center;
cursor: pointer;
padding-left: 5px;
padding-top: 5px;
padding-bottom: 5px;
/* margin-left: 20px; */
transition: background-color 0.3s;
}
.sprintHeader2:hover {
background-color: var(--hover-background);
}
.criteriaTable {
padding-left: 35px; /* Further indentation for the criteria table */
}
.project {
margin-top: 1rem !important;
}

View File

@ -0,0 +1,162 @@
import { Box, Table, Text } from '@mantine/core'
import { IconCornerDownRight } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
import classes from './ProjectInvolvement.module.css'
interface Project {
name: string
sprints: Sprint[]
}
interface Sprint {
name: string
criterias: TableRow[]
}
interface TableRow {
criteria: string
note: string
createdBy: string
point: number
}
interface ExpandedProjects {
[projectName: string]: boolean
}
interface ExpandedSprints {
[projectName: string]: {
[sprintName: string]: boolean
}
}
interface ProjectInvolvementProps {
dataProfile: Project[]
page: string
}
type CriteriaTableProps = {
data: TableRow[]
page: string
}
const CriteriaTable: React.FC<CriteriaTableProps> = ({ data, page }) => (
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr bg="#228be66b">
<Table.Th style={{ textAlign: 'center', width: '25%' }}>
Criteria
</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '45%' }}>Note</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '20%' }}>
Created by
</Table.Th>
{page == 'profile' ? (
''
) : (
<Table.Th style={{ textAlign: 'center', width: '10%' }}>
Point
</Table.Th>
)}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data.map((row, index) => (
<Table.Tr key={index}>
<Table.Td style={{ textAlign: 'start' }}>{row.criteria}</Table.Td>
<Table.Td style={{ textAlign: 'start' }}>{row.note}</Table.Td>
<Table.Td style={{ textAlign: 'start' }}>{row.createdBy}</Table.Td>
{page == 'profile' ? (
''
) : (
<Table.Td>{row.point == 0 ? '' : row.point}</Table.Td>
)}
</Table.Tr>
))}
</Table.Tbody>
</Table>
)
const ProjectInvolvement = ({ dataProfile, page }: ProjectInvolvementProps) => {
const [expandedProjects, setExpandedProjects] = useState<ExpandedProjects>({})
const [expandedSprints, setExpandedSprints] = useState<ExpandedSprints>({})
useEffect(() => {
const initialProjectState: ExpandedProjects = {}
const initialSprintState: ExpandedSprints = {}
dataProfile.forEach((project) => {
initialProjectState[project.name] = true // Expand all projects by default
initialSprintState[project.name] = {}
project.sprints.forEach((sprint) => {
initialSprintState[project.name][sprint.name] = true // Expand all sprints by default
})
})
setExpandedProjects(initialProjectState)
setExpandedSprints(initialSprintState)
}, [dataProfile])
const handleProjectToggle = (projectName: string) => {
setExpandedProjects((prev) => ({
...prev,
[projectName]: !prev[projectName], // Toggle state for this project
}))
}
const handleSprintToggle = (projectName: string, sprintName: string) => {
setExpandedSprints((prev) => ({
...prev,
[projectName]: {
...prev[projectName],
[sprintName]: !prev[projectName]?.[sprintName], // Toggle state for this sprint
},
}))
}
return (
<Box className={classes.project} mt="lg">
{dataProfile.map((project) => (
<div key={project.name}>
<Box
className={classes.projectHeader}
onClick={() => handleProjectToggle(project.name)}
>
<Text ml="sm" fw={600}>
{project.name}
</Text>
</Box>
{expandedProjects[project.name] && (
<Box className={classes.sprintList}>
{project.sprints.map((sprint) => (
<div key={sprint.name}>
<Box
className={classes.sprintHeader}
onClick={() =>
handleSprintToggle(project.name, sprint.name)
}
>
<IconCornerDownRight size={20} />
<Text ml="xs" className={classes.sprintHeader2} fw={600}>
{sprint.name}
</Text>
</Box>
{expandedSprints[project.name]?.[sprint.name] && (
<Box className={classes.criteriaTable}>
<CriteriaTable data={sprint.criterias} page={page} />
</Box>
)}
</div>
))}
</Box>
)}
</div>
))}
</Box>
)
}
export default ProjectInvolvement

View File

@ -1,73 +1,28 @@
import { getProfilesData, updateProfilesData } from '@/api/Admin'
import { changePassword } from '@/api/Auth'
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
import { logout } from '@/rtk/dispatches/auth'
import { get, post, postImage } from '@/rtk/helpers/apiService'
import { requirementsPassword } from '@/rtk/helpers/variables'
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
import { getUser } from '@/rtk/localStorage'
import { success } from '@/rtk/slices/auth'
import {
Avatar,
Box,
Button,
Modal,
PasswordInput,
Table,
Text,
TextInput,
Title,
} from '@mantine/core'
import { notifications } from '@mantine/notifications'
import { IconCornerDownRight, IconPasswordUser } from '@tabler/icons-react'
import { IconPasswordUser } from '@tabler/icons-react'
import { useCallback, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import classes from './Profile.module.css'
import { success } from '@/rtk/slices/auth'
interface TableRow {
// Add properties for each column in the table
criteria: string
note: string
createdBy: string
point: number
}
interface ExpandedProjects {
[projectName: string]: boolean
}
interface ExpandedSprints {
[projectName: string]: {
[sprintName: string]: boolean
}
}
const CriteriaTable = ({ data }: { data: TableRow[] }) => (
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr bg="#228be66b">
<Table.Th style={{ textAlign: 'center', width: '25%' }}>
Criteria
</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '45%' }}>Note</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '20%' }}>
Created by
</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '10%' }}>Point</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data.map((row, index) => (
<Table.Tr key={index}>
<Table.Td style={{ textAlign: 'start' }}>{row.criteria}</Table.Td>
<Table.Td style={{ textAlign: 'start' }}>{row.note}</Table.Td>
<Table.Td style={{ textAlign: 'start' }}>{row.createdBy}</Table.Td>
<Table.Td>{row.point == 0 ? '' : row.point}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)
const isCompactMenu = false
const Profile = () => {
@ -83,8 +38,6 @@ const Profile = () => {
const [avatar, setAvatar] = useState(user.user.avatar)
const [loading, setLoading] = useState(false)
const [dataProfile, setDataProfile] = useState<any>([])
const [expandedProjects, setExpandedProjects] = useState<ExpandedProjects>({})
const [expandedSprints, setExpandedSprints] = useState<ExpandedSprints>({})
const [countSpam, setCountSpam] = useState(0)
const [selectedAvatar, setSelectedAvatar] = useState<string | null>(null)
@ -229,22 +182,6 @@ const Profile = () => {
const handleLogout = useCallback(() => {
dispatch(logout(navigate))
}, [dispatch, navigate])
const handleProjectToggle = (projectName: string) => {
setExpandedProjects((prev) => ({
...prev,
[projectName]: !prev[projectName], // Toggle state for this project
}))
}
const handleSprintToggle = (projectName: string, sprintName: string) => {
setExpandedSprints((prev) => ({
...prev,
[projectName]: {
...prev[projectName],
[sprintName]: !prev[projectName]?.[sprintName], // Toggle state for this sprint
},
}))
}
return (
<div>
@ -364,58 +301,7 @@ const Profile = () => {
<Box className={classes.projectInvolvement}>
<Title order={3}>Project Involved</Title>
<Box className={classes.project} mt="lg">
{dataProfile.map((project: any) => (
<div key={project.name}>
{/* Project Header */}
<Box
className={classes.projectHeader}
onClick={() => handleProjectToggle(project.name)}
>
<Text ml="sm" fw={600}>
{project.name}
</Text>
</Box>
{/* Project's Sprints */}
{expandedProjects[project.name] && (
<Box className={classes.sprintList}>
{project.sprints.map((sprint: any) => (
<div key={sprint.name}>
<Box
className={classes.sprintHeader}
onClick={() =>
handleSprintToggle(project.name, sprint.name)
}
>
<IconCornerDownRight size={20} />
<Text
ml="xs"
className={classes.sprintHeader2}
fw={600}
>
{sprint.name}
</Text>
</Box>
{/* Criteria Table */}
{expandedSprints[project.name]?.[sprint.name] && (
<Box className={classes.criteriaTable}>
{/* <CriteriaTable data={sprint.criteria} /> */}
<CriteriaTable
data={sprint.criterias.map((criteria: any) => ({
...criteria,
}))}
/>
</Box>
)}
</div>
))}
</Box>
)}
</div>
))}
</Box>
<ProjectInvolvement dataProfile={dataProfile} page="profile" />
</Box>
</Box>

View File

@ -0,0 +1,202 @@
:root {
--primary-color: rgb(9, 132, 132);
--secondary-color: #ff9100;
--hover-background: rgba(203, 203, 203, 0.809);
}
.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;
}
.optionIcon {
display: flex;
justify-content: space-evenly;
}
.deleteIcon,
.editIcon {
cursor: pointer;
padding: 2px;
border-radius: 25%;
}
.deleteIcon {
color: red;
}
.editIcon {
color: var(--primary-color);
}
.editIcon:hover,
.deleteIcon:hover {
background-color: var(--hover-background);
}
.dialog {
background-color: light-dark(white, #2d353c);
text-align: center;
border: solid 1px var(--secondary-color);
}
.dialogText {
color: light-dark(#2d353c, white);
}
/* Wrapper for the entire user info and project section */
.userInfoSection {
display: flex;
margin-top: 20px;
gap: 10px;
}
.titleSidebar {
text-align: center;
}
/* Sidebar section for user information */
.sidebar {
flex-grow: 1;
padding: 15px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Main section for projects and sprints */
.projectInvolvement {
width: 60%;
padding: 0 15px 15px 15px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Responsive Styles */
@media (max-width: 768px) {
.userInfoSection {
flex-direction: column;
}
.sidebar,
.projectInvolvement {
width: 100%;
padding: 10px;
}
}
.sprintContainer {
display: flex;
align-items: center;
}
.iconWrapper {
margin-right: 10px;
flex-shrink: 0;
display: flex;
align-items: center;
}
.projectHeader {
display: flex;
align-items: center;
cursor: pointer;
padding: 5px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 4px;
margin-top: 10px;
background-color: #457bad;
color: white;
transition: background-color 0.3s;
}
.projectHeader:hover {
background-color: var(--hover-background);
color: #2d353c;
}
.sprintList {
padding-left: 20px; /* Indentation for sprints under the project */
}
.sprintHeader2 {
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.1);
width: 100%;
padding-left: 5px;
padding-top: 5px;
padding-bottom: 5px;
margin-left: 20px;
align-items: center;
cursor: pointer;
background-color: rgba(162, 162, 162, 0.1);
}
.sprintHeader {
margin-top: 5px;
display: flex;
align-items: center;
cursor: pointer;
padding-left: 5px;
padding-top: 5px;
padding-bottom: 5px;
/* margin-left: 20px; */
transition: background-color 0.3s;
}
.sprintHeader2:hover {
background-color: var(--hover-background);
}
.criteriaTable {
padding-left: 35px; /* Further indentation for the criteria table */
}
.link {
display: flex;
align-items: center;
text-decoration: none;
font-size: var(--mantine-font-size-sm);
color: rgb(255, 255, 255);
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
margin: var(--mantine-spacing-xs);
border-radius: var(--mantine-radius-sm);
font-weight: 500;
cursor: pointer;
background-color: #00a164;
@mixin hover {
/* background-color: #035737; */
color: var(--mantine-color-white);
.linkIcon {
color: var(--mantine-color-white);
}
}
&[data-active] {
&,
&:hover {
box-shadow: var(--mantine-shadow-sm);
background-color: #6089afb7;
color: var(--mantine-color-white);
.linkIcon {
color: var(--mantine-color-white);
}
}
}
}
.linkIcon {
color: rgb(255, 255, 255);
width: rem(20px);
height: rem(22px);
}

View File

@ -0,0 +1,423 @@
import {
evaluation,
getAllTechByUserId,
getAllUser,
sprintReview
} from '@/api/Admin'
import DataTableAll from '@/components/DataTable/DataTable'
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
import { get } from '@/rtk/helpers/apiService'
import { Box, Button, Loader, Select, Text, Title } from '@mantine/core'
import { DateInput } from '@mantine/dates'
import { notifications } from '@mantine/notifications'
import axios from 'axios'
import moment from 'moment'
import { useEffect, useState } from 'react'
import classes from './StaffEvaluation.module.css'
interface User {
id: number
name: string
email: string
email_verified_at: string | null
permission: string
remember_token: string | null
created_at: string | null
updated_at: string | null
}
interface Filter {
userID: string
fromDate: Date | null
toDate: Date | null
// other properties of the filter object
}
interface DataTechnical {
id: number
name: string
level: number
point: number
updated_at: string
}
const StaffEvaluation = () => {
const [loading, setLoading] = useState(false)
const [loadingTechnical, setLoadingTechnical] = useState(false)
const [dataProfile, setDataProfile] = useState<any>([])
const [dataTechnical, setDataTechnical] = useState<DataTechnical[]>([])
const [listUsers, setListUsers] = useState<User[]>([])
const [filter, setFilter] = useState<Filter>({
userID: '',
fromDate: null,
toDate: null,
})
console.log(filter, 'filter')
const getListUser = async () => {
try {
const params = {}
const res = await get(getAllUser, params)
if (res.status) {
return res.data
}
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
}
return []
}
const downloadFile = async () => {
await axios({
url: evaluation,
method: 'GET',
responseType: 'blob',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwMDAvYXBpL3YxL2FkbWluL2xvZ2luIiwiaWF0IjoxNzI2ODE3ODU3LCJleHAiOjE3MjY5MDQyNTcsIm5iZiI6MTcyNjgxNzg1NywianRpIjoid0kzeXM5SGZiV21wS3pONSIsInN1YiI6IjE2IiwicHJ2IjoiZDJmZjI5MzM5YThhM2U4MmMzNTgyYTVhOGU3MzlkZjE3ODliYjEyZiJ9.oEHW0cIlQkawYQMVZnz5TZ5twzY18301eLmXjC55LfY`,
},
params: {
...filter,
userID: 3,
},
})
.then((response) => {
const fileURL = window.URL.createObjectURL(new Blob([response.data]))
const fileLink = document.createElement('a')
fileLink.href = fileURL
fileLink.setAttribute('download', 'RENAME_WORD_FILE.docx') // -------------> RENAME_WORD_FILE
document.body.appendChild(fileLink)
fileLink.click()
fileLink.remove()
})
.catch((error) => {
console.error('Error downloading the file:', error)
})
}
useEffect(() => {
const fetchData = async () => {
const result = await getListUser()
setListUsers(result ?? [])
}
fetchData()
}, [])
const getListProfilesData = async (filterSearch: Filter) => {
try {
const params = {
userID: filterSearch.userID ?? '',
fromDate: filterSearch.fromDate
? moment(filterSearch.fromDate).format('YYYY-MM-DD')
: null,
toDate: filterSearch.toDate
? moment(filterSearch.toDate).format('YYYY-MM-DD')
: null,
}
const res = await get(sprintReview, params)
if (res.status) {
return res.data
}
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
}
return []
}
const getListTechnicalByUserId = async (id: string) => {
try {
const params = {}
const res = await get(`${getAllTechByUserId}/${id}`, params)
if (res.status) {
return res.data
}
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
}
return []
}
useEffect(() => {
if (filter?.userID) {
setLoading(true)
const fetchData = async () => {
const result = await getListProfilesData(filter)
setDataProfile(result ?? [])
setLoading(false)
}
fetchData()
}
}, [filter])
useEffect(() => {
if (filter?.userID) {
setLoadingTechnical(true)
const fetchData = async () => {
const result = await getListTechnicalByUserId(filter?.userID)
setDataTechnical(result ?? [])
setLoadingTechnical(false)
}
fetchData()
}
}, [filter?.userID])
const columns = [
{
name: 'level',
size: '10%',
header: 'Level',
render: (row: any) => {
if (row.level)
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
}}
>
{row?.level}
</div>
)
},
},
{
name: 'name',
size: '',
header: 'Name',
},
{
name: 'point',
size: '10%',
header: 'Point',
render: (row: any) => {
if (row?.point > 0)
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
}}
>
{row?.point}
</div>
)
},
},
{
name: 'updated_at',
size: '25%',
header: 'Last update',
render: (row: any) => {
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
}}
>
{moment(row?.updated_at).format('DD/MM/YYYY HH:mm:ss')}
</div>
)
},
},
]
const infoTotal = () => {
// Tính tổng point và số lượng point > 0
let totalPoint = 0
let count = 0
dataTechnical.forEach((item) => {
if (item.point > 0) {
totalPoint += item.point
count++
}
})
const averagePoint = count > 0 ? (totalPoint / count).toFixed(2) : '0.00'
return (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Text mr={20} fs={'italic'}>
Avg: {averagePoint}
</Text>
<Text fs={'italic'}>Total: {totalPoint}</Text>
</div>
)
}
return (
<div>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Staff Evaluation
</h3>
</div>
<Box w="100%" display={'flex'} mt={15} ml={10}>
<Box w="50%" display={'flex'}>
<Text
mr={'xs'}
style={{ alignContent: 'center' }}
fw={600}
size={'md'}
>
User:
</Text>
<Select
style={{ width: '50%' }}
label={''}
placeholder="Select user"
maxLength={255}
required
data={listUsers.map((i: User) => ({
value: i.id.toString(),
label: i.name,
}))}
value={filter.userID}
onChange={(e) => setFilter({ ...filter, userID: e! })}
/>
</Box>
<Box
w="50%"
display={'flex'}
style={{ justifyContent: 'flex-end' }}
mr={10}
>
<Button
// m={5}
onClick={() => downloadFile()}
>
Export
</Button>
</Box>
</Box>
<Box className={classes.userInfoSection} display="flex">
<Box className={classes.projectInvolvement}>
<Box
w="100%"
display={'flex'}
mt={15}
style={{ justifyContent: 'space-evenly' }}
>
<Box display={'flex'} mr={10}>
<Text
mr={'xs'}
style={{ alignContent: 'center' }}
fw={600}
size={'md'}
>
From Date:
</Text>
<DateInput
placeholder="Select date"
clearable
required
label={''}
value={filter.fromDate ? new Date(filter.fromDate) : null}
valueFormat="DD/MM/YYYY"
onChange={(e) => setFilter({ ...filter, fromDate: e! })}
></DateInput>
</Box>
<Box display={'flex'} mr={10}>
<Text
mr={'xs'}
style={{ alignContent: 'center' }}
fw={600}
size={'md'}
>
To Date:
</Text>
<DateInput
placeholder="Select date"
clearable
required
label={''}
value={filter.toDate ? new Date(filter.toDate) : null}
valueFormat="DD/MM/YYYY"
onChange={(e) => setFilter({ ...filter, toDate: e! })}
></DateInput>
</Box>
</Box>
<Box
style={{
marginTop: '10%',
textAlign: 'center',
display: loading ? 'block' : 'none',
// display: 'none',
}}
>
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
<Text fw={600} c={'gray'}>
Loading . . .
</Text>
</Box>
{!loading && dataProfile.length == 0 && (
<Box
style={{
marginTop: '10%',
textAlign: 'center',
display: 'block',
}}
>
<Text fw={600} c={'gray'}>
No Data Sprint
</Text>
</Box>
)}
{!loading && (
<ProjectInvolvement dataProfile={dataProfile} page="admin" />
)}
</Box>
<Box className={classes.sidebar}>
<Title order={3} className={classes.titleSidebar}>
Technicals
</Title>
{loadingTechnical ? (
<Box
style={{
marginTop: '10%',
textAlign: 'center',
display: 'block',
}}
>
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
<Text fw={600} c={'gray'}>
Loading . . .
</Text>
</Box>
) : (
<DataTableAll
data={dataTechnical}
columns={columns}
size=""
searchInput
infoTotal={infoTotal()}
/>
)}
</Box>
</Box>
</div>
)
}
export default StaffEvaluation

View File

@ -8,6 +8,7 @@ import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
import PageNotFound from '@/pages/NotFound/NotFound'
import Profile from '@/pages/Profile/Profile'
import SprintReview from '@/pages/SprintReview/SprintReview'
import StaffEvaluation from '@/pages/StaffEvaluation/StaffEvaluation'
import TestReport from '@/pages/TestReport/TestReport'
import Tickets from '@/pages/Tickets/Tickets'
import TicketsManagement from '@/pages/TicketsManagement/TicketsManagement'
@ -204,6 +205,20 @@ const mainRoutes = [
</ProtectedRoute>
),
},
{
path: '/staff-avaluation',
element: (
<ProtectedRoute mode="route" permission="admin">
<BasePage
main={
<>
<StaffEvaluation />
</>
}
></BasePage>
</ProtectedRoute>
),
},
// {
// path: '/packages',
// element: (

View File

@ -4,3 +4,5 @@ pyzbar
opencv-python
qrcode
pyautogui
requests
pillow