From 86f2bb12fc6f7955a6499d3ce4bc804f37393517 Mon Sep 17 00:00:00 2001 From: Truong Vo <41848815+vmtruong301296@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:50:57 +0700 Subject: [PATCH 01/24] =?UTF-8?q?[Ng=C3=A0y=20Ph=C3=A9p]=20C=E1=BA=ADp=20n?= =?UTF-8?q?h=E1=BA=ADt=20l=E1=BA=A1i=20t=C3=AAn=20c=E1=BB=99t=20cho=20b?= =?UTF-8?q?=E1=BA=A3ng=20ng=C3=A0y=20ngh=E1=BB=89=20ph=C3=A9p=20n=C4=83m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/LeaveManagementController.php | 4 ++-- BACKEND/app/Exports/LeaveManagementExport.php | 2 +- BACKEND/app/Jobs/InitializeLeaveDays.php | 6 ++--- BACKEND/app/Models/LeaveDays.php | 2 +- ...4_08_06_013033_create_leave_days_table.php | 2 +- ...ay_to_ld_day_total_in_leave_days_table.php | 22 +++++++++++++++++++ .../pages/LeaveManagement/LeaveManagement.tsx | 8 +++---- 7 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 BACKEND/database/migrations/2025_03_13_070714_rename_ld_day_to_ld_day_total_in_leave_days_table.php diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php index 477b056..9eff7e5 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php @@ -105,7 +105,7 @@ class LeaveManagementController extends Controller 'leaveDay' => [ 'id' => $item->id, 'ld_user_id' => $item->ld_user_id, - 'ld_day' => $item->ld_day, + 'ld_day_total' => $item->ld_day_total, 'ld_year' => $item->ld_year, 'ld_date_additional' => $item->ld_date_additional, 'ld_note' => $item->ld_note, @@ -133,7 +133,7 @@ class LeaveManagementController extends Controller $validatedData = $request->all(); $leaveDays = LeaveDays::find($validatedData['id']); - $leaveDays->ld_day = $validatedData['totalLeave']; + $leaveDays->ld_day_total = $validatedData['totalLeave']; $leaveDays->ld_date_additional = $validatedData['dayAdditional']; // Assuming you have this field to store additional days $leaveDays->ld_note = $validatedData['note']; diff --git a/BACKEND/app/Exports/LeaveManagementExport.php b/BACKEND/app/Exports/LeaveManagementExport.php index 038ade9..236ae42 100644 --- a/BACKEND/app/Exports/LeaveManagementExport.php +++ b/BACKEND/app/Exports/LeaveManagementExport.php @@ -41,7 +41,7 @@ class LeaveManagementExport implements FromArray, WithHeadings, WithStyles, With $stt = 0; foreach ($this->data as $index => $user) { $totalDayOff = 0; - $totalDayLeave = $user['leaveDay']['ld_day'] + $user['leaveDay']['ld_date_additional']; + $totalDayLeave = $user['leaveDay']['ld_day_total'] + $user['leaveDay']['ld_date_additional']; // Tính tổng ngày nghỉ theo tháng $monthlyLeaves = array_fill(1, 12, 0); diff --git a/BACKEND/app/Jobs/InitializeLeaveDays.php b/BACKEND/app/Jobs/InitializeLeaveDays.php index 3c545b5..57ec833 100644 --- a/BACKEND/app/Jobs/InitializeLeaveDays.php +++ b/BACKEND/app/Jobs/InitializeLeaveDays.php @@ -34,7 +34,7 @@ class InitializeLeaveDays implements ShouldQueue public function handle(): void { $users = User::get(); - $ld_day = 12; + $ld_day_total = 12; foreach ($users as $user) { // Kiểm tra xem dữ liệu của user này đã tồn tại cho năm hiện tại chưa $existingData = LeaveDays::where('ld_user_id', $user->id) @@ -55,7 +55,7 @@ class InitializeLeaveDays implements ShouldQueue $ld_note = ''; if ($previousYearData) { - $ld_date_additional = $previousYearData->ld_day + $previousYearData->ld_date_additional; + $ld_date_additional = $previousYearData->ld_day_total + $previousYearData->ld_date_additional; $totalLeaveDaysByMonth = Notes::join('categories', function ($join) { $join->on('notes.n_time_type', '=', 'categories.c_code') ->where('categories.c_type', 'TIME_TYPE'); @@ -82,7 +82,7 @@ class InitializeLeaveDays implements ShouldQueue // Tạo dữ liệu cho năm hiện tại LeaveDays::insert([ 'ld_user_id' => $user->id, - 'ld_day' => $ld_day, + 'ld_day_total' => $ld_day_total, 'ld_year' => $this->year, 'ld_date_additional' => $ld_date_additional, 'ld_note' => $ld_note, diff --git a/BACKEND/app/Models/LeaveDays.php b/BACKEND/app/Models/LeaveDays.php index 67b2024..8fad637 100644 --- a/BACKEND/app/Models/LeaveDays.php +++ b/BACKEND/app/Models/LeaveDays.php @@ -10,7 +10,7 @@ class LeaveDays extends Model use HasFactory; protected $fillable = [ - 'id', 'ld_user_id', 'ld_day', 'ld_year', 'ld_date_additional', 'ld_note' + 'id', 'ld_user_id', 'ld_day_total', 'ld_year', 'ld_date_additional', 'ld_note' ]; protected $table = 'leave_days'; diff --git a/BACKEND/database/migrations/2024_08_06_013033_create_leave_days_table.php b/BACKEND/database/migrations/2024_08_06_013033_create_leave_days_table.php index bb4db24..ec4db4e 100644 --- a/BACKEND/database/migrations/2024_08_06_013033_create_leave_days_table.php +++ b/BACKEND/database/migrations/2024_08_06_013033_create_leave_days_table.php @@ -14,7 +14,7 @@ return new class extends Migration Schema::create('leave_days', function (Blueprint $table) { $table->id(); $table->integer('ld_user_id'); // Giả định user_id là khóa ngoại - $table->float('ld_day'); + $table->float('ld_day_total'); $table->integer('ld_year'); $table->float('ld_date_additional')->default(0); $table->text('ld_note')->nullable(); diff --git a/BACKEND/database/migrations/2025_03_13_070714_rename_ld_day_to_ld_day_total_in_leave_days_table.php b/BACKEND/database/migrations/2025_03_13_070714_rename_ld_day_to_ld_day_total_in_leave_days_table.php new file mode 100644 index 0000000..51b4754 --- /dev/null +++ b/BACKEND/database/migrations/2025_03_13_070714_rename_ld_day_to_ld_day_total_in_leave_days_table.php @@ -0,0 +1,22 @@ +renameColumn('ld_day_total', 'ld_day_total'); + }); + } + + public function down() + { + Schema::table('leave_days', function (Blueprint $table) { + $table->renameColumn('ld_day_total', 'ld_day_total'); + }); + } +} diff --git a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx index 1a8b020..181e615 100644 --- a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx +++ b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx @@ -40,7 +40,7 @@ interface LeaveDay { id: number ld_user_id: number ld_year: number - ld_day: number + ld_day_total: number ld_date_additional: number ld_note: string created_at: string | null @@ -472,7 +472,7 @@ const LeaveManagement = () => { {data.map((user, index) => { let totalDayOff = 0 let totalDayLeave = - user.leaveDay.ld_day + user.leaveDay.ld_date_additional + user.leaveDay.ld_day_total + user.leaveDay.ld_date_additional let ld_note = user.leaveDay.ld_note return ( @@ -585,9 +585,9 @@ const LeaveManagement = () => { style={{ cursor: 'pointer' }} onClick={() => { let totalLeave = - user.leaveDay.ld_day == 0 + user.leaveDay.ld_day_total == 0 ? '' - : String(user.leaveDay.ld_day) + : String(user.leaveDay.ld_day_total) let dayAdditional = user.leaveDay.ld_date_additional == 0 ? '' From 8ce0d957b159312a7198ff22b6d56fbb732ae3d5 Mon Sep 17 00:00:00 2001 From: Truong Vo <41848815+vmtruong301296@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:54:37 +0700 Subject: [PATCH 02/24] =?UTF-8?q?[Ng=C3=A0y=20Ph=C3=A9p]=20C=E1=BA=ADp=20n?= =?UTF-8?q?h=E1=BA=ADt=20l=E1=BA=A1i=20t=C3=AAn=20c=E1=BB=99t=20cho=20b?= =?UTF-8?q?=E1=BA=A3ng=20ng=C3=A0y=20ngh=E1=BB=89=20ph=C3=A9p=20n=C4=83m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/LeaveManagementController.php | 4 ++-- BACKEND/app/Exports/LeaveManagementExport.php | 2 +- BACKEND/app/Jobs/DeductLeaveDays.php | 8 +++---- BACKEND/app/Jobs/InitializeLeaveDays.php | 12 +++++----- BACKEND/app/Models/LeaveDays.php | 2 +- ...4_08_06_013033_create_leave_days_table.php | 2 +- ..._special_leave_day_to_leave_days_table.php | 22 +++++++++++++++++++ ..._ld_additional_day_in_leave_days_table.php | 22 +++++++++++++++++++ .../pages/LeaveManagement/LeaveManagement.tsx | 8 +++---- 9 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 BACKEND/database/migrations/2025_03_13_075133_add_ld_special_leave_day_to_leave_days_table.php create mode 100644 BACKEND/database/migrations/2025_03_13_075235_rename_ld_date_additional_to_ld_additional_day_in_leave_days_table.php diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php index 9eff7e5..79c3516 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/LeaveManagementController.php @@ -107,7 +107,7 @@ class LeaveManagementController extends Controller 'ld_user_id' => $item->ld_user_id, 'ld_day_total' => $item->ld_day_total, 'ld_year' => $item->ld_year, - 'ld_date_additional' => $item->ld_date_additional, + 'ld_additional_day' => $item->ld_additional_day, 'ld_note' => $item->ld_note, 'created_at' => $item->created_at, 'updated_at' => $item->updated_at, @@ -134,7 +134,7 @@ class LeaveManagementController extends Controller $leaveDays = LeaveDays::find($validatedData['id']); $leaveDays->ld_day_total = $validatedData['totalLeave']; - $leaveDays->ld_date_additional = $validatedData['dayAdditional']; // Assuming you have this field to store additional days + $leaveDays->ld_additional_day = $validatedData['dayAdditional']; // Assuming you have this field to store additional days $leaveDays->ld_note = $validatedData['note']; $leaveDays->save(); diff --git a/BACKEND/app/Exports/LeaveManagementExport.php b/BACKEND/app/Exports/LeaveManagementExport.php index 236ae42..402353b 100644 --- a/BACKEND/app/Exports/LeaveManagementExport.php +++ b/BACKEND/app/Exports/LeaveManagementExport.php @@ -41,7 +41,7 @@ class LeaveManagementExport implements FromArray, WithHeadings, WithStyles, With $stt = 0; foreach ($this->data as $index => $user) { $totalDayOff = 0; - $totalDayLeave = $user['leaveDay']['ld_day_total'] + $user['leaveDay']['ld_date_additional']; + $totalDayLeave = $user['leaveDay']['ld_day_total'] + $user['leaveDay']['ld_additional_day']; // Tính tổng ngày nghỉ theo tháng $monthlyLeaves = array_fill(1, 12, 0); diff --git a/BACKEND/app/Jobs/DeductLeaveDays.php b/BACKEND/app/Jobs/DeductLeaveDays.php index 86dc3b0..2d4bd4a 100644 --- a/BACKEND/app/Jobs/DeductLeaveDays.php +++ b/BACKEND/app/Jobs/DeductLeaveDays.php @@ -36,7 +36,7 @@ class DeductLeaveDays implements ShouldQueue foreach ($users as $user) { $existingData = LeaveDays::where('ld_user_id', $user->id) ->where('ld_year', $this->year) - ->where('ld_date_additional', ">", 0) + ->where('ld_additional_day', ">", 0) ->first(); if (!$existingData) { continue; @@ -59,11 +59,11 @@ class DeductLeaveDays implements ShouldQueue if ($totalLeaveDaysByMonth) { //Nếu ngày phép thừa năm trước chưa sử dụng hết => cập nhật lại ngày đó (Ngày tồn đọng - ngày sử dụng) - if ($existingData->ld_date_additional > $totalLeaveDaysByMonth->leave_days) { + if ($existingData->ld_additional_day > $totalLeaveDaysByMonth->leave_days) { LeaveDays::where('ld_year', $this->year) ->where('ld_user_id', $user->id) ->update([ - 'ld_date_additional' => $totalLeaveDaysByMonth->leave_days, + 'ld_additional_day' => $totalLeaveDaysByMonth->leave_days, ]); } } else { @@ -71,7 +71,7 @@ class DeductLeaveDays implements ShouldQueue LeaveDays::where('ld_year', $this->year) ->where('ld_user_id', $user->id) ->update([ - 'ld_date_additional' => "0", + 'ld_additional_day' => "0", ]); } } diff --git a/BACKEND/app/Jobs/InitializeLeaveDays.php b/BACKEND/app/Jobs/InitializeLeaveDays.php index 57ec833..1d7569f 100644 --- a/BACKEND/app/Jobs/InitializeLeaveDays.php +++ b/BACKEND/app/Jobs/InitializeLeaveDays.php @@ -51,11 +51,11 @@ class InitializeLeaveDays implements ShouldQueue ->where('ld_year', $this->year - 1) ->first(); - $ld_date_additional = 0; + $ld_additional_day = 0; $ld_note = ''; if ($previousYearData) { - $ld_date_additional = $previousYearData->ld_day_total + $previousYearData->ld_date_additional; + $ld_additional_day = $previousYearData->ld_day_total + $previousYearData->ld_additional_day; $totalLeaveDaysByMonth = Notes::join('categories', function ($join) { $join->on('notes.n_time_type', '=', 'categories.c_code') ->where('categories.c_type', 'TIME_TYPE'); @@ -71,9 +71,9 @@ class InitializeLeaveDays implements ShouldQueue ->groupBy(DB::raw('notes.n_year')) ->first(); if ($totalLeaveDaysByMonth) { - $ld_date_additional = $ld_date_additional - $totalLeaveDaysByMonth->leave_days; - if ($ld_date_additional < 0) { - $ld_date_additional = 0; + $ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days; + if ($ld_additional_day < 0) { + $ld_additional_day = 0; } } $ld_note = 'Cộng dồn ngày phép năm cũ'; @@ -84,7 +84,7 @@ class InitializeLeaveDays implements ShouldQueue 'ld_user_id' => $user->id, 'ld_day_total' => $ld_day_total, 'ld_year' => $this->year, - 'ld_date_additional' => $ld_date_additional, + 'ld_additional_day' => $ld_additional_day, 'ld_note' => $ld_note, 'created_at' => now(), 'updated_at' => now(), diff --git a/BACKEND/app/Models/LeaveDays.php b/BACKEND/app/Models/LeaveDays.php index 8fad637..a974950 100644 --- a/BACKEND/app/Models/LeaveDays.php +++ b/BACKEND/app/Models/LeaveDays.php @@ -10,7 +10,7 @@ class LeaveDays extends Model use HasFactory; protected $fillable = [ - 'id', 'ld_user_id', 'ld_day_total', 'ld_year', 'ld_date_additional', 'ld_note' + 'id', 'ld_user_id', 'ld_day_total', 'ld_year', 'ld_additional_day', 'ld_note' ]; protected $table = 'leave_days'; diff --git a/BACKEND/database/migrations/2024_08_06_013033_create_leave_days_table.php b/BACKEND/database/migrations/2024_08_06_013033_create_leave_days_table.php index ec4db4e..bb4db24 100644 --- a/BACKEND/database/migrations/2024_08_06_013033_create_leave_days_table.php +++ b/BACKEND/database/migrations/2024_08_06_013033_create_leave_days_table.php @@ -14,7 +14,7 @@ return new class extends Migration Schema::create('leave_days', function (Blueprint $table) { $table->id(); $table->integer('ld_user_id'); // Giả định user_id là khóa ngoại - $table->float('ld_day_total'); + $table->float('ld_day'); $table->integer('ld_year'); $table->float('ld_date_additional')->default(0); $table->text('ld_note')->nullable(); diff --git a/BACKEND/database/migrations/2025_03_13_075133_add_ld_special_leave_day_to_leave_days_table.php b/BACKEND/database/migrations/2025_03_13_075133_add_ld_special_leave_day_to_leave_days_table.php new file mode 100644 index 0000000..f419e78 --- /dev/null +++ b/BACKEND/database/migrations/2025_03_13_075133_add_ld_special_leave_day_to_leave_days_table.php @@ -0,0 +1,22 @@ +integer('ld_special_leave_day')->nullable(); // Adding the new field + }); + } + + public function down() + { + Schema::table('leave_days', function (Blueprint $table) { + $table->dropColumn('ld_special_leave_day'); // Dropping the field if needed + }); + } +} diff --git a/BACKEND/database/migrations/2025_03_13_075235_rename_ld_date_additional_to_ld_additional_day_in_leave_days_table.php b/BACKEND/database/migrations/2025_03_13_075235_rename_ld_date_additional_to_ld_additional_day_in_leave_days_table.php new file mode 100644 index 0000000..ec600f4 --- /dev/null +++ b/BACKEND/database/migrations/2025_03_13_075235_rename_ld_date_additional_to_ld_additional_day_in_leave_days_table.php @@ -0,0 +1,22 @@ +renameColumn('ld_additional_day', 'ld_additional_day'); + }); + } + + public function down() + { + Schema::table('leave_days', function (Blueprint $table) { + $table->renameColumn('ld_additional_day', 'ld_additional_day'); + }); + } +} \ No newline at end of file diff --git a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx index 181e615..0e4d890 100644 --- a/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx +++ b/FRONTEND/src/pages/LeaveManagement/LeaveManagement.tsx @@ -41,7 +41,7 @@ interface LeaveDay { ld_user_id: number ld_year: number ld_day_total: number - ld_date_additional: number + ld_additional_day: number ld_note: string created_at: string | null updated_at: string | null @@ -472,7 +472,7 @@ const LeaveManagement = () => { {data.map((user, index) => { let totalDayOff = 0 let totalDayLeave = - user.leaveDay.ld_day_total + user.leaveDay.ld_date_additional + user.leaveDay.ld_day_total + user.leaveDay.ld_additional_day let ld_note = user.leaveDay.ld_note return ( @@ -589,9 +589,9 @@ const LeaveManagement = () => { ? '' : String(user.leaveDay.ld_day_total) let dayAdditional = - user.leaveDay.ld_date_additional == 0 + user.leaveDay.ld_additional_day == 0 ? '' - : String(user.leaveDay.ld_date_additional) + : String(user.leaveDay.ld_additional_day) open1() setCustomAddNotes({ ...customAddNotes, From 650cfe1b13c3c3a435318add4ad355faf46ee0c1 Mon Sep 17 00:00:00 2001 From: Truong Vo <41848815+vmtruong301296@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:41:43 +0700 Subject: [PATCH 03/24] =?UTF-8?q?[Ng=C3=A0y=20Ph=C3=A9p]=20C=E1=BA=ADp=20n?= =?UTF-8?q?h=E1=BA=ADt=20lo=E1=BA=A1i=20ph=C3=A9p=20n=E1=BB=99p:=20WFH,=20?= =?UTF-8?q?Ngh=E1=BB=89=20ph=C3=A9p=20n=C4=83m,=20Ngh=E1=BB=89=20kh=C3=B4n?= =?UTF-8?q?g=20l=C6=B0=C6=A1ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...083500_update_name_in_categories_table.php | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 BACKEND/database/migrations/2025_03_13_083500_update_name_in_categories_table.php diff --git a/BACKEND/database/migrations/2025_03_13_083500_update_name_in_categories_table.php b/BACKEND/database/migrations/2025_03_13_083500_update_name_in_categories_table.php new file mode 100644 index 0000000..9cd370f --- /dev/null +++ b/BACKEND/database/migrations/2025_03_13_083500_update_name_in_categories_table.php @@ -0,0 +1,41 @@ +insert([ + [ + 'c_code' => 'LEAVE_WITHOUT_PAY', + 'c_name' => 'Nghỉ không hưởng lương', + 'c_type' => 'REASON', + 'c_value' => "", + 'c_active' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + + DB::table('categories') + ->where('c_name', 'Nghỉ phép') + ->update(['c_name' => 'Nghỉ phép năm']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('categories') + ->where('c_name', 'Nghỉ phép năm') + ->update(['c_name' => 'Nghỉ phép']); + } +}; From c6a9fc28a331c4e7edcdd2c886de73671a4aaf34 Mon Sep 17 00:00:00 2001 From: Joseph Date: Wed, 26 Mar 2025 08:56:06 +0700 Subject: [PATCH 04/24] update url --- FRONTEND/src/pages/Document/Document.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/FRONTEND/src/pages/Document/Document.tsx b/FRONTEND/src/pages/Document/Document.tsx index 703e1e5..383d698 100644 --- a/FRONTEND/src/pages/Document/Document.tsx +++ b/FRONTEND/src/pages/Document/Document.tsx @@ -103,8 +103,12 @@ const Document = () => { if (['doc'].includes(extension!)) { return ( @@ -115,8 +119,12 @@ const Document = () => { if (['xls', 'xlsx'].includes(extension!)) { return ( From 6e2a8c25780d626f0ca3f7084f7aac43f2363234 Mon Sep 17 00:00:00 2001 From: Joseph Date: Wed, 26 Mar 2025 09:29:14 +0700 Subject: [PATCH 05/24] update type file profile upload --- .../Modules/Admin/app/Http/Controllers/ProfileController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php b/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php index 2bc0725..d32762b 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php @@ -169,7 +169,7 @@ class ProfileController extends Controller $name = $request->input('name') ?? auth('admins')->user()->name; // Validate the incoming files $request->validate([ - 'files.*' => 'required|file|mimes:jpg,png,jpeg,pdf,doc,docx|max:5120', // Adjust file types and size limit as needed + 'files.*' => 'required|file|mimes:jpg,png,jpeg,pdf,doc,docx,xlsx,xls,csv|max:5120', // Adjust file types and size limit as needed ]); $uploadedFiles = []; From 4da48df8d7eccabdcdabdcd17c30397435d6bac0 Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 27 Mar 2025 17:05:32 +0700 Subject: [PATCH 06/24] update logic upload file --- .../Http/Controllers/ProfileController.php | 229 +++++- BACKEND/Modules/Admin/routes/api.php | 3 + BACKEND/app/Models/Files.php | 25 + .../2025_03_27_131933_create_files_table.php | 32 + BACKEND/database/seeders/DatabaseSeeder.php | 5 + .../emails/file_upload_notification.blade.php | 72 ++ FRONTEND/src/api/Admin.ts | 5 + .../src/pages/AllProfiles/AllProfiles.tsx | 686 +++++++----------- FRONTEND/src/pages/Profile/Profile.tsx | 380 ++++------ .../components/FileUploadForm.module.css | 121 +++ .../Profile/components/FileUploadForm.tsx | 251 +++++++ 11 files changed, 1126 insertions(+), 683 deletions(-) create mode 100644 BACKEND/app/Models/Files.php create mode 100644 BACKEND/database/migrations/2025_03_27_131933_create_files_table.php create mode 100644 BACKEND/resources/views/emails/file_upload_notification.blade.php create mode 100644 FRONTEND/src/pages/Profile/components/FileUploadForm.module.css create mode 100644 FRONTEND/src/pages/Profile/components/FileUploadForm.tsx diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php b/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php index d32762b..e1c812e 100644 --- a/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php +++ b/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php @@ -10,9 +10,14 @@ use App\Traits\HasFilterRequest; use App\Traits\HasOrderByRequest; use App\Traits\HasSearchRequest; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Log; +use Modules\Admin\app\Models\Admin; use Modules\Admin\app\Models\Sprint; use Modules\Admin\app\Models\UserCriteria; +use App\Models\Files; +use App\DataTransferObjects\FileData; class ProfileController extends Controller { @@ -28,6 +33,7 @@ class ProfileController extends Controller $this->jiraService = $jiraService; } + public function getProfilesData(Request $request) { $user = auth('admins')->user(); @@ -125,7 +131,7 @@ class ProfileController extends Controller $rootFolder = rtrim($rootFolder, '/') . '/'; // Get all files and directories in the specified root folder - $fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder: 'image'.$rootFolder); + $fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder : 'image' . $rootFolder); return response()->json(['data' => $fileList, 'status' => true]); } @@ -185,6 +191,10 @@ class ProfileController extends Controller if (!Storage::disk('public')->exists($othersDirectory)) { Storage::disk('public')->makeDirectory($othersDirectory); } + + $adminEmails = Admin::where('permission', 'like', '%admin%')->pluck('email')->toArray(); + $currentUser = auth('admins')->user(); + if ($request->hasFile('files')) { foreach ($request->file('files') as $file) { // Store the file and get its path @@ -197,6 +207,32 @@ class ProfileController extends Controller $path = $file->storeAs($baseDirectory, $originalFilename, 'public'); } $uploadedFiles[] = $path; + + // Tạo URL đầy đủ cho file + $fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production') + ? env('APP_URL') . '/image/' . str_replace('/storage/', '', Storage::url($path)) + : env('APP_URL') . str_replace('/storage/', '', Storage::url($path)); + + // // Gửi email thông báo cho admin + // foreach ($adminEmails as $adminEmail) { + // $admin = Admin::where('email', $adminEmail)->first(); + // if ($admin) { + // $this->sendFileUploadNotification( + // $admin, + // "File {$originalFilename} đã được tải lên bởi {$currentUser->name}", + // $fileUrl, + // "[APAC Tech] {$currentUser->name} - Đã tải lên file mới" + // ); + // } + // } + + // // Gửi email xác nhận cho người tải lên + // $this->sendFileUploadNotification( + // $currentUser, + // "Bạn đã tải lên file {$originalFilename} thành công", + // $fileUrl, + // "[APAC Tech] {$currentUser->name} - Tải file thành công" + // ); } } @@ -237,4 +273,195 @@ class ProfileController extends Controller 'message' => 'File not found', ], 404); } + + public function sendFileUploadNotification($user, $description, $url, $subject, $note) + { + try { + // Gửi email bất đồng bộ không cần job + dispatch(function() use ($user, $description, $url, $subject, $note) { + Mail::send('emails.file_upload_notification', [ + 'user' => $user, + 'description' => $description, + 'url' => $url, + 'note' => $note + ], function ($message) use ($user, $subject) { + $message->to($user->email) + ->subject($subject); + }); + })->afterResponse(); + + return true; + } catch (\Exception $e) { + Log::error('Error dispatching file upload notification email: ' . $e->getMessage()); + return false; + } + } + + public function uploadFiles(Request $request) + { + try { + $request->validate([ + 'file' => 'required|file|mimes:jpg,jpeg,png,pdf,doc,docx,xls,xlsx,csv|max:5120', + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'user_name' => 'required|string|max:255' + ]); + + $file = $request->file('file'); + $user = auth('admins')->user(); + + // Tạo thư mục cho user nếu chưa tồn tại + $userFolder = 'files/' . $request->user_name; + if (!Storage::disk('public')->exists($userFolder)) { + Storage::disk('public')->makeDirectory($userFolder); + } + + $path = $file->store($userFolder, 'public'); + + $fileRecord = Files::create([ + 'name' => $request->name, + 'url' => $path, + 'type' => $this->getFileType($file->getClientOriginalName()), + 'description' => $request->description, + 'user_id' => Admin::where('name', $request->user_name)->first()->id + ]); + + $currentUser = Admin::where('name', $request->user_name)->first(); + // Gửi email thông báo cho người upload + $fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production') + ? env('APP_URL') . '/image' . Storage::url($path) + : env('APP_URL') . Storage::url($path); + $this->sendFileUploadNotification( + $user, + 'Bạn đã tải lên file "' . $request->name . '" thành công', + $fileUrl, + "[APAC Tech] {$currentUser->name} - Đã tải lên file mới", + $request->description ?? 'No description' + ); + + // Gửi email thông báo cho tất cả admin khác + $otherAdmins = Admin::where('permission', 'like', '%admin%')->get(); + foreach ($otherAdmins as $admin) { + $this->sendFileUploadNotification( + $admin, + 'File "' . $request->name . '" đã được tải lên bởi ' . $user->name, + $fileUrl, + "[APAC Tech] {$currentUser->name} - Đã tải lên file mới", + $request->description ?? 'No description' + ); + } + + return response()->json([ + 'status' => true, + 'message' => 'File uploaded successfully', + 'data' => [ + 'id' => $fileRecord->id, + 'name' => $fileRecord->name, + 'url' => Storage::url($path), + 'type' => $fileRecord->type, + 'description' => $fileRecord->description + ] + ]); + + } catch (\Exception $e) { + return response()->json([ + 'status' => false, + 'message' => $e->getMessage() + ], 500); + } + } + + public function getFiles() + { + try { + $files = Files::with('user')->get() + ->map(function($file) { + return [ + 'id' => $file->id, + 'name' => $file->name, + 'url' => Storage::url($file->url), + 'type' => $file->type, + 'description' => $file->description, + 'created_at' => $file->created_at, + 'user_id' => $file->user_id, + 'user_name' => $file->user->name + ]; + }); + + // Gom nhóm files theo tên user + $groupedFiles = $files->groupBy('user_name') + ->map(function($files) { + return $files->map(function(array $file) { + return (object)[ + 'id' => $file['id'], + 'name' => $file['name'], + 'url' => $file['url'], + 'type' => $file['type'], + 'description' => $file['description'], + 'created_at' => $file['created_at'], + 'user_id' => $file['user_id'] + ]; + }); + }); + + return response()->json([ + 'status' => true, + 'data' => $groupedFiles + ]); + + } catch (\Exception $e) { + return response()->json([ + 'status' => false, + 'message' => $e->getMessage() + ], 500); + } + } + + public function deleteFile($id) + { + try { + $file = Files::findOrFail($id); + $user = auth('admins')->user(); + + if ($file->user_id !== $user->id) { + return response()->json([ + 'status' => false, + 'message' => 'Unauthorized' + ], 403); + } + + Storage::disk('public')->delete($file->url); + $file->delete(); + + return response()->json([ + 'status' => true, + 'message' => 'File deleted successfully' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'status' => false, + 'message' => $e->getMessage() + ], 500); + } + } + + private function getFileType($filename) + { + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + $typeMap = [ + 'pdf' => 'document', + 'doc' => 'document', + 'docx' => 'document', + 'jpg' => 'image', + 'jpeg' => 'image', + 'png' => 'image', + 'xls' => 'spreadsheet', + 'xlsx' => 'spreadsheet', + 'csv' => 'spreadsheet' + ]; + + return $typeMap[$extension] ?? 'other'; + } } diff --git a/BACKEND/Modules/Admin/routes/api.php b/BACKEND/Modules/Admin/routes/api.php index b561918..8f9ee2c 100755 --- a/BACKEND/Modules/Admin/routes/api.php +++ b/BACKEND/Modules/Admin/routes/api.php @@ -174,6 +174,9 @@ Route::middleware('api') Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant'); Route::post('/update-profile', [ProfileController::class, 'updateProfile'])->middleware('check.permission:admin.hr.staff.accountant'); Route::get('/delete-profile-file', [ProfileController::class, 'removeFile'])->middleware('check.permission:admin.hr.staff.accountant'); + Route::get('/files', [ProfileController::class, 'getFiles'])->middleware('check.permission:admin.hr.staff.accountant'); + Route::post('/upload-files', [ProfileController::class, 'uploadFiles'])->middleware('check.permission:admin.hr.staff.accountant'); + Route::delete('/files/{id}', [ProfileController::class, 'deleteFile'])->middleware('check.permission:admin.hr.staff.accountant'); }); Route::group([ diff --git a/BACKEND/app/Models/Files.php b/BACKEND/app/Models/Files.php new file mode 100644 index 0000000..64be814 --- /dev/null +++ b/BACKEND/app/Models/Files.php @@ -0,0 +1,25 @@ +belongsTo(User::class); + } +} diff --git a/BACKEND/database/migrations/2025_03_27_131933_create_files_table.php b/BACKEND/database/migrations/2025_03_27_131933_create_files_table.php new file mode 100644 index 0000000..0951ec1 --- /dev/null +++ b/BACKEND/database/migrations/2025_03_27_131933_create_files_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('url'); + $table->string('type'); + $table->text('description')->nullable(); + $table->foreignId('user_id')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('files'); + } +}; diff --git a/BACKEND/database/seeders/DatabaseSeeder.php b/BACKEND/database/seeders/DatabaseSeeder.php index a9f4519..c067f12 100755 --- a/BACKEND/database/seeders/DatabaseSeeder.php +++ b/BACKEND/database/seeders/DatabaseSeeder.php @@ -18,5 +18,10 @@ class DatabaseSeeder extends Seeder // 'name' => 'Test User', // 'email' => 'test@example.com', // ]); + + $this->call([ + UserSeeder::class, + FileSeeder::class, + ]); } } diff --git a/BACKEND/resources/views/emails/file_upload_notification.blade.php b/BACKEND/resources/views/emails/file_upload_notification.blade.php new file mode 100644 index 0000000..3bbad65 --- /dev/null +++ b/BACKEND/resources/views/emails/file_upload_notification.blade.php @@ -0,0 +1,72 @@ + + + + + Thông báo tải lên file mới + + + + + + \ No newline at end of file diff --git a/FRONTEND/src/api/Admin.ts b/FRONTEND/src/api/Admin.ts index ef404fe..b1c613f 100755 --- a/FRONTEND/src/api/Admin.ts +++ b/FRONTEND/src/api/Admin.ts @@ -119,3 +119,8 @@ export const deleteDocument = API_URL + 'v1/admin/document/delete' // Download File export const downloadFile = API_URL + 'v1/admin/download-file' + +// Files APIs +export const getFiles = API_URL + 'v1/admin/profile/files' +export const uploadFiles = API_URL + 'v1/admin/profile/upload-files' +export const deleteFileById = API_URL + 'v1/admin/profile/files' diff --git a/FRONTEND/src/pages/AllProfiles/AllProfiles.tsx b/FRONTEND/src/pages/AllProfiles/AllProfiles.tsx index 1893703..9873dda 100644 --- a/FRONTEND/src/pages/AllProfiles/AllProfiles.tsx +++ b/FRONTEND/src/pages/AllProfiles/AllProfiles.tsx @@ -1,498 +1,330 @@ -import { - deleteFile, - getAllFilesInProfiles, - updateProfileFolder, -} from '@/api/Admin' -import { Xdelete } from '@/rtk/helpers/CRUD' +import { getFiles, uploadFiles } from '@/api/Admin' import { get } from '@/rtk/helpers/apiService' import { getAccessToken } from '@/rtk/localStorage' import { Box, Button, - FileInput, + Card, + Collapse, Group, Modal, - RenderTreeNodePayload, Stack, Text, TextInput, - Tooltip, - Tree, + Title, } from '@mantine/core' +import { notifications } from '@mantine/notifications' import { + IconChevronDown, + IconDownload, IconFileTypeDocx, IconFileTypePdf, IconFolder, - IconFolderOpen, - IconFolderX, IconListCheck, IconPhoto, + IconSearch, + IconTrash, } from '@tabler/icons-react' import axios from 'axios' import { useEffect, useState } from 'react' +import FileUploadForm from '../Profile/components/FileUploadForm' import classes from './AllProfiles.module.css' -interface FileIconProps { +interface FileData { + id: number name: string - isFolder: boolean - expanded: boolean + url: string + type: string + description?: string + created_at: string } -type TFileProfile = { - label: string - type: string - value: string - children?: TFileProfile[] +interface GroupedFiles { + [key: string]: FileData[] } const AllProfiles = () => { - const [treeData, setTreeData] = useState([]) - const [cv, setCv] = useState() - const [idCard, setIdCard] = useState() - const [transcript, setTranscript] = useState() - const [universityDiploma, setUniversityDiploma] = useState() - const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }]) - const [data, setData] = useState([]) - const [currentName, setCurrentName] = useState('') + const [groupedFiles, setGroupedFiles] = useState({}) + const [currentUser, setCurrentUser] = useState('') const [openedProfile, setOpenedProfile] = useState(false) - function FileIcon({ name, isFolder, expanded }: FileIconProps) { - if (name.endsWith('.pdf')) { - return - } + const [selectedFile, setSelectedFile] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [expandedFolders, setExpandedFolders] = useState<{ + [key: string]: boolean + }>({}) + const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({}) - if (name.endsWith('.doc') || name.endsWith('.docx')) { - return - } - - if ( - name.endsWith('.jpg') || - name.endsWith('.png') || - name.endsWith('.jpeg') || - name.endsWith('.webp') - ) { - return - } - - if (isFolder) { - return expanded ? ( - - ) : ( - - ) - } - - return ( - - ) + const toggleFolder = (userName: string) => { + setExpandedFolders((prev) => ({ + ...prev, + [userName]: !prev[userName], + })) } - function Leaf({ - node, - expanded, - hasChildren, - elementProps, - }: RenderTreeNodePayload) { - return ( - - {!node.children ? ( - - - {node.label} - - ) : ( - <> - - {node.label} - - { - setCurrentName(node.label!.toString()) - setOpenedProfile(true) - }} - /> - - - )} - - ) + const getFileIcon = (type: string) => { + switch (type) { + case 'document': + return + case 'image': + return + default: + return + } } - const handleOtherFileChange = ( - index: number, - field: string, - value: File | string, + const handleSubmit = async ( + e: React.FormEvent, + fileName: string, + description: string, + currentUser: string ) => { - const updatedFiles: any = [...otherFiles] - updatedFiles[index][field] = value - setOtherFiles(updatedFiles) - } - - const addOtherFileInput = () => { - setOtherFiles([...otherFiles, { file: null, type: '' }]) - } - - const handleSubmit = async (e: any) => { e.preventDefault() + setIsLoading(true) const formData = new FormData() + if (selectedFile) { + formData.append('file', selectedFile) + formData.append('name', fileName) + formData.append('description', description) + formData.append('user_name', currentUser) - // Append each selected file to FormData - for (let i = 0; i < otherFiles.length; i++) { - if (otherFiles[i].file !== null && otherFiles[i].type !== '') { - formData.append( - 'files[]', - handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!, - ) + try { + const token = await getAccessToken() + const response = await axios.post(uploadFiles, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer ${token}`, + }, + }) + + if (response.status === 200) { + setSelectedFile(null) + await getAllFiles() + return true + } + return false + } catch (error) { + console.error('Error uploading file:', error) + throw error + } finally { + setIsLoading(false) } } + return false + } - if (cv) { - formData.append('files[]', cv) - } - - if (idCard) { - formData.append('files[]', idCard) - } - - if (transcript) { - formData.append('files[]', transcript) - } - - if (universityDiploma) { - formData.append('files[]', universityDiploma) - } - - formData.append('name', currentName) - - const token = await getAccessToken() + const getAllFiles = async () => { try { - const response = await axios.post(updateProfileFolder, formData, { + const res = await get(getFiles) + if (res.status === true) { + setGroupedFiles(res.data) + } + } catch (error) { + console.log(error) + } + } + + const removeFile = async (id: number) => { + try { + const token = await getAccessToken(); + const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, { headers: { - 'Content-Type': 'multipart/form-data', Authorization: `Bearer ${token}`, }, - }) + }); if (response.status === 200) { - getAllFile() - getTree() - setOtherFiles([]) + notifications.show({ + title: 'Thành công', + message: 'Xóa file thành công', + color: 'green', + }); + await getAllFiles(); } } catch (error) { - console.error('Error uploading files', error) + console.log(error); + notifications.show({ + title: 'Lỗi', + message: 'Không thể xóa file', + color: 'red', + }); } } - const getAllFile = async () => { - try { - const res = await get(getAllFilesInProfiles, { - root_folder: '/storage/profiles/' + currentName, - }) - if (res.status === true) { - setData(res.data) - } - } catch (error) { - console.log(error) - } - } - - const removeFile = async (url: string) => { - try { - await Xdelete(deleteFile, { file_url: url }, getAllFile) - getTree() - } catch (error) { - console.log(error) - } - } - const getTree = async () => { - try { - const res = await get(getAllFilesInProfiles, { - root_folder: '/storage/profiles', - }) - if (res.status === true) { - setTreeData(res.data) - } - } catch (error) { - console.log(error) - } - } - - const handleChangeFileName = (e: File, newName: string) => { - const originalFile = e // Get the original file - const extend = originalFile.name.split('.')[1] - if (originalFile) { - const newFileName = `${newName}.${extend}` // Create new file name - const newFile = new File([originalFile], newFileName, { - type: originalFile.type, - }) // Create new file object - - return newFile // Save the new file object for further processing - } - } - - const checkFileExist = (nameField: string) => { - const file = data.find((f) => f.label.includes(nameField)) - return file - } - useEffect(() => { - getTree() + getAllFiles() }, []) - useEffect(() => { - getAllFile() - }, [currentName]) + const filterFiles = (files: FileData[], searchTerm: string) => { + return files.filter( + (file) => + file.name.toLowerCase().includes(searchTerm.toLowerCase()) || + (file.description && + file.description.toLowerCase().includes(searchTerm.toLowerCase())), + ) + } + return (

- Admin/ - Profiles + Admin/ + Files Management

- - } - /> + + + {Object.entries(groupedFiles).map(([userName, files]) => ( + + toggleFolder(userName)} + style={{ cursor: 'pointer' }} + > + + + {userName} + + + + + + + + + } + value={searchTerms[userName] || ''} + onChange={(e) => + setSearchTerms((prev) => ({ + ...prev, + [userName]: e.target.value, + })) + } + onClick={(e) => e.stopPropagation()} + /> + {filterFiles(files, searchTerms[userName] || '') + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .map((file: FileData) => ( + + + + {getFileIcon(file.type)} + + + {file.name} + + {file.description && ( + + {file.description} + + )} + + Uploaded:{' '} + {new Date(file.created_at).toLocaleDateString()} + + + + + + + + + + ), + )} + + + + ))} + + { setOpenedProfile(false) + setCurrentUser('') + setSelectedFile(null) }} > -
- - - CV - - {`: ${checkFileExist('cv')?.label}`} - - - - { - 0 - setCv(handleChangeFileName(e!, 'cv')) - }} - accept=".pdf,.doc,.docx" - /> - - CCCD - - {`: ${checkFileExist('idCard')?.label}`} - - - - - { - setIdCard(handleChangeFileName(e!, 'idCard')) - }} - accept=".jpg,.jpeg,.png,.pdf" - /> - - Bảng điểm - - {`: ${checkFileExist('transcript')?.label}`} - - - - { - setTranscript(handleChangeFileName(e!, 'transcript')) - }} - accept=".pdf" - /> - - - Bằng đại học - - {`: ${ - checkFileExist('universityDiploma')?.label - }`} - - - - { - setUniversityDiploma( - handleChangeFileName(e!, 'universityDiploma'), - ) - }} - accept=".pdf,.jpg,.jpeg,.png" - /> - - Danh sách file khác: - - {data - .find((f) => f.label === 'others') - ?.children?.map((c, index) => { - return ( - - - {`${c?.label}`} - - - - ) - })} - - {otherFiles.map((fileInput, index) => ( - - - handleOtherFileChange(index, 'file', file!) - } - w={'30%'} - /> - - handleOtherFileChange( - index, - 'type', - e.currentTarget.value, - ) - } - /> - - ))} - - - - -
+ file && setSelectedFile(file)} + removeFile={removeFile} + isLoading={isLoading} + currentUser={currentUser} + />
diff --git a/FRONTEND/src/pages/Profile/Profile.tsx b/FRONTEND/src/pages/Profile/Profile.tsx index 14c6210..e839e65 100644 --- a/FRONTEND/src/pages/Profile/Profile.tsx +++ b/FRONTEND/src/pages/Profile/Profile.tsx @@ -1,18 +1,16 @@ import { - deleteFile, - getAllFilesInProfiles, + getFiles, getProfilesData, listUserTechnical, - updateProfileFolder, updateProfilesData, updateUserTechnical, + uploadFiles } from '@/api/Admin' import { changePassword } from '@/api/Auth' import DataTableAll from '@/components/DataTable/DataTable' import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput' import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement' import { logout } from '@/rtk/dispatches/auth' -import { Xdelete } from '@/rtk/helpers/CRUD' import { get, post, postImage } from '@/rtk/helpers/apiService' import { requirementsPassword } from '@/rtk/helpers/variables' import { useAppDispatch, useAppSelector } from '@/rtk/hooks' @@ -22,16 +20,13 @@ import { Avatar, Box, Button, - FileInput, Flex, - Group, Loader, Modal, PasswordInput, - Stack, Text, TextInput, - Title, + Title } from '@mantine/core' import { notifications } from '@mantine/notifications' import { @@ -46,15 +41,26 @@ import moment from 'moment' import { useCallback, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import classes from './Profile.module.css' +import FileUploadForm from './components/FileUploadForm' const isCompactMenu = false -type TFileProfile = { - label: string - type: string - value: string - children?: TFileProfile[] +// type TFileProfile = { +// label: string +// type: string +// value: string +// children?: TFileProfile[] +// } + +interface FileData { + id: number; + name: string; + url: string; + type: string; + description?: string; + created_at: string; } + const Profile = () => { const user = useAppSelector((state) => state.authentication.user) const userData = getUser() @@ -77,6 +83,11 @@ const Profile = () => { const navigate = useNavigate() const dispatch = useAppDispatch() + const [selectedFile, setSelectedFile] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [data, setData] = useState([]) + const [openedProfile, setOpenedProfile] = useState(false) + const updateAvatar = async (file: File) => { try { const res = await postImage(updateProfilesData, file, 'post') @@ -137,102 +148,113 @@ const Profile = () => { return [] } - const [cv, setCv] = useState() - const [idCard, setIdCard] = useState() - const [transcript, setTranscript] = useState() - const [universityDiploma, setUniversityDiploma] = useState() - const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }]) - const [data, setData] = useState([]) - const [openedProfile, setOpenedProfile] = useState(false) - const handleOtherFileChange = ( - index: number, - field: string, - value: File | string, + // const [cv, setCv] = useState() + // const [idCard, setIdCard] = useState() + // const [transcript, setTranscript] = useState() + // const [universityDiploma, setUniversityDiploma] = useState() + // const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }]) + // const handleOtherFileChange = ( + // index: number, + // field: string, + // value: File | string, + // ) => { + // const updatedFiles: any = [...otherFiles] + // updatedFiles[index][field] = value + // setOtherFiles(updatedFiles) + // } + + // const addOtherFileInput = () => { + // setOtherFiles([...otherFiles, { file: null, type: '' }]) + // } + + const handleSubmit = async ( + e: React.FormEvent, + fileName: string, + description: string, ) => { - const updatedFiles: any = [...otherFiles] - updatedFiles[index][field] = value - setOtherFiles(updatedFiles) - } - - const addOtherFileInput = () => { - setOtherFiles([...otherFiles, { file: null, type: '' }]) - } - - const handleSubmit = async (e: any) => { e.preventDefault() + setIsLoading(true) const formData = new FormData() - // Append each selected file to FormData - for (let i = 0; i < otherFiles.length; i++) { - if (otherFiles[i].file !== null && otherFiles[i].type !== '') { - formData.append( - 'files[]', - handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!, - ) + if (selectedFile) { + formData.append('file', selectedFile) + formData.append('name', fileName) + formData.append('description', description) + formData.append('user_name', user.user.name) + formData.append('user_id', user.user.id.toString()) + + try { + const token = await getAccessToken() + const response = await axios.post(uploadFiles, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer ${token}`, + }, + }) + + if (response.status === 200) { + await getAllFile() + setSelectedFile(null) + } + } catch (error) { + console.error('Error uploading file:', error) + } finally { + setIsLoading(false) } } - - if (cv) { - formData.append('files[]', cv) - } - - if (idCard) { - formData.append('files[]', idCard) - } - - if (transcript) { - formData.append('files[]', transcript) - } - - if (universityDiploma) { - formData.append('files[]', universityDiploma) - } - - const token = await getAccessToken() - try { - const response = await axios.post(updateProfileFolder, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - Authorization: `Bearer ${token}`, - }, - }) - - if(response.status === 200){ - getAllFile() - setOtherFiles([]) - } - } catch (error) { - console.error('Error uploading files', error) - } } const getAllFile = async () => { try { - const res = await get(getAllFilesInProfiles, { - root_folder: '/storage/profiles/' + JSON.parse(getUser())?.user?.name, - }) + const res = await get(getFiles) if (res.status === true) { - setData(res.data) + const userFiles = res.data[user.user.name] || []; + setData(userFiles); } } catch (error) { console.log(error) + notifications.show({ + title: 'Lỗi', + message: 'Không thể tải danh sách file', + color: 'red', + }) } } - const removeFile = async (url: string) => { + const removeFile = async (id: number) => { try { - await Xdelete(deleteFile, {file_url: url}, getAllFile) + const token = await getAccessToken(); + const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (response.status === 200) { + notifications.show({ + title: 'Thành công', + message: 'Xóa file thành công', + color: 'green', + }); + await getAllFile(); + } } catch (error) { - console.log(error) + console.log(error); + notifications.show({ + title: 'Lỗi', + message: 'Không thể xóa file', + color: 'red', + }); } - } + }; + useEffect(() => { const fetchData = async () => { const result = await getListProfilesData() setDataProfile(result ?? []) + await getAllFile() } fetchData() - getAllFile() }, []) const handleChangePassword = async () => { @@ -306,23 +328,19 @@ const Profile = () => { dispatch(logout(navigate)) }, [dispatch, navigate]) - const handleChangeFileName = (e: File, newName: string) => { - const originalFile = e // Get the original file - const extend = originalFile.name.split('.')[1] - if (originalFile) { - const newFileName = `${newName}.${extend}` // Create new file name - const newFile = new File([originalFile], newFileName, { - type: originalFile.type, - }) // Create new file object + // const handleChangeFileName = (e: File, newName: string): File => { + // const originalFile = e; + // const extend = originalFile.name.split('.')[1]; + // const newFileName = `${newName}.${extend}`; + // return new File([originalFile], newFileName, { + // type: originalFile.type, + // }); + // }; - return newFile // Save the new file object for further processing - } - } - - const checkFileExist = (nameField: string) => { - const file = data.find((f) => f.label.includes(nameField)) - return file - } + // const checkFileExist = (nameField: string) => { + // const file = data.find((f) => f.name.includes(nameField)); + // return file; + // }; return (
@@ -578,162 +596,14 @@ const Profile = () => { }} > -
- - - CV - - {`: ${checkFileExist('cv')?.label}`} - - - - { - 0 - setCv(handleChangeFileName(e!, 'cv')) - }} - accept=".pdf,.doc,.docx" - /> - - CCCD - - {`: ${checkFileExist('idCard')?.label}`} - - - - - { - setIdCard(handleChangeFileName(e!, 'idCard')) - }} - accept=".jpg,.jpeg,.png,.pdf" - /> - - Bảng điểm - - {`: ${checkFileExist('transcript')?.label}`} - - - - { - setTranscript(handleChangeFileName(e!, 'transcript')) - }} - accept=".pdf" - /> - - - Bằng đại học - - {`: ${ - checkFileExist('universityDiploma')?.label - }`} - - - - { - setUniversityDiploma( - handleChangeFileName(e!, 'universityDiploma'), - ) - }} - accept=".pdf,.jpg,.jpeg,.png" - /> - - Danh sách file khác: - - {data.find((f)=>f.label === 'others')?.children?.map((c, index)=>{ - return - - {`${ - c?.label - }`} - - - - })} - - {otherFiles.map((fileInput, index) => ( - - - handleOtherFileChange(index, 'file', file!) - } - w={'30%'} - /> - - handleOtherFileChange( - index, - 'type', - e.currentTarget.value, - ) - } - /> - - ))} - - - - -
+ file && setSelectedFile(file)} + removeFile={removeFile} + isLoading={isLoading} + currentUser={user.user.name} + />
diff --git a/FRONTEND/src/pages/Profile/components/FileUploadForm.module.css b/FRONTEND/src/pages/Profile/components/FileUploadForm.module.css new file mode 100644 index 0000000..a02278b --- /dev/null +++ b/FRONTEND/src/pages/Profile/components/FileUploadForm.module.css @@ -0,0 +1,121 @@ +.fileContainer { + padding: 12px; + margin: 8px 0; + border: 1px solid #e9ecef; + border-radius: 6px; + transition: all 0.2s ease; + display: flex; + flex-direction: column; + gap: 4px; +} + +.fileContainer:hover { + background-color: #f8f9fa; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.fileHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.fileName { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; +} + +.fileDescription { + font-size: 13px; + color: #666; + margin: 2px 0; +} + +.fileActions { + display: flex; + gap: 8px; + margin-top: 4px; +} + +.fileLink { + color: #228be6; + text-decoration: none; + font-size: 13px; + transition: color 0.2s ease; + padding: 4px 8px; + border-radius: 4px; + background-color: #e7f5ff; +} + +.fileLink:hover { + color: #1c7ed6; + background-color: #d0ebff; +} + +.deleteButton { + padding: 4px 8px; + font-size: 13px; +} + +.fileInputGroup { + padding: 16px; + margin: 16px 0; + border: 2px dashed #e9ecef; + border-radius: 6px; + background-color: #f8f9fa; +} + +.fileInput, +.fileNameInput, +.descriptionInput { + margin-bottom: 12px; +} + +.saveButton { + margin-top: 16px; + width: 100%; + max-width: 180px; +} + +.saveButton:disabled { + background-color: #e9ecef; + cursor: not-allowed; +} + +.loadingOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.loadingSpinner { + width: 32px; + height: 32px;Ø + border: 3px solid #f3f3f3; + border-top: 3px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.sectionTitle { + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; + color: #343a40; + padding-bottom: 8px; + border-bottom: 1px solid #e9ecef; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} \ No newline at end of file diff --git a/FRONTEND/src/pages/Profile/components/FileUploadForm.tsx b/FRONTEND/src/pages/Profile/components/FileUploadForm.tsx new file mode 100644 index 0000000..d8315e7 --- /dev/null +++ b/FRONTEND/src/pages/Profile/components/FileUploadForm.tsx @@ -0,0 +1,251 @@ +import { + Box, + Button, + Card, + FileInput, + Group, + Stack, + Text, + TextInput, + Textarea, +} from '@mantine/core' +import { notifications } from '@mantine/notifications' +import { + IconDownload, + IconFileTypeDocx, + IconFileTypePdf, + IconFileTypeXls, + IconPhoto, + IconSearch, + IconTrash, +} from '@tabler/icons-react' +import { useState } from 'react' +import classes from './FileUploadForm.module.css' + +// type TFileProfile = { +// label: string +// type: string +// value: string +// children?: TFileProfile[] +// } + +interface FileData { + id: number + name: string + url: string + type: string + description?: string + created_at: string +} + +type FileUploadFormProps = { + data: FileData[]; + handleSubmit: (e: React.FormEvent, fileName: string, description: string, currentUser: string) => Promise; + handleFileChange: (file: File | null) => void; + removeFile: (id: number) => Promise; + isLoading: boolean; + currentUser: string; +}; + +const FileUploadForm = ({ + data, + handleSubmit, + handleFileChange, + removeFile, + isLoading, + currentUser, +}: FileUploadFormProps) => { + const [selectedFile, setSelectedFile] = useState(null) + const [fileName, setFileName] = useState('') + const [description, setDescription] = useState('') + const [isUploading, setIsUploading] = useState(false) + const [searchTerm, setSearchTerm] = useState('') + + const handleFileSelect = (file: File | null) => { + setSelectedFile(file) + handleFileChange(file) + if (file) { + // Set default name as file name without extension + setFileName(file.name.split('.')[0]) + } + } + + const handleFormSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsUploading(true) + + try { + await handleSubmit(e, fileName, description, currentUser) + notifications.show({ + title: 'Thành công', + message: 'Tải file lên thành công', + color: 'green', + }) + setFileName('') + setDescription('') + setSelectedFile(null) + } catch (error) { + console.error('Error submitting form:', error) + notifications.show({ + title: 'Lỗi', + message: 'Không thể tải file lên', + color: 'red', + }) + } finally { + setIsUploading(false) + } + } + + const getFileIcon = (type: string) => { + switch (type) { + case 'document': + return + case 'image': + return + case 'spreadsheet': + return + default: + return + } + } + + const filteredFiles = data.filter( + (file) => + file.name.toLowerCase().includes(searchTerm.toLowerCase()) || + (file.description && + file.description.toLowerCase().includes(searchTerm.toLowerCase())), + ) + + return ( + <> + {isLoading && ( +
+
+
+ )} + +
+ + Tài liệu + + + + + setFileName(e.target.value)} + className={classes.fileNameInput} + required + /> + + +