diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/EvaluationController.php b/BACKEND/Modules/Admin/app/Http/Controllers/EvaluationController.php index e4fa8bb..bb31f73 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/EvaluationController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/EvaluationController.php @@ -8,9 +8,11 @@ use App\Services\JiraService; use Carbon\Carbon; use Modules\Admin\app\Models\TechnicalUser; use Illuminate\Http\Request; +use Modules\Admin\app\Models\ProjectReview; use Modules\Admin\app\Models\UserCriteria; use PhpOffice\PhpWord\IOFactory; use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\SimpleType\Jc; class EvaluationController extends Controller { @@ -222,6 +224,50 @@ class EvaluationController extends Controller 'spaceAfter' => 600, ]); } + + // **ProjectReview Section** + // Fetch Project Reviews + $projectReviews = ProjectReview::where('user_id', $user->id); + if ($startDate && $endDate) { + $projectReviews->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']); + } elseif ($startDate) { + $projectReviews->where('updated_at', '>=', $startDate); + } elseif ($endDate) { + $projectReviews->where('updated_at', '<=', $endDate . ' 23:59:59'); + } + + if ($projectReviews->get()->count() > 0) { + $section->addText("Project Reviews", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]); + + $table = $section->addTable([ + 'borderColor' => '000000', + 'borderSize' => 6, + 'cellMargin' => 80, + ]); + + // Table Header + $table->addRow(); + $table->addCell(3500)->addText('Project Name', ['bold' => true]); + $table->addCell(2500)->addText('Role', ['bold' => true]); + $table->addCell(5000)->addText('Note', ['bold' => true]); + $table->addCell(2500)->addText('Created At', ['bold' => true]); + $table->addCell(2500)->addText('Updated At', ['bold' => true]); + + + + foreach ($projectReviews->get() as $review) { + $table->addRow(); + $table->addCell(3500)->addText($review->name); + $table->addCell(2500)->addText($review->role); + $table->addCell(5000)->addText($review->note); + $table->addCell(2500)->addText(Carbon::parse($review->created_at)->format('d/m/Y H:i:s')); + $table->addCell(2500)->addText(Carbon::parse($review->updated_at)->format('d/m/Y H:i:s')); + } + + $section->addText(' ', [], [ + 'spaceAfter' => 600, + ]); + } if ($technicalData) $section->addPageBreak(); //Technical @@ -260,4 +306,156 @@ class EvaluationController extends Controller return response()->download($tempFile, "$user->name.docx")->deleteFileAfterSend(true); } + + public function reportAllUsers(Request $request) + { + $request->validate([ + 'fromDate' => 'nullable|date', + 'toDate' => 'nullable|date', + ]); + + $startDate = $request->input('fromDate'); + $endDate = $request->input('toDate'); + + $users = User::all(); + + $phpWord = new PhpWord(); + $phpWord->setDefaultFontName('Times New Roman'); + $phpWord->setDefaultFontSize(12); + $section = $phpWord->addSection(); + + foreach ($users as $index => $user) { + $userEmail = $user->email; + + + // Add user heading + $section->addText("Staff Evaluation", ['bold' => true, 'size' => 20, 'color' => '000000'], ['align' => 'center']); + + if ($startDate) { + $fromDate = Carbon::parse($startDate)->format('d-m-Y'); + $section->addText("From: " . $fromDate, ['size' => 12], ['align' => 'end']); + } + if ($endDate) { + $toDate = Carbon::parse($endDate)->format('d-m-Y'); + $section->addText("To: " . $toDate, ['size' => 12], ['align' => 'end']); + } + + $section->addText("{$user->name}", ['bold' => true, 'size' => 14, 'color' => '000000'], ['spaceAfter' => 400]); + + // **Projects Data** + $projectsData = self::getProjectReviewByParams($startDate, $endDate, $userEmail); + if (!empty($projectsData)) { + 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]); + + $table = $section->addTable(['borderSize' => 6, 'cellMargin' => 80]); + $table->addRow(); + $table->addCell(3000)->addText('Criteria', ['bold' => true]); + $table->addCell(3000)->addText('Note', ['bold' => true]); + $table->addCell(2500)->addText('Created By', ['bold' => true]); + $table->addCell(1500)->addText('Point', ['bold' => true]); + + foreach ($sprint['criterias'] as $criteria) { + $table->addRow(); + $table->addCell(3000)->addText($criteria['criteria']); + $table->addCell(3000)->addText($criteria['note']); + $table->addCell(2500)->addText($criteria['createdBy']); + $table->addCell(1500)->addText($criteria['point'] > 0 ? $criteria['point'] : ''); + } + } + $section->addText(' ', [], [ + 'spaceAfter' => 600, + ]); + } + } + + + + // **ProjectReview Section** + // Fetch Project Reviews + $projectReviews = ProjectReview::where('user_id', $user->id); + if ($startDate && $endDate) { + $projectReviews->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']); + } elseif ($startDate) { + $projectReviews->where('updated_at', '>=', $startDate); + } elseif ($endDate) { + $projectReviews->where('updated_at', '<=', $endDate . ' 23:59:59'); + } + if ($projectReviews->get()->count() > 0) { + $section->addText("Project Reviews", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]); + + $table = $section->addTable([ + 'borderColor' => '000000', + 'borderSize' => 6, + 'cellMargin' => 80, + ]); + + // Table Header + $table->addRow(); + $table->addCell(3500)->addText('Project Name', ['bold' => true]); + $table->addCell(2500)->addText('Role', ['bold' => true]); + $table->addCell(5000)->addText('Note', ['bold' => true]); + $table->addCell(2500)->addText('Created At', ['bold' => true]); + $table->addCell(2500)->addText('Updated At', ['bold' => true]); + + + foreach ($projectReviews->get() as $review) { + $table->addRow(); + $table->addCell(3500)->addText($review->name); + $table->addCell(2500)->addText($review->role); + $table->addCell(5000)->addText($review->note); + $table->addCell(2500)->addText(Carbon::parse($review->created_at)->format('d/m/Y H:i:s')); + $table->addCell(2500)->addText(Carbon::parse($review->updated_at)->format('d/m/Y H:i:s')); + } + + $section->addText(' ', [], [ + 'spaceAfter' => 600, + ]); + } + + // **Technical Section** + $section->addText("Technicals", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]); + + $table = $section->addTable([ + 'borderColor' => '000000', + 'borderSize' => 6, + 'cellMargin' => 80, + ]); + + $table->addRow(); + $table->addCell(1500)->addText('Level', ['bold' => true]); + $table->addCell(3500)->addText('Name', ['bold' => true]); + $table->addCell(2500)->addText('Point', ['bold' => true]); + $table->addCell(2500)->addText('Last Update', ['bold' => true]); + + // Fetch Technical Data + $technicalData = TechnicalController::getDataTechnicalsByUserId($user->id); + + foreach ($technicalData as $technical) { + $updated_at = $technical['updated_at'] ? Carbon::parse($technical['updated_at'])->format('d/m/Y H:i:s') : null; + $table->addRow(); + $table->addCell(1500)->addText($technical['level']); + $table->addCell(3500)->addText($technical['name']); + $table->addCell(2500)->addText($technical['point']); + $table->addCell(2500)->addText($updated_at); + } + + // Add page break between users (except last one) + if ($index < count($users) - 1) { + $section->addPageBreak(); + } + } + + // Save & Download Word File + $tempFile = tempnam(sys_get_temp_dir(), 'word'); + $objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007'); + $objWriter->save($tempFile); + + return response()->download($tempFile, "All_Users_Report.docx")->deleteFileAfterSend(true); + } + + } diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/ProjectReviewController.php b/BACKEND/Modules/Admin/app/Http/Controllers/ProjectReviewController.php new file mode 100644 index 0000000..6c4f3f4 --- /dev/null +++ b/BACKEND/Modules/Admin/app/Http/Controllers/ProjectReviewController.php @@ -0,0 +1,91 @@ +validate([ + 'userID' => 'required|exists:users,id', + 'fromDate' => 'nullable|date', + 'toDate' => 'nullable|date', + ]); + $userID = $request->input('userID'); + $startDate = $request->input('fromDate'); + $endDate = $request->input('toDate'); + $projectsData = ProjectReview::where('user_id', $userID); + + if ($startDate && $endDate) { + $projectsData->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']); + } elseif ($startDate) { + $projectsData->where('updated_at', '>=', $startDate); + } elseif ($endDate) { + $projectsData->where('updated_at', '<=', $endDate . ' 23:59:59'); + } + return AbstractController::ResultSuccess($projectsData->get()); + } + + /** + * Store a newly created resource in storage. + */ + public function create(Request $request) + { + $request->validate([ + 'name' => 'required|string', + 'role' => 'required|string', + 'note' => 'required|string', + 'user_id' => 'required|exists:users,id', + ]); + + $review = ProjectReview::create($request->all()); + return response()->json($review, 201); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request) + { + $request->validate([ + 'name' => 'sometimes|required|string', + 'role' => 'sometimes|required|string', + 'note' => 'sometimes|required|string', + 'user_id' => 'sometimes|required|exists:users,id', + ]); + $id = $request->get('id'); + $projectReview = ProjectReview::find($id); + $payload = $request->all(); + // if ($request->has('created_at')) { + // $created_at = Carbon::create($request->get('created_at'))->setTimezone(env('TIME_ZONE')); + // $payload['created_at'] = $created_at; + // } + if ($projectReview) { + $projectReview->update($payload); + } + return response()->json([ + 'data' => $projectReview, + 'status' => true + ]); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Request $request) + { + $id = $request->get('id'); + ProjectReview::destroy($id); + return response()->json(['message' => 'Deleted successfully', 'status' => true]); + } +} diff --git a/BACKEND/Modules/Admin/app/Models/ProjectReview.php b/BACKEND/Modules/Admin/app/Models/ProjectReview.php new file mode 100644 index 0000000..d38f06c --- /dev/null +++ b/BACKEND/Modules/Admin/app/Models/ProjectReview.php @@ -0,0 +1,19 @@ +belongsTo(User::class); + } +} diff --git a/BACKEND/Modules/Admin/routes/api.php b/BACKEND/Modules/Admin/routes/api.php index d3aef1d..01f51bd 100755 --- a/BACKEND/Modules/Admin/routes/api.php +++ b/BACKEND/Modules/Admin/routes/api.php @@ -17,6 +17,7 @@ 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\ProjectReviewController; use Modules\Admin\app\Http\Controllers\ProfileController; use Modules\Admin\app\Http\Controllers\TechnicalController; use Modules\Admin\app\Http\Controllers\TestCaseForSprintController; @@ -192,6 +193,11 @@ Route::middleware('api') 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::get('/report-all-users', [EvaluationController::class, 'reportAllUsers'])->middleware('check.permission:admin'); + Route::get('/project-review', [ProjectReviewController::class, 'getListReviews'])->middleware('check.permission:admin'); + Route::post('/project-review/create', [ProjectReviewController::class, 'create'])->middleware('check.permission:admin'); + Route::post('/project-review/update', [ProjectReviewController::class, 'update'])->middleware('check.permission:admin'); + Route::get('/project-review/delete', [ProjectReviewController::class, 'destroy'])->middleware('check.permission:admin'); }); Route::group([ diff --git a/BACKEND/database/migrations/2025_02_06_100745_create_project_reviews.php b/BACKEND/database/migrations/2025_02_06_100745_create_project_reviews.php new file mode 100644 index 0000000..d6603b7 --- /dev/null +++ b/BACKEND/database/migrations/2025_02_06_100745_create_project_reviews.php @@ -0,0 +1,30 @@ +id(); + $table->string('name'); + $table->string('role'); + $table->longText('note'); + $table->foreignId('user_id')->constrained('users')->onDelete('cascade'); // Khóa ngoại tới bảng users + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('project_reviews'); + } +}; diff --git a/FRONTEND/src/api/Admin.ts b/FRONTEND/src/api/Admin.ts index ec3addc..1c9a70e 100755 --- a/FRONTEND/src/api/Admin.ts +++ b/FRONTEND/src/api/Admin.ts @@ -37,7 +37,8 @@ export const getListMaster = API_URL + 'v1/admin/category/get-list-master' export const getLeaveManagement = API_URL + 'v1/admin/leave-management' export const updateNoteLeave = API_URL + 'v1/admin/leave-management/saveNoteLeave' -export const exportLeaveManagement = API_URL + 'v1/admin/leave-management/export' +export const exportLeaveManagement = + API_URL + 'v1/admin/leave-management/export' //Tickets export const getTickets = API_URL + 'v1/admin/ticket/all' @@ -79,14 +80,24 @@ export const updateProfilesData = API_URL + 'v1/admin/criterias/profiles-data/update' export const listUserTechnical = API_URL + 'v1/admin/technical/get-tech-of-user' -export const updateUserTechnical = API_URL + 'v1/admin/technical/technicals-user/update' +export const updateUserTechnical = + API_URL + 'v1/admin/technical/technicals-user/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 evaluationReportAllUsers = + API_URL + 'v1/admin/evaluation/report-all-users' export const sprintReview = API_URL + 'v1/admin/evaluation/sprint-review' +export const projectReview = API_URL + 'v1/admin/evaluation/project-review' +export const projectReviewAdd = + API_URL + 'v1/admin/evaluation/project-review/create' +export const projectReviewUpdate = + API_URL + 'v1/admin/evaluation/project-review/update' +export const projectReviewDelete = + API_URL + 'v1/admin/evaluation/project-review/delete' export const getAllFilesInProfiles = API_URL + 'v1/admin/profile/all-files' export const updateProfileFolder = API_URL + 'v1/admin/profile/update-profile' diff --git a/FRONTEND/src/components/DataTable/DataTable.tsx b/FRONTEND/src/components/DataTable/DataTable.tsx index ac42f76..627d18d 100755 --- a/FRONTEND/src/components/DataTable/DataTable.tsx +++ b/FRONTEND/src/components/DataTable/DataTable.tsx @@ -84,6 +84,7 @@ export const DataTableAll = ({ checkBox, size, infoTotal, + componentRight, }: { data: any[] columns: Column[] @@ -92,6 +93,7 @@ export const DataTableAll = ({ checkBox?: boolean size: string infoTotal?: React.ReactNode // Set the type to ReactNode to allow JSX elements + componentRight?: React.ReactNode }) => { const [Tdata, setTData] = useState(data) // const [tempData, setTempData] = useState([]) @@ -325,10 +327,7 @@ export const DataTableAll = ({ )} - + {infoTotal} @@ -368,6 +367,7 @@ export const DataTableAll = ({ }} /> + {componentRight} { const [loading, setLoading] = useState(false) const [loadingTechnical, setLoadingTechnical] = useState(false) const [dataProfile, setDataProfile] = useState([]) const [dataTechnical, setDataTechnical] = useState([]) + const [dataProjectReview, setDataProjectReview] = useState< + DataProjectReview[] + >([]) const [listUsers, setListUsers] = useState([]) const [filter, setFilter] = useState({ userID: '', fromDate: null, toDate: null, }) + const [action, setAction] = useState('') + const [item, setItem] = useState({ id: 0 }) + const [disableBtn, setDisableBtn] = useState(false) + const [activeBtn, setActiveBtn] = useState(false) + const [loadingExport, setLoadingExport] = useState(false) + const [loadingExportAll, setLoadingExportAll] = useState(false) + + const form = useForm({ + initialValues: { + id: 0, + name: '', + role: '', + note: '', + }, + validate: { + name: (value) => + value.length === 0 ? 'Please enter project name' : null, + role: (value) => (value.length === 0 ? 'Please enter role' : null), + note: (value) => (value.length === 0 ? 'Please enter note' : null), + }, + }) const getListUser = async () => { try { @@ -92,14 +152,19 @@ const StaffEvaluation = () => { ? moment(filterSearch.toDate).format('YYYY-MM-DD') : null, } - + const user = listUsers.find( + (el) => el.id.toString() === filterSearch.userID, + ) + setLoadingExport(true) const res = await getDownloadFile(evaluation, params) if (res.status) { const fileURL = window.URL.createObjectURL(new Blob([res.data])) const fileLink = document.createElement('a') - const fileName = `EXPORT_SPRINT_REVIEW_AND_TECHNICAL_EVALUATION_${getFormattedDateTime()}.docx` + const fileName = `STAFF_EVALUATION_${user?.name + ?.split(' ') + .join('_')}_${getFormattedDateTime()}.docx` fileLink.href = fileURL fileLink.setAttribute('download', fileName) @@ -108,6 +173,43 @@ const StaffEvaluation = () => { fileLink.click() fileLink.remove() } + setLoadingExport(false) + } catch (error: any) { + notifications.show({ + title: 'Error', + message: error.message ?? error, + color: 'red', + }) + } + return [] + } + const downloadFileAll = async (filterSearch: Filter) => { + try { + const params = { + fromDate: filterSearch.fromDate + ? moment(filterSearch.fromDate).format('YYYY-MM-DD') + : null, + toDate: filterSearch.toDate + ? moment(filterSearch.toDate).format('YYYY-MM-DD') + : null, + } + setLoadingExportAll(true) + const res = await getDownloadFile(evaluationReportAllUsers, params) + + if (res.status) { + const fileURL = window.URL.createObjectURL(new Blob([res.data])) + const fileLink = document.createElement('a') + + const fileName = `STAFF_EVALUATION_All_USERS_${getFormattedDateTime()}.docx` + + fileLink.href = fileURL + fileLink.setAttribute('download', fileName) + document.body.appendChild(fileLink) + + fileLink.click() + fileLink.remove() + } + setLoadingExportAll(false) } catch (error: any) { notifications.show({ title: 'Error', @@ -168,12 +270,39 @@ const StaffEvaluation = () => { return [] } + const getListProjectReview = 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(projectReview, 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) + const resultProject = await getListProjectReview(filter) setDataProfile(result ?? []) + setDataProjectReview(resultProject ?? []) setLoading(false) } fetchData() @@ -284,6 +413,148 @@ const StaffEvaluation = () => { ) } + + const columnsProjectReview = [ + // { + // name: 'id', + // size: '5%', + // header: 'Num', + // render: (row: any) => { + // return ( + // + // {row?.id ? row.id : ''} + // + // ) + // }, + // }, + { + name: 'name', + size: '15%', + header: 'Project Name', + }, + { + name: 'role', + size: '15%', + header: 'Role', + }, + { + name: 'note', + size: '', + header: 'Note', + }, + { + name: 'created_at', + size: '10%', + header: 'Created at', + render: (row: any) => { + if (row?.created_at) + return ( +
+ {moment(row?.created_at).format('DD/MM/YYYY HH:mm:ss')} +
+ ) + }, + }, + { + name: 'updated_at', + size: '10%', + header: 'Last update', + render: (row: any) => { + if (row?.updated_at) + return ( +
+ {moment(row?.updated_at).format('DD/MM/YYYY HH:mm:ss')} +
+ ) + }, + }, + { + name: '#', + size: '5%', + header: 'Action', + render: (row: DataProjectReview) => { + return ( + + { + setAction('edit') + form.setValues(row) + }} + width={20} + height={20} + /> + { + setAction('delete') + setItem(row) + }} + width={20} + height={20} + /> + + ) + }, + }, + ] + + const handleCreate = async (values: DataProjectReview) => { + try { + const { id, ...data } = values + const res = await post(projectReviewAdd, { + ...data, + user_id: filter.userID, + }) + if (res.id) { + setAction('') + form.reset() + const resultProject = await getListProjectReview(filter) + setDataProjectReview(resultProject ?? []) + } + } catch (error) { + console.log(error) + } + } + + const handleUpdate = async (values: DataProjectReview) => { + try { + const res = await update(projectReviewUpdate, { + ...values, + user_id: filter.userID, + }) + if (res) { + setAction('') + form.reset() + const resultProject = await getListProjectReview(filter) + setDataProjectReview(resultProject ?? []) + } + } catch (error) { + console.log(error) + } + } + + const handleDelete = async (id: number) => { + try { + await Xdelete(projectReviewDelete, { id: id }, async () => { + const resultProject = await getListProjectReview(filter) + setDataProjectReview(resultProject ?? []) + }) + } catch (error) { + console.log(error) + } + } + return (
@@ -291,10 +562,38 @@ const StaffEvaluation = () => { Admin/ Staff Evaluation + + {loadingExportAll ? ( + + ) : ( + + )} +
- + { User: