Merge branch 'master' into truong-leave-day
This commit is contained in:
commit
1f9ab744ae
|
|
@ -0,0 +1,161 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Admin\app\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Traits\HasFilterRequest;
|
||||||
|
use App\Traits\HasOrderByRequest;
|
||||||
|
use App\Traits\HasSearchRequest;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Modules\Admin\app\Models\Document;
|
||||||
|
|
||||||
|
class DocumentController extends Controller
|
||||||
|
{
|
||||||
|
use HasOrderByRequest;
|
||||||
|
use HasFilterRequest;
|
||||||
|
use HasSearchRequest;
|
||||||
|
|
||||||
|
public function all(Request $request)
|
||||||
|
{
|
||||||
|
$documents = new Document;
|
||||||
|
|
||||||
|
// Order by
|
||||||
|
$this->orderByRequest($documents, $request);
|
||||||
|
$documents->orderBy('title', 'asc');
|
||||||
|
// Filter
|
||||||
|
$this->filterRequest(
|
||||||
|
builder: $documents,
|
||||||
|
request: $request,
|
||||||
|
filterKeys: [
|
||||||
|
'title' => self::F_TEXT,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$this->searchRequest(
|
||||||
|
builder: $documents,
|
||||||
|
value: $request->get('search'),
|
||||||
|
fields: [
|
||||||
|
'title',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$responseData = $documents->get();
|
||||||
|
return AbstractController::ResultSuccess($responseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'type' => 'required|in:file,link',
|
||||||
|
'files' => 'nullable|array',
|
||||||
|
'files.*.title' => 'required|string|max:255',
|
||||||
|
'files.*.file' => 'required|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
|
||||||
|
'links' => 'nullable|array',
|
||||||
|
'links.*.title' => 'required|string|max:255',
|
||||||
|
'links.*.uri' => 'required|string|url',
|
||||||
|
'is_active' => 'required|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$documents = [];
|
||||||
|
|
||||||
|
if ($request->type === "file") {
|
||||||
|
foreach ($request->file('files') as $index => $file) {
|
||||||
|
$path = $file['file']->store('uploads', 'public');
|
||||||
|
$documents[] = [
|
||||||
|
'title' => $request->input("files.$index.title"),
|
||||||
|
'type' => 'file',
|
||||||
|
'uri' => "storage/{$path}",
|
||||||
|
'is_active' => $request->is_active,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($request->links as $link) {
|
||||||
|
$documents[] = [
|
||||||
|
'title' => $link['title'],
|
||||||
|
'type' => 'link',
|
||||||
|
'uri' => $link['uri'],
|
||||||
|
'is_active' => $request->is_active,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($documents)) {
|
||||||
|
Document::insert($documents);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AbstractController::ResultSuccess($documents, "Documents created successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'id' => 'required|exists:documents,id',
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'type' => 'required|in:file,link',
|
||||||
|
'uri' => 'nullable|url',
|
||||||
|
'file' => 'nullable|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
|
||||||
|
'existing_file' => 'nullable|string',
|
||||||
|
'is_active' => 'required|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$document = Document::find($request->input('id'));
|
||||||
|
if (!$document) {
|
||||||
|
return AbstractController::ResultError("Document not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->type === "file") {
|
||||||
|
$uri = $request->existing_file;
|
||||||
|
|
||||||
|
if ($request->hasFile('file')) {
|
||||||
|
$filePath = str_replace('storage/', 'public/', $request->existing_file);
|
||||||
|
if (Storage::exists($filePath)) {
|
||||||
|
Storage::delete($filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $request->file('file')->store('uploads', 'public');
|
||||||
|
$uri = "storage/{$path}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->update([
|
||||||
|
'title' => $request->title,
|
||||||
|
'type' => $request->type,
|
||||||
|
'uri' => $uri,
|
||||||
|
'is_active' => $request->is_active,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return AbstractController::ResultSuccess($document, "Document updated successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->update([
|
||||||
|
'title' => $request->title,
|
||||||
|
'type' => $request->type,
|
||||||
|
'uri' => $request->uri,
|
||||||
|
'is_active' => $request->is_active,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return AbstractController::ResultSuccess($document, "Document updated successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Request $request)
|
||||||
|
{
|
||||||
|
$id = $request->input('id');
|
||||||
|
$document = Document::find($id);
|
||||||
|
if (!$document) {
|
||||||
|
return AbstractController::ResultError("Document not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($document->type === "file") {
|
||||||
|
$filePath = str_replace('storage/', 'public/', $document->uri);
|
||||||
|
if (Storage::exists($filePath)) {
|
||||||
|
Storage::delete($filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->delete();
|
||||||
|
return AbstractController::ResultSuccess("Document deleted successfully!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -178,6 +178,27 @@ class JiraController extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getWeeklyReport()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$startOfWeek = Carbon::now()->startOfWeek()->format('Y-m-d'); // Mặc định là Thứ Hai
|
||||||
|
$endOfWeek = Carbon::now()->endOfWeek()->format('Y-m-d'); // Mặc định là Chủ Nhật
|
||||||
|
// dd($startOfWeek);
|
||||||
|
$results = [];
|
||||||
|
$workLogs = $this->jiraService->getAllUserWorkLogs($startOfWeek, $endOfWeek);
|
||||||
|
foreach($workLogs as $data){
|
||||||
|
$results[$data['username']] = $data['information']['issues'];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
$results
|
||||||
|
], 200);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['error' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function sendReport()
|
public function sendReport()
|
||||||
{
|
{
|
||||||
$dateFormatted = Carbon::yesterday()->setTimezone(env('TIME_ZONE'))->format('Y-m-d');
|
$dateFormatted = Carbon::yesterday()->setTimezone(env('TIME_ZONE'))->format('Y-m-d');
|
||||||
|
|
@ -186,7 +207,7 @@ class JiraController extends Controller
|
||||||
|
|
||||||
$tasksByUser = $this->formatWorkLogsByUser($workLogs);
|
$tasksByUser = $this->formatWorkLogsByUser($workLogs);
|
||||||
// Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
|
// Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
|
||||||
Mail::to(['luanlt632000@gmail.com', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
|
Mail::to(['joseph@apactech.io', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
|
||||||
|
|
||||||
// return "Email sent successfully!";
|
// return "Email sent successfully!";
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,14 @@ use App\Traits\HasFilterRequest;
|
||||||
use App\Traits\HasOrderByRequest;
|
use App\Traits\HasOrderByRequest;
|
||||||
use App\Traits\HasSearchRequest;
|
use App\Traits\HasSearchRequest;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Illuminate\Support\Facades\Storage;
|
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\Sprint;
|
||||||
use Modules\Admin\app\Models\UserCriteria;
|
use Modules\Admin\app\Models\UserCriteria;
|
||||||
|
use App\Models\Files;
|
||||||
|
use App\DataTransferObjects\FileData;
|
||||||
|
|
||||||
class ProfileController extends Controller
|
class ProfileController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -28,6 +33,7 @@ class ProfileController extends Controller
|
||||||
$this->jiraService = $jiraService;
|
$this->jiraService = $jiraService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function getProfilesData(Request $request)
|
public function getProfilesData(Request $request)
|
||||||
{
|
{
|
||||||
$user = auth('admins')->user();
|
$user = auth('admins')->user();
|
||||||
|
|
@ -125,7 +131,7 @@ class ProfileController extends Controller
|
||||||
$rootFolder = rtrim($rootFolder, '/') . '/';
|
$rootFolder = rtrim($rootFolder, '/') . '/';
|
||||||
|
|
||||||
// Get all files and directories in the specified root folder
|
// 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]);
|
return response()->json(['data' => $fileList, 'status' => true]);
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +175,7 @@ class ProfileController extends Controller
|
||||||
$name = $request->input('name') ?? auth('admins')->user()->name;
|
$name = $request->input('name') ?? auth('admins')->user()->name;
|
||||||
// Validate the incoming files
|
// Validate the incoming files
|
||||||
$request->validate([
|
$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 = [];
|
$uploadedFiles = [];
|
||||||
|
|
@ -185,6 +191,10 @@ class ProfileController extends Controller
|
||||||
if (!Storage::disk('public')->exists($othersDirectory)) {
|
if (!Storage::disk('public')->exists($othersDirectory)) {
|
||||||
Storage::disk('public')->makeDirectory($othersDirectory);
|
Storage::disk('public')->makeDirectory($othersDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$adminEmails = Admin::where('permission', 'like', '%admin%')->pluck('email')->toArray();
|
||||||
|
$currentUser = auth('admins')->user();
|
||||||
|
|
||||||
if ($request->hasFile('files')) {
|
if ($request->hasFile('files')) {
|
||||||
foreach ($request->file('files') as $file) {
|
foreach ($request->file('files') as $file) {
|
||||||
// Store the file and get its path
|
// Store the file and get its path
|
||||||
|
|
@ -197,6 +207,32 @@ class ProfileController extends Controller
|
||||||
$path = $file->storeAs($baseDirectory, $originalFilename, 'public');
|
$path = $file->storeAs($baseDirectory, $originalFilename, 'public');
|
||||||
}
|
}
|
||||||
$uploadedFiles[] = $path;
|
$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,201 @@ class ProfileController extends Controller
|
||||||
'message' => 'File not found',
|
'message' => 'File not found',
|
||||||
], 404);
|
], 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 {
|
||||||
|
// Lấy tất cả users
|
||||||
|
$users = Admin::all();
|
||||||
|
|
||||||
|
// Lấy files và map theo cấu trúc
|
||||||
|
$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
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tạo mảng kết quả với tất cả users, không có file thì mảng rỗng
|
||||||
|
$result = $users->pluck('name')->mapWithKeys(function($userName) use ($files) {
|
||||||
|
$userFiles = $files->where('user_name', $userName)
|
||||||
|
->map(function($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']
|
||||||
|
];
|
||||||
|
})->values();
|
||||||
|
|
||||||
|
return [$userName => $userFiles];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'status' => true,
|
||||||
|
'data' => $result
|
||||||
|
]);
|
||||||
|
|
||||||
|
} 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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Admin\app\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Document extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['title', 'uri', 'type', 'is_active'];
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use App\Http\Middleware\CheckPermission;
|
use App\Http\Middleware\CheckPermission;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Modules\Admin\app\Http\Controllers\AdminController;
|
use Modules\Admin\app\Http\Controllers\AdminController;
|
||||||
use Modules\Admin\app\Http\Controllers\BannerController;
|
use Modules\Admin\app\Http\Controllers\BannerController;
|
||||||
use Modules\Admin\app\Http\Controllers\CategoryController;
|
use Modules\Admin\app\Http\Controllers\CategoryController;
|
||||||
|
|
@ -9,6 +10,7 @@ use Modules\Admin\app\Http\Controllers\ClientController;
|
||||||
use Modules\Admin\app\Http\Controllers\CountryController;
|
use Modules\Admin\app\Http\Controllers\CountryController;
|
||||||
use Modules\Admin\app\Http\Controllers\CustomThemeController;
|
use Modules\Admin\app\Http\Controllers\CustomThemeController;
|
||||||
use Modules\Admin\app\Http\Controllers\DashboardController;
|
use Modules\Admin\app\Http\Controllers\DashboardController;
|
||||||
|
use Modules\Admin\app\Http\Controllers\DocumentController;
|
||||||
use Modules\Admin\app\Http\Controllers\JiraController;
|
use Modules\Admin\app\Http\Controllers\JiraController;
|
||||||
use Modules\Admin\app\Http\Controllers\LeaveManagementController;
|
use Modules\Admin\app\Http\Controllers\LeaveManagementController;
|
||||||
use Modules\Admin\app\Http\Controllers\SettingController;
|
use Modules\Admin\app\Http\Controllers\SettingController;
|
||||||
|
|
@ -112,6 +114,8 @@ Route::middleware('api')
|
||||||
Route::get('/get-all-sprint-by-id-board', [JiraController::class, 'getAllSprintByIdBoard'])->middleware('check.permission:admin.tester');
|
Route::get('/get-all-sprint-by-id-board', [JiraController::class, 'getAllSprintByIdBoard'])->middleware('check.permission:admin.tester');
|
||||||
Route::get('/get-all-issue-by-id-sprint', [JiraController::class, 'getAllIssueByIdSprint']);
|
Route::get('/get-all-issue-by-id-sprint', [JiraController::class, 'getAllIssueByIdSprint']);
|
||||||
|
|
||||||
|
Route::get('/export-weekly-report', [JiraController::class, 'getWeeklyReport']);
|
||||||
|
|
||||||
Route::get('/all-issue-by-project', [JiraController::class, 'fetchIssuesByProject']);
|
Route::get('/all-issue-by-project', [JiraController::class, 'fetchIssuesByProject']);
|
||||||
Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs'])->middleware('check.permission:admin.staff');
|
Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs'])->middleware('check.permission:admin.staff');
|
||||||
Route::get('/allocation', [JiraController::class, 'getAllUserDoing'])->middleware('check.permission:admin.staff');
|
Route::get('/allocation', [JiraController::class, 'getAllUserDoing'])->middleware('check.permission:admin.staff');
|
||||||
|
|
@ -170,6 +174,9 @@ Route::middleware('api')
|
||||||
Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
|
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::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('/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([
|
Route::group([
|
||||||
|
|
@ -214,6 +221,32 @@ Route::middleware('api')
|
||||||
Route::get('/get-list-user-by-tech-id/{technicalId}', [TechnicalController::class, 'getListUserByTechnicalId'])->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']);
|
Route::post('/technicals-user/update', [TechnicalController::class, 'updateTechnicalsUser']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::group([
|
||||||
|
'prefix' => 'document',
|
||||||
|
], function () {
|
||||||
|
Route::get('/all', [DocumentController::class, 'all'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||||
|
Route::post('/create', [DocumentController::class, 'create'])->middleware('check.permission:admin');
|
||||||
|
Route::post('/update', [DocumentController::class, 'update'])->middleware('check.permission:admin');
|
||||||
|
Route::get('/delete', [DocumentController::class, 'delete'])->middleware('check.permission:admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::get('/download-file/{filename}', function ($filename) {
|
||||||
|
$path = "uploads/{$filename}";
|
||||||
|
|
||||||
|
if (!Storage::disk('public')->exists($path)) {
|
||||||
|
return response()->json(['error' => 'File not found'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Access-Control-Allow-Origin' => '*',
|
||||||
|
'Access-Control-Allow-Methods' => 'GET',
|
||||||
|
'Access-Control-Allow-Headers' => 'Content-Type',
|
||||||
|
'Content-Disposition' => 'inline; filename="' . $filename . '"',
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->file(storage_path("app/public/{$path}"), $headers);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Files extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'url',
|
||||||
|
'type',
|
||||||
|
'description',
|
||||||
|
'user_id'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,9 @@ class JiraService
|
||||||
'Authorization' => $this->authHeader,
|
'Authorization' => $this->authHeader,
|
||||||
'Accept' => 'application/json',
|
'Accept' => 'application/json',
|
||||||
'Content-Type' => 'application/json'
|
'Content-Type' => 'application/json'
|
||||||
]
|
],
|
||||||
|
'timeout' => 60, // Tăng thời gian timeout lên 60 giây
|
||||||
|
'connect_timeout' => 30 // Tăng thời gian chờ kết nối lên 30 giây
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,7 +268,7 @@ class JiraService
|
||||||
|
|
||||||
foreach ($issues['issues'] as $issue) {
|
foreach ($issues['issues'] as $issue) {
|
||||||
$projectName = $issue['fields']['project']['name'];
|
$projectName = $issue['fields']['project']['name'];
|
||||||
if(!in_array($projectName, $ignore_projects)) {
|
if (!in_array($projectName, $ignore_projects)) {
|
||||||
$username = $issue['fields']['assignee']['displayName'];
|
$username = $issue['fields']['assignee']['displayName'];
|
||||||
$issue['fields']['assignee']['emailAddress'] = $user['emailAddress'];
|
$issue['fields']['assignee']['emailAddress'] = $user['emailAddress'];
|
||||||
if (!isset($groupedIssues[$projectName])) {
|
if (!isset($groupedIssues[$projectName])) {
|
||||||
|
|
@ -288,7 +290,6 @@ class JiraService
|
||||||
$users_data[$user['displayName']]['total_spent'] = $users_data[$user['displayName']]['total_spent'] + $issue['fields']['timespent'];
|
$users_data[$user['displayName']]['total_spent'] = $users_data[$user['displayName']]['total_spent'] + $issue['fields']['timespent'];
|
||||||
$users_data[$user['displayName']]['total_est'] = $users_data[$user['displayName']]['total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
|
$users_data[$user['displayName']]['total_est'] = $users_data[$user['displayName']]['total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,4 +326,18 @@ class JiraService
|
||||||
$response = $this->client->get('/rest/agile/1.0/sprint/' . $id . '/issue');
|
$response = $this->client->get('/rest/agile/1.0/sprint/' . $id . '/issue');
|
||||||
return json_decode($response->getBody()->getContents(), true);
|
return json_decode($response->getBody()->getContents(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getWeeklyReport()
|
||||||
|
{
|
||||||
|
$body = [
|
||||||
|
'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'assignee', 'project', 'worklog'],
|
||||||
|
'jql' => 'worklogDate >= startOfWeek() AND worklogDate < startOfWeek(1) order by created DESC',
|
||||||
|
'maxResults' => 1000
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->client->post('/rest/api/3/search', [
|
||||||
|
'body' => json_encode($body)
|
||||||
|
]);
|
||||||
|
return json_decode($response->getBody()->getContents(), true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('documents', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('title');
|
||||||
|
$table->text('uri');
|
||||||
|
$table->enum('type', ['file', 'link'])->default("file");
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('documents');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('files', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -18,5 +18,10 @@ class DatabaseSeeder extends Seeder
|
||||||
// 'name' => 'Test User',
|
// 'name' => 'Test User',
|
||||||
// 'email' => 'test@example.com',
|
// 'email' => 'test@example.com',
|
||||||
// ]);
|
// ]);
|
||||||
|
|
||||||
|
$this->call([
|
||||||
|
UserSeeder::class,
|
||||||
|
FileSeeder::class,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Thông báo tải lên file mới</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background-color: #1a73e8;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: #1a73e8;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Thông báo tải lên file mới</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<p>Xin chào {{ $user->name }},</p>
|
||||||
|
|
||||||
|
<p>{{ $description }} ở hệ thống APAC Tech.</p>
|
||||||
|
<p>Note: {{ $note }}</p>
|
||||||
|
<p>Vui lòng kiểm tra ngay thông tin bằng cách nhấn nút bên dưới:</p>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="{{ $url }}" class="button">Kiểm tra ngay</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Trân trọng,<br>Đội ngũ APAC Tech</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Email này được gửi tự động từ hệ thống APAC Tech. Vui lòng không trả lời email này.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -34,6 +34,7 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-doc-viewer": "^0.1.14",
|
"react-doc-viewer": "^0.1.14",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-file-viewer": "^1.2.1",
|
||||||
"react-redux": "^8.1.3",
|
"react-redux": "^8.1.3",
|
||||||
"react-router-dom": "^6.19.0",
|
"react-router-dom": "^6.19.0",
|
||||||
"reactstrap": "^9.2.2",
|
"reactstrap": "^9.2.2",
|
||||||
|
|
|
||||||
|
|
@ -110,3 +110,17 @@ export const deleteFile = API_URL + 'v1/admin/profile/delete-profile-file'
|
||||||
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
|
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
|
||||||
export const createTechnical = API_URL + 'v1/admin/technical/create'
|
export const createTechnical = API_URL + 'v1/admin/technical/create'
|
||||||
export const deleteTechnical = API_URL + 'v1/admin/technical/delete'
|
export const deleteTechnical = API_URL + 'v1/admin/technical/delete'
|
||||||
|
|
||||||
|
// Document
|
||||||
|
export const listDocument = API_URL + 'v1/admin/document/all'
|
||||||
|
export const createDocument = API_URL + 'v1/admin/document/create'
|
||||||
|
export const updateDocument = API_URL + 'v1/admin/document/update'
|
||||||
|
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'
|
||||||
|
|
|
||||||
|
|
@ -250,12 +250,7 @@ export const DataTableAll = ({
|
||||||
if (query !== '') {
|
if (query !== '') {
|
||||||
setTData(
|
setTData(
|
||||||
data.filter((obj) =>
|
data.filter((obj) =>
|
||||||
Object.values(obj).some(
|
Object.values(obj)?.find((c: any) => c.toString().normalize('NFC').toLowerCase().includes(query.normalize('NFC').toLowerCase())))
|
||||||
(value: any) =>
|
|
||||||
value !== null &&
|
|
||||||
value.toString().toLowerCase().includes(query.toLowerCase()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if (pagination) {
|
if (pagination) {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
IconCalendarClock,
|
IconCalendarClock,
|
||||||
IconChartDots2,
|
IconChartDots2,
|
||||||
IconDevices,
|
IconDevices,
|
||||||
|
IconFileInvoice,
|
||||||
IconFolders,
|
IconFolders,
|
||||||
IconLayoutSidebarLeftExpand,
|
IconLayoutSidebarLeftExpand,
|
||||||
IconLayoutSidebarRightExpand,
|
IconLayoutSidebarRightExpand,
|
||||||
|
|
@ -41,7 +42,7 @@ import {
|
||||||
IconSun,
|
IconSun,
|
||||||
IconTicket,
|
IconTicket,
|
||||||
IconUsersGroup,
|
IconUsersGroup,
|
||||||
IconZoomExclamation
|
IconZoomExclamation,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
@ -71,6 +72,13 @@ const data = [
|
||||||
permissions: 'admin,hr,staff,tester',
|
permissions: 'admin,hr,staff,tester',
|
||||||
group: 'staff',
|
group: 'staff',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: '/documents',
|
||||||
|
label: 'Documents',
|
||||||
|
icon: IconFileInvoice,
|
||||||
|
permissions: 'admin,hr,staff,tester,accountant',
|
||||||
|
group: 'staff',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
link: '/leave-management',
|
link: '/leave-management',
|
||||||
label: 'Leave Management',
|
label: 'Leave Management',
|
||||||
|
|
@ -239,7 +247,11 @@ const Navbar = ({
|
||||||
// })
|
// })
|
||||||
|
|
||||||
const group = [
|
const group = [
|
||||||
{ name: 'staff', label: 'General', permissions: 'admin,hr,staff,tester,accountant' },
|
{
|
||||||
|
name: 'staff',
|
||||||
|
label: 'General',
|
||||||
|
permissions: 'admin,hr,staff,tester,accountant',
|
||||||
|
},
|
||||||
{ name: 'admin', label: 'Admin', permissions: 'admin' },
|
{ name: 'admin', label: 'Admin', permissions: 'admin' },
|
||||||
{ name: 'other', label: 'Other', permissions: 'admin,hr' },
|
{ name: 'other', label: 'Other', permissions: 'admin,hr' },
|
||||||
{ name: 'test', label: 'Test', permissions: 'admin,tester' },
|
{ name: 'test', label: 'Test', permissions: 'admin,tester' },
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,8 @@ declare module '*.css'
|
||||||
declare module '@codemirror/lang-javascript'
|
declare module '@codemirror/lang-javascript'
|
||||||
|
|
||||||
declare const __VITE_BACKEND_URL__: string
|
declare const __VITE_BACKEND_URL__: string
|
||||||
|
|
||||||
|
declare module 'react-file-viewer' {
|
||||||
|
const FileViewer: any
|
||||||
|
export default FileViewer
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,195 +1,95 @@
|
||||||
import {
|
import { getFiles, uploadFiles } from '@/api/Admin'
|
||||||
deleteFile,
|
|
||||||
getAllFilesInProfiles,
|
|
||||||
updateProfileFolder,
|
|
||||||
} from '@/api/Admin'
|
|
||||||
import { Xdelete } from '@/rtk/helpers/CRUD'
|
|
||||||
import { get } from '@/rtk/helpers/apiService'
|
import { get } from '@/rtk/helpers/apiService'
|
||||||
import { getAccessToken } from '@/rtk/localStorage'
|
import { getAccessToken } from '@/rtk/localStorage'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
FileInput,
|
Card,
|
||||||
|
Collapse,
|
||||||
Group,
|
Group,
|
||||||
Modal,
|
Modal,
|
||||||
RenderTreeNodePayload,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Tooltip,
|
Title,
|
||||||
Tree,
|
|
||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
|
import { notifications } from '@mantine/notifications'
|
||||||
import {
|
import {
|
||||||
|
IconChevronDown,
|
||||||
|
IconDownload,
|
||||||
IconFileTypeDocx,
|
IconFileTypeDocx,
|
||||||
IconFileTypePdf,
|
IconFileTypePdf,
|
||||||
IconFolder,
|
IconFolder,
|
||||||
IconFolderOpen,
|
|
||||||
IconFolderX,
|
|
||||||
IconListCheck,
|
IconListCheck,
|
||||||
IconPhoto,
|
IconPhoto,
|
||||||
|
IconSearch,
|
||||||
|
IconTrash,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import FileUploadForm from '../Profile/components/FileUploadForm'
|
||||||
import classes from './AllProfiles.module.css'
|
import classes from './AllProfiles.module.css'
|
||||||
|
|
||||||
interface FileIconProps {
|
interface FileData {
|
||||||
|
id: number
|
||||||
name: string
|
name: string
|
||||||
isFolder: boolean
|
url: string
|
||||||
expanded: boolean
|
type: string
|
||||||
|
description?: string
|
||||||
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TFileProfile = {
|
interface GroupedFiles {
|
||||||
label: string
|
[key: string]: FileData[]
|
||||||
type: string
|
|
||||||
value: string
|
|
||||||
children?: TFileProfile[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AllProfiles = () => {
|
const AllProfiles = () => {
|
||||||
const [treeData, setTreeData] = useState([])
|
const [groupedFiles, setGroupedFiles] = useState<GroupedFiles>({})
|
||||||
const [cv, setCv] = useState<File>()
|
const [currentUser, setCurrentUser] = useState<string>('')
|
||||||
const [idCard, setIdCard] = useState<File>()
|
|
||||||
const [transcript, setTranscript] = useState<File>()
|
|
||||||
const [universityDiploma, setUniversityDiploma] = useState<File>()
|
|
||||||
const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
|
|
||||||
const [data, setData] = useState<TFileProfile[]>([])
|
|
||||||
const [currentName, setCurrentName] = useState<string>('')
|
|
||||||
const [openedProfile, setOpenedProfile] = useState(false)
|
const [openedProfile, setOpenedProfile] = useState(false)
|
||||||
function FileIcon({ name, isFolder, expanded }: FileIconProps) {
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
if (name.endsWith('.pdf')) {
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
return <IconFileTypePdf size={14} />
|
const [expandedFolders, setExpandedFolders] = useState<{
|
||||||
|
[key: string]: boolean
|
||||||
|
}>({})
|
||||||
|
const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
|
||||||
|
|
||||||
|
const toggleFolder = (userName: string) => {
|
||||||
|
setExpandedFolders((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[userName]: !prev[userName],
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.endsWith('.doc') || name.endsWith('.docx')) {
|
const getFileIcon = (type: string) => {
|
||||||
return <IconFileTypeDocx size={14} />
|
switch (type) {
|
||||||
|
case 'document':
|
||||||
|
return <IconFileTypeDocx size={16} />
|
||||||
|
case 'image':
|
||||||
|
return <IconPhoto size={16} />
|
||||||
|
default:
|
||||||
|
return <IconFileTypePdf size={16} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const handleSubmit = async (
|
||||||
name.endsWith('.jpg') ||
|
e: React.FormEvent,
|
||||||
name.endsWith('.png') ||
|
fileName: string,
|
||||||
name.endsWith('.jpeg') ||
|
description: string,
|
||||||
name.endsWith('.webp')
|
currentUser: string
|
||||||
) {
|
|
||||||
return <IconPhoto size={14} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFolder) {
|
|
||||||
return expanded ? (
|
|
||||||
<IconFolderOpen
|
|
||||||
color="var(--mantine-color-yellow-9)"
|
|
||||||
size={14}
|
|
||||||
stroke={2.5}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<IconFolder
|
|
||||||
color="var(--mantine-color-yellow-9)"
|
|
||||||
size={14}
|
|
||||||
stroke={2.5}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconFolderX color="var(--mantine-color-red-9)" size={14} stroke={2.5} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Leaf({
|
|
||||||
node,
|
|
||||||
expanded,
|
|
||||||
hasChildren,
|
|
||||||
elementProps,
|
|
||||||
}: RenderTreeNodePayload) {
|
|
||||||
return (
|
|
||||||
<Group gap={5} {...elementProps}>
|
|
||||||
{!node.children ? (
|
|
||||||
<a href={node.value} target="_blank">
|
|
||||||
<FileIcon
|
|
||||||
name={node.value}
|
|
||||||
isFolder={hasChildren}
|
|
||||||
expanded={expanded}
|
|
||||||
/>
|
|
||||||
<span>{node.label}</span>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FileIcon
|
|
||||||
name={node.value}
|
|
||||||
isFolder={hasChildren}
|
|
||||||
expanded={expanded}
|
|
||||||
/>
|
|
||||||
<span>{node.label}</span>
|
|
||||||
<Tooltip label="Upload">
|
|
||||||
<IconListCheck
|
|
||||||
color="green"
|
|
||||||
width={15}
|
|
||||||
height={15}
|
|
||||||
style={{
|
|
||||||
display: node.label !== 'others' ? 'block' : 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
zIndex: 100000,
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentName(node.label!.toString())
|
|
||||||
setOpenedProfile(true)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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: any) => {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
setIsLoading(true)
|
||||||
const formData = new FormData()
|
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}`)!,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(updateProfileFolder, formData, {
|
const token = await getAccessToken()
|
||||||
|
const response = await axios.post(uploadFiles, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
|
|
@ -197,302 +97,234 @@ const AllProfiles = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
getAllFile()
|
setSelectedFile(null)
|
||||||
getTree()
|
await getAllFiles()
|
||||||
setOtherFiles([])
|
return true
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uploading files', error)
|
console.error('Error uploading file:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const getAllFile = async () => {
|
const getAllFiles = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await get(getAllFilesInProfiles, {
|
const res = await get(getFiles)
|
||||||
root_folder: '/storage/profiles/' + currentName,
|
|
||||||
})
|
|
||||||
if (res.status === true) {
|
if (res.status === true) {
|
||||||
setData(res.data)
|
setGroupedFiles(res.data)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeFile = async (url: string) => {
|
const removeFile = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await Xdelete(deleteFile, { file_url: url }, getAllFile)
|
const token = await getAccessToken();
|
||||||
getTree()
|
const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
|
||||||
} catch (error) {
|
headers: {
|
||||||
console.log(error)
|
Authorization: `Bearer ${token}`,
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
const getTree = async () => {
|
|
||||||
try {
|
if (response.status === 200) {
|
||||||
const res = await get(getAllFilesInProfiles, {
|
notifications.show({
|
||||||
root_folder: '/storage/profiles',
|
title: 'Thành công',
|
||||||
})
|
message: 'Xóa file thành công',
|
||||||
if (res.status === true) {
|
color: 'green',
|
||||||
setTreeData(res.data)
|
});
|
||||||
|
await getAllFiles();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error);
|
||||||
|
notifications.show({
|
||||||
|
title: 'Lỗi',
|
||||||
|
message: 'Không thể xóa file',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
getTree()
|
getAllFiles()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
const filterFiles = (files: FileData[], searchTerm: string) => {
|
||||||
getAllFile()
|
return files.filter(
|
||||||
}, [currentName])
|
(file) =>
|
||||||
|
file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(file.description &&
|
||||||
|
file.description.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes.title}>
|
<div className={classes.title}>
|
||||||
<h3>
|
<h3>
|
||||||
<Text>Admin/</Text>
|
<Text size="sm">Admin/</Text>
|
||||||
Profiles
|
Files Management
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<Box ml={'lg'}>
|
<Box ml={'md'}>
|
||||||
<Tree
|
<Stack gap="xs">
|
||||||
classNames={classes}
|
{Object.entries(groupedFiles).map(([userName, files]) => (
|
||||||
selectOnClick
|
<Card key={userName} shadow="xs" radius="sm" withBorder p="xs">
|
||||||
clearSelectionOnOutsideClick
|
<Group
|
||||||
data={treeData}
|
justify="space-between"
|
||||||
renderNode={(payload) => <Leaf {...payload} />}
|
mb="xs"
|
||||||
|
gap="xs"
|
||||||
|
onClick={() => toggleFolder(userName)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconFolder size={18} color="var(--mantine-color-yellow-9)" />
|
||||||
|
<Title order={5}>{userName}</Title>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => toggleFolder(userName)}
|
||||||
|
leftSection={
|
||||||
|
<IconChevronDown
|
||||||
|
size={14}
|
||||||
|
style={{
|
||||||
|
transform: expandedFolders[userName]
|
||||||
|
? 'rotate(180deg)'
|
||||||
|
: 'none',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{expandedFolders[userName] ? 'Collapse' : 'Expand'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentUser(userName)
|
||||||
|
setOpenedProfile(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap={2}>
|
||||||
|
<IconListCheck size={14} />
|
||||||
|
<Text size="xs">Upload Files</Text>
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Collapse in={expandedFolders[userName]}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Search files by name or description..."
|
||||||
|
size="xs"
|
||||||
|
leftSection={<IconSearch size={14} />}
|
||||||
|
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) => (
|
||||||
|
<Card
|
||||||
|
key={file.id}
|
||||||
|
shadow="xs"
|
||||||
|
padding="xs"
|
||||||
|
radius="sm"
|
||||||
|
withBorder
|
||||||
|
>
|
||||||
|
<Group justify="space-between" gap="xs">
|
||||||
|
<Group gap="xs">
|
||||||
|
{getFileIcon(file.type)}
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" fw={500}>
|
||||||
|
{file.name}
|
||||||
|
</Text>
|
||||||
|
{file.description && (
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{file.description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Uploaded:{' '}
|
||||||
|
{new Date(file.created_at).toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
component='a'
|
||||||
|
href={`${import.meta.env.VITE_BACKEND_URL}${
|
||||||
|
import.meta.env.VITE_BACKEND_URL?.includes(
|
||||||
|
'localhost',
|
||||||
|
)
|
||||||
|
? ''
|
||||||
|
: 'image/'
|
||||||
|
}${file.url.slice(1)}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Group gap={2}>
|
||||||
|
<IconDownload size={12} />
|
||||||
|
<Text size="xs">Download</Text>
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
onClick={() => removeFile(file.id)}
|
||||||
|
>
|
||||||
|
<Group gap={2}>
|
||||||
|
<IconTrash size={12} />
|
||||||
|
<Text size="xs">Delete</Text>
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
size={'lg'}
|
size="lg"
|
||||||
opened={openedProfile}
|
opened={openedProfile}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setOpenedProfile(false)
|
setOpenedProfile(false)
|
||||||
|
setCurrentUser('')
|
||||||
|
setSelectedFile(null)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<form onSubmit={handleSubmit}>
|
<FileUploadForm
|
||||||
<Stack>
|
data={groupedFiles[currentUser] || []}
|
||||||
<Box
|
handleSubmit={handleSubmit}
|
||||||
style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
|
handleFileChange={(file) => file && setSelectedFile(file)}
|
||||||
>
|
removeFile={removeFile}
|
||||||
<Text>CV</Text>
|
isLoading={isLoading}
|
||||||
<a href={checkFileExist('cv')?.value} target="_blank">
|
currentUser={currentUser}
|
||||||
<Text>{`: ${checkFileExist('cv')?.label}`}</Text>
|
|
||||||
</a>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="xs"
|
|
||||||
color="red"
|
|
||||||
ml={'sm'}
|
|
||||||
onClick={() => {
|
|
||||||
removeFile(checkFileExist('cv')?.value!)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<FileInput
|
|
||||||
label={'CV'}
|
|
||||||
placeholder="Tải lên CV"
|
|
||||||
style={{ display: checkFileExist('cv') ? 'none' : 'block' }}
|
|
||||||
onChange={(e) => {
|
|
||||||
0
|
|
||||||
setCv(handleChangeFileName(e!, 'cv'))
|
|
||||||
}}
|
|
||||||
accept=".pdf,.doc,.docx"
|
|
||||||
/>
|
/>
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: checkFileExist('idCard') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>CCCD</Text>
|
|
||||||
<a href={checkFileExist('idCard')?.value} target="_blank">
|
|
||||||
<Text>{`: ${checkFileExist('idCard')?.label}`}</Text>
|
|
||||||
</a>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="xs"
|
|
||||||
color="red"
|
|
||||||
ml={'sm'}
|
|
||||||
onClick={() => {
|
|
||||||
removeFile(checkFileExist('idCard')?.value!)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<FileInput
|
|
||||||
label="CCCD"
|
|
||||||
style={{
|
|
||||||
display: checkFileExist('idCard') ? 'none' : 'block',
|
|
||||||
}}
|
|
||||||
placeholder="Tải lên CCCD"
|
|
||||||
onChange={(e) => {
|
|
||||||
setIdCard(handleChangeFileName(e!, 'idCard'))
|
|
||||||
}}
|
|
||||||
accept=".jpg,.jpeg,.png,.pdf"
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: checkFileExist('transcript') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>Bảng điểm</Text>
|
|
||||||
<a href={checkFileExist('transcript')?.value} target="_blank">
|
|
||||||
<Text>{`: ${checkFileExist('transcript')?.label}`}</Text>
|
|
||||||
</a>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="xs"
|
|
||||||
color="red"
|
|
||||||
ml={'sm'}
|
|
||||||
onClick={() => {
|
|
||||||
removeFile(checkFileExist('transcript')?.value!)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<FileInput
|
|
||||||
label="Bảng điểm"
|
|
||||||
style={{
|
|
||||||
display: checkFileExist('transcript') ? 'none' : 'block',
|
|
||||||
}}
|
|
||||||
placeholder="Tải lên bảng điểm"
|
|
||||||
onChange={(e) => {
|
|
||||||
setTranscript(handleChangeFileName(e!, 'transcript'))
|
|
||||||
}}
|
|
||||||
accept=".pdf"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: checkFileExist('universityDiploma')
|
|
||||||
? 'flex'
|
|
||||||
: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>Bằng đại học</Text>
|
|
||||||
<a
|
|
||||||
href={checkFileExist('universityDiploma')?.value}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Text>{`: ${
|
|
||||||
checkFileExist('universityDiploma')?.label
|
|
||||||
}`}</Text>
|
|
||||||
</a>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="xs"
|
|
||||||
color="red"
|
|
||||||
ml={'sm'}
|
|
||||||
onClick={() => {
|
|
||||||
removeFile(checkFileExist('universityDiploma')?.value!)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<FileInput
|
|
||||||
label="Bằng đại học"
|
|
||||||
style={{
|
|
||||||
display: checkFileExist('universityDiploma')
|
|
||||||
? 'none'
|
|
||||||
: 'block',
|
|
||||||
}}
|
|
||||||
placeholder="Tải lên bằng đại học"
|
|
||||||
onChange={(e) => {
|
|
||||||
setUniversityDiploma(
|
|
||||||
handleChangeFileName(e!, 'universityDiploma'),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
accept=".pdf,.jpg,.jpeg,.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text>Danh sách file khác:</Text>
|
|
||||||
<Box>
|
|
||||||
{data
|
|
||||||
.find((f) => f.label === 'others')
|
|
||||||
?.children?.map((c, index) => {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
marginBottom: '5px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a href={c?.value} target="_blank">
|
|
||||||
<Text>{`${c?.label}`}</Text>
|
|
||||||
</a>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="xs"
|
|
||||||
color="red"
|
|
||||||
ml={'sm'}
|
|
||||||
onClick={() => {
|
|
||||||
removeFile(c?.value!)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
{otherFiles.map((fileInput, index) => (
|
|
||||||
<Group key={index}>
|
|
||||||
<FileInput
|
|
||||||
placeholder="Chọn file"
|
|
||||||
onChange={(file) =>
|
|
||||||
handleOtherFileChange(index, 'file', file!)
|
|
||||||
}
|
|
||||||
w={'30%'}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
placeholder="Nhập tên file (không cần phần mở rộng)"
|
|
||||||
value={fileInput.type}
|
|
||||||
w={'65%'}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleOtherFileChange(
|
|
||||||
index,
|
|
||||||
'type',
|
|
||||||
e.currentTarget.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
<Button type="button" onClick={addOtherFileInput}>
|
|
||||||
Thêm file khác
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button type="submit" color="blue">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
.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 {
|
||||||
|
color: red;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editIcon {
|
||||||
|
color: rgb(9, 132, 132);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editIcon:hover {
|
||||||
|
background-color: rgba(203, 203, 203, 0.809);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteIcon:hover {
|
||||||
|
background-color: rgba(203, 203, 203, 0.809);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { get } from '@/rtk/helpers/apiService'
|
||||||
|
import { deleteDocument, listDocument } from '@/api/Admin'
|
||||||
|
import { Xdelete } from '@/rtk/helpers/CRUD'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { RootState } from '@/rtk/store'
|
||||||
|
|
||||||
|
import { Anchor, Box, Button, Dialog, Group, Loader, Text } from '@mantine/core'
|
||||||
|
import { useDisclosure } from '@mantine/hooks'
|
||||||
|
import { notifications } from '@mantine/notifications'
|
||||||
|
import {
|
||||||
|
IconEdit,
|
||||||
|
IconFileTypeDoc,
|
||||||
|
IconFileTypePdf,
|
||||||
|
IconFileTypeXls,
|
||||||
|
IconLink,
|
||||||
|
IconX,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
|
import DataTableAll from '@/components/DataTable/DataTable'
|
||||||
|
import ModalFileDocument from './ModalFileDocument'
|
||||||
|
|
||||||
|
import classes from './Document.module.css'
|
||||||
|
import ModalAddDocument from './ModalAddDocument'
|
||||||
|
import ModalEditDocument from './ModalEditDocument'
|
||||||
|
|
||||||
|
interface TDocument {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
uri: string
|
||||||
|
type: string
|
||||||
|
is_active: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestPagination = {
|
||||||
|
data: TDocument[]
|
||||||
|
status: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Document = () => {
|
||||||
|
const user = useSelector((state: RootState) => state.authentication)
|
||||||
|
|
||||||
|
const [loader, setLoader] = useState<boolean>(false)
|
||||||
|
const [rows, setRows] = useState<RequestPagination>({
|
||||||
|
data: [],
|
||||||
|
status: true,
|
||||||
|
})
|
||||||
|
const [selectDataRow, setSelectDataRow] = useState<any>({})
|
||||||
|
|
||||||
|
const [openedModalAdd, { open: openModalAdd, close: closeModalAdd }] =
|
||||||
|
useDisclosure(false)
|
||||||
|
const [openedModalEdit, { open: openModalEdit, close: closeModalEdit }] =
|
||||||
|
useDisclosure(false)
|
||||||
|
const [openedModalFile, { open: openModalFile, close: closeModalFile }] =
|
||||||
|
useDisclosure(false)
|
||||||
|
|
||||||
|
const [openedDialogDelete, setOpenedDialogDelete] = useState(false)
|
||||||
|
const [deleteLoader, setDeleteLoader] = useState<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAllData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getAllData = async () => {
|
||||||
|
try {
|
||||||
|
setLoader(true)
|
||||||
|
const params = {}
|
||||||
|
const res = await get(listDocument, params)
|
||||||
|
if (res.status) {
|
||||||
|
setRows(res)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: error.message ?? error,
|
||||||
|
color: 'red',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setLoader(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id: number | undefined) => {
|
||||||
|
try {
|
||||||
|
setDeleteLoader(true)
|
||||||
|
await Xdelete(deleteDocument, { id: id }, getAllData)
|
||||||
|
setSelectDataRow({})
|
||||||
|
setOpenedDialogDelete(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
} finally {
|
||||||
|
setDeleteLoader(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileTypeIcon = (row: TDocument) => {
|
||||||
|
const uri = row?.uri
|
||||||
|
if (!uri) return null
|
||||||
|
|
||||||
|
const extension = uri.split('.').pop()?.toLowerCase()
|
||||||
|
|
||||||
|
if (['doc'].includes(extension!)) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`${import.meta.env.VITE_BACKEND_URL}${
|
||||||
|
import.meta.env.VITE_BACKEND_URL?.includes('localhost')
|
||||||
|
? ''
|
||||||
|
: 'image/'
|
||||||
|
}${uri}`}
|
||||||
|
download={row.title}
|
||||||
|
target="_self"
|
||||||
|
>
|
||||||
|
<IconFileTypeDoc style={{ color: '#1e62c1' }} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['xls', 'xlsx'].includes(extension!)) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`${import.meta.env.VITE_BACKEND_URL}${
|
||||||
|
import.meta.env.VITE_BACKEND_URL?.includes('localhost')
|
||||||
|
? ''
|
||||||
|
: 'image/'
|
||||||
|
}${uri}`}
|
||||||
|
download={row.title}
|
||||||
|
target="_self"
|
||||||
|
>
|
||||||
|
<IconFileTypeXls style={{ color: '#0e864b' }} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['docx'].includes(extension!)) {
|
||||||
|
return (
|
||||||
|
<IconFileTypeDoc
|
||||||
|
onClick={() => {
|
||||||
|
openModalFile()
|
||||||
|
setSelectDataRow(row)
|
||||||
|
}}
|
||||||
|
style={{ color: '#1e62c1' }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconFileTypePdf
|
||||||
|
onClick={() => {
|
||||||
|
openModalFile()
|
||||||
|
setSelectDataRow(row)
|
||||||
|
}}
|
||||||
|
style={{ color: '#ff1b0e' }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
size: '50%',
|
||||||
|
header: 'Title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'uri',
|
||||||
|
size: '45%',
|
||||||
|
header: 'URI',
|
||||||
|
render: (row: TDocument) => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{row.type === 'file' ? (
|
||||||
|
<Box
|
||||||
|
w="fit-content"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
title={`File .${row?.uri
|
||||||
|
.split('.')
|
||||||
|
.pop()
|
||||||
|
?.toLowerCase()} detail`}
|
||||||
|
>
|
||||||
|
{getFileTypeIcon(row)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Anchor
|
||||||
|
ta="start"
|
||||||
|
href={row?.uri}
|
||||||
|
target="_blank"
|
||||||
|
title={row?.uri}
|
||||||
|
>
|
||||||
|
<IconLink />
|
||||||
|
</Anchor>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '#',
|
||||||
|
size: '5%',
|
||||||
|
header: 'Action',
|
||||||
|
render: (row: TDocument) => {
|
||||||
|
if (!user.user.user.permission.includes('admin')) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={classes.optionIcon}>
|
||||||
|
<IconEdit
|
||||||
|
className={classes.editIcon}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectDataRow(row)
|
||||||
|
openModalEdit()
|
||||||
|
}}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
<IconX
|
||||||
|
className={classes.deleteIcon}
|
||||||
|
onClick={() => {
|
||||||
|
setOpenedDialogDelete(true)
|
||||||
|
setSelectDataRow(row)
|
||||||
|
}}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<div className={classes.title}>
|
||||||
|
<h3>
|
||||||
|
<Text>Admin/</Text>
|
||||||
|
Documents
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{user.user.user.permission.includes('admin') ? (
|
||||||
|
<Button onClick={() => openModalAdd()} disabled={loader}>
|
||||||
|
+ Add
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Box mt={'md'}>
|
||||||
|
{loader ? (
|
||||||
|
<Box ta={'center'}>
|
||||||
|
<Loader size={40} mt={'15%'} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<DataTableAll
|
||||||
|
data={rows.data}
|
||||||
|
columns={columns}
|
||||||
|
size=""
|
||||||
|
searchInput
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{openedModalFile && (
|
||||||
|
<ModalFileDocument
|
||||||
|
opened={openedModalFile}
|
||||||
|
close={closeModalFile}
|
||||||
|
selectDataRow={selectDataRow}
|
||||||
|
setSelectDataRow={setSelectDataRow}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{openedModalAdd && (
|
||||||
|
<ModalAddDocument
|
||||||
|
opened={openedModalAdd}
|
||||||
|
close={closeModalAdd}
|
||||||
|
getAllData={getAllData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{openedModalEdit && (
|
||||||
|
<ModalEditDocument
|
||||||
|
opened={openedModalEdit}
|
||||||
|
close={closeModalEdit}
|
||||||
|
selectDataRow={selectDataRow}
|
||||||
|
setSelectDataRow={setSelectDataRow}
|
||||||
|
getAllData={getAllData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{openedDialogDelete && (
|
||||||
|
<Dialog
|
||||||
|
opened={openedDialogDelete}
|
||||||
|
className={classes.dialog}
|
||||||
|
withCloseButton
|
||||||
|
onClose={() => {
|
||||||
|
setSelectDataRow({})
|
||||||
|
setOpenedDialogDelete(false)
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
radius="md"
|
||||||
|
position={{ top: 30, right: 10 }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className={classes.dialogText}
|
||||||
|
size="sm"
|
||||||
|
mb="xs"
|
||||||
|
fw={500}
|
||||||
|
pr={20}
|
||||||
|
>
|
||||||
|
<Text ta="center">
|
||||||
|
Do you want to delete the document <b>{selectDataRow?.title}</b> ?
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Group justify="center" m={10}>
|
||||||
|
<Button
|
||||||
|
fw={700}
|
||||||
|
size="xs"
|
||||||
|
variant="filled"
|
||||||
|
color="red"
|
||||||
|
onClick={async () => handleDelete(selectDataRow?.id)}
|
||||||
|
disabled={deleteLoader}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
fw={700}
|
||||||
|
size="xs"
|
||||||
|
variant="filled"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectDataRow({})
|
||||||
|
setOpenedDialogDelete(false)
|
||||||
|
}}
|
||||||
|
disabled={deleteLoader}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Document
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useForm } from '@mantine/form'
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Switch,
|
||||||
|
Checkbox,
|
||||||
|
FileInput,
|
||||||
|
TextInput,
|
||||||
|
Group,
|
||||||
|
ActionIcon,
|
||||||
|
} from '@mantine/core'
|
||||||
|
import { IconPlus, IconTrash } from '@tabler/icons-react'
|
||||||
|
|
||||||
|
import { create } from '@/rtk/helpers/CRUD'
|
||||||
|
import { createDocument } from '@/api/Admin'
|
||||||
|
import { getHeaderInfo } from '@/rtk/helpers/tokenCreator'
|
||||||
|
import { notifications } from '@mantine/notifications'
|
||||||
|
|
||||||
|
type MProps = {
|
||||||
|
opened: boolean
|
||||||
|
close: () => void
|
||||||
|
getAllData: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalAddDocument = ({ opened, close, getAllData }: MProps) => {
|
||||||
|
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
type: 'file',
|
||||||
|
files: [] as { title: string; file: File }[],
|
||||||
|
links: [] as { title: string; uri: string }[],
|
||||||
|
is_active: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleCreate = async (values: any) => {
|
||||||
|
try {
|
||||||
|
if (values.type === 'file' && values.files.length === 0) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'No files uploaded!!!',
|
||||||
|
color: 'red',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.type === 'link' && values.links.length === 0) {
|
||||||
|
notifications.show({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'No links provided!!!',
|
||||||
|
color: 'red',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingSubmit(true)
|
||||||
|
const header = await getHeaderInfo()
|
||||||
|
const formData = new FormData()
|
||||||
|
header.headers['Content-Type'] = 'multipart/form-data'
|
||||||
|
|
||||||
|
formData.append('type', values.type)
|
||||||
|
formData.append('is_active', values.is_active ? '1' : '0')
|
||||||
|
|
||||||
|
if (values.type === 'file') {
|
||||||
|
values.files.forEach((item: any, index: number) => {
|
||||||
|
formData.append(`files[${index}][title]`, item.title)
|
||||||
|
formData.append(`files[${index}][file]`, item.file)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
values.links.forEach((item: any, index: number) => {
|
||||||
|
formData.append(`links[${index}][title]`, item.title)
|
||||||
|
formData.append(`links[${index}][uri]`, item.uri)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await create(createDocument, formData, getAllData, header)
|
||||||
|
if (res === true) {
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setLoadingSubmit(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
close()
|
||||||
|
form.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={resetForm}
|
||||||
|
size="lg"
|
||||||
|
title={
|
||||||
|
<Text fw={700} fz={'lg'}>
|
||||||
|
Add Document
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form onSubmit={form.onSubmit(handleCreate)}>
|
||||||
|
<Switch
|
||||||
|
style={{ width: 'fit-content' }}
|
||||||
|
label={form.values.type === 'file' ? 'Upload files' : 'Enter links'}
|
||||||
|
checked={form.values.type === 'file'}
|
||||||
|
onChange={(event) =>
|
||||||
|
form.setFieldValue(
|
||||||
|
'type',
|
||||||
|
event.currentTarget.checked ? 'file' : 'link',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mb={'md'}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.values.type === 'file' ? (
|
||||||
|
<Box>
|
||||||
|
<FileInput
|
||||||
|
accept=".doc,.docx,.xls,.xlsx,.pdf"
|
||||||
|
label="Upload Doc, Excel, PDF files"
|
||||||
|
multiple
|
||||||
|
mb="md"
|
||||||
|
value={form.values.files.map((file) => file.file)}
|
||||||
|
onChange={(files) => {
|
||||||
|
if (files) {
|
||||||
|
const newFiles = files.map((file) => ({
|
||||||
|
title: file.name.split('.')[0],
|
||||||
|
file,
|
||||||
|
}))
|
||||||
|
form.setFieldValue('files', newFiles)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.values.files.map((item, index) => (
|
||||||
|
<Group key={index} mb={'md'}>
|
||||||
|
<TextInput
|
||||||
|
w="50%"
|
||||||
|
label="Title"
|
||||||
|
value={form.values.files[index].title}
|
||||||
|
onChange={(event) =>
|
||||||
|
form.setFieldValue(
|
||||||
|
`files.${index}.title`,
|
||||||
|
event.currentTarget.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
maxLength={255}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Text mt={24} w="30%">
|
||||||
|
{item.file?.name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
{form.values.links.map((_, index) => (
|
||||||
|
<Group key={index} mb={'md'}>
|
||||||
|
<TextInput
|
||||||
|
w="35%"
|
||||||
|
label="Title"
|
||||||
|
value={form.values.links[index].title}
|
||||||
|
onChange={(event) =>
|
||||||
|
form.setFieldValue(
|
||||||
|
`links.${index}.title`,
|
||||||
|
event.currentTarget.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
maxLength={255}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
w="50%"
|
||||||
|
label="URL"
|
||||||
|
value={form.values.links[index].uri}
|
||||||
|
onChange={(event) =>
|
||||||
|
form.setFieldValue(
|
||||||
|
`links.${index}.uri`,
|
||||||
|
event.currentTarget.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
variant="light"
|
||||||
|
onClick={() =>
|
||||||
|
form.setFieldValue(
|
||||||
|
'links',
|
||||||
|
form.values.links.filter((_, i) => i !== index),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
mt={24}
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
leftSection={<IconPlus size={16} />}
|
||||||
|
variant="light"
|
||||||
|
onClick={() =>
|
||||||
|
form.setFieldValue('links', [
|
||||||
|
...form.values.links,
|
||||||
|
{ title: '', uri: '' },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
mb="md"
|
||||||
|
>
|
||||||
|
Add Link
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Is Active"
|
||||||
|
mb={'md'}
|
||||||
|
checked={form.values.is_active}
|
||||||
|
onChange={(event) =>
|
||||||
|
form.setFieldValue('is_active', event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box ta="center" mt="lg">
|
||||||
|
<Button color="green" type="submit" loading={loadingSubmit}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalAddDocument
|
||||||
|
|
@ -0,0 +1,285 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useForm } from '@mantine/form'
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Checkbox,
|
||||||
|
FileInput,
|
||||||
|
Badge,
|
||||||
|
TextInput,
|
||||||
|
Group,
|
||||||
|
} from '@mantine/core'
|
||||||
|
|
||||||
|
import { update } from '@/rtk/helpers/CRUD'
|
||||||
|
import { updateDocument } from '@/api/Admin'
|
||||||
|
import { getHeaderInfo } from '@/rtk/helpers/tokenCreator'
|
||||||
|
import {
|
||||||
|
IconFileTypeDoc,
|
||||||
|
IconFileTypePdf,
|
||||||
|
IconFileTypeXls,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
|
import ModalFileDocument from './ModalFileDocument'
|
||||||
|
import { useDisclosure } from '@mantine/hooks'
|
||||||
|
|
||||||
|
type MProps = {
|
||||||
|
opened: boolean
|
||||||
|
close: () => void
|
||||||
|
selectDataRow: any
|
||||||
|
setSelectDataRow: any
|
||||||
|
getAllData: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalEditDocument = ({
|
||||||
|
opened,
|
||||||
|
close,
|
||||||
|
selectDataRow,
|
||||||
|
setSelectDataRow,
|
||||||
|
getAllData,
|
||||||
|
}: MProps) => {
|
||||||
|
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
||||||
|
const [selectDataFileRow, setSelectDataFileRow] = useState<any>({})
|
||||||
|
|
||||||
|
const [openedModalFile, { open: openModalFile, close: closeModalFile }] =
|
||||||
|
useDisclosure(false)
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
title: '',
|
||||||
|
type: true,
|
||||||
|
file: null as File | null,
|
||||||
|
uri: '',
|
||||||
|
is_active: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setValues({
|
||||||
|
title: selectDataRow?.title || '',
|
||||||
|
type: selectDataRow?.type === 'file',
|
||||||
|
uri: selectDataRow?.uri || '',
|
||||||
|
is_active: selectDataRow?.is_active || true,
|
||||||
|
})
|
||||||
|
}, [selectDataRow])
|
||||||
|
|
||||||
|
const handleUpdate = async (data: any) => {
|
||||||
|
try {
|
||||||
|
setLoadingSubmit(true)
|
||||||
|
|
||||||
|
let formData: any = {}
|
||||||
|
const header = await getHeaderInfo()
|
||||||
|
|
||||||
|
if (data.type) {
|
||||||
|
header.headers['Content-Type'] = 'multipart/form-data'
|
||||||
|
const tmpFormData = new FormData()
|
||||||
|
|
||||||
|
tmpFormData.append('title', data.title)
|
||||||
|
tmpFormData.append('type', 'file')
|
||||||
|
tmpFormData.append('is_active', data.is_active ? '1' : '0')
|
||||||
|
tmpFormData.append('existing_file', data.uri)
|
||||||
|
|
||||||
|
if (data.file) {
|
||||||
|
tmpFormData.append('file', data.file)
|
||||||
|
}
|
||||||
|
|
||||||
|
formData = tmpFormData
|
||||||
|
} else {
|
||||||
|
formData = {
|
||||||
|
title: data.title,
|
||||||
|
type: 'link',
|
||||||
|
uri: data.uri,
|
||||||
|
is_active: data.is_active ? '1' : '0',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await update(
|
||||||
|
updateDocument + `?id=${selectDataRow?.id}`,
|
||||||
|
formData,
|
||||||
|
getAllData,
|
||||||
|
header,
|
||||||
|
)
|
||||||
|
if (res === true) {
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
setLoadingSubmit(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileTypeIcon = (uri: string) => {
|
||||||
|
if (!uri) return null
|
||||||
|
|
||||||
|
const extension = uri.split('.').pop()?.toLowerCase()
|
||||||
|
|
||||||
|
if (['doc'].includes(extension!)) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`${import.meta.env.VITE_BACKEND_URL}${uri}`}
|
||||||
|
download="Document Download"
|
||||||
|
target="_self"
|
||||||
|
>
|
||||||
|
<IconFileTypeDoc style={{ color: '#1e62c1' }} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['xls', 'xlsx'].includes(extension!)) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`${import.meta.env.VITE_BACKEND_URL}${uri}`}
|
||||||
|
download="Document Download"
|
||||||
|
target="_self"
|
||||||
|
>
|
||||||
|
<IconFileTypeXls style={{ color: '#0e864b' }} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['docx'].includes(extension!)) {
|
||||||
|
return (
|
||||||
|
<IconFileTypeDoc
|
||||||
|
onClick={() => {
|
||||||
|
openModalFile()
|
||||||
|
setSelectDataFileRow(selectDataRow)
|
||||||
|
}}
|
||||||
|
style={{ color: '#1e62c1' }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconFileTypePdf
|
||||||
|
onClick={() => {
|
||||||
|
openModalFile()
|
||||||
|
setSelectDataFileRow(selectDataRow)
|
||||||
|
}}
|
||||||
|
style={{ color: '#ff1b0e' }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
close()
|
||||||
|
form.reset()
|
||||||
|
setSelectDataRow({})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={resetForm}
|
||||||
|
size="lg"
|
||||||
|
title={
|
||||||
|
<Text fw={700} fz={'lg'}>
|
||||||
|
Update Document
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => {
|
||||||
|
handleUpdate(values)
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{form.values.type ? (
|
||||||
|
<Badge tt="initial" color="orange" mb="md">
|
||||||
|
Files
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge tt="initial" mb="md">
|
||||||
|
Links
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.values.type ? (
|
||||||
|
<Box>
|
||||||
|
<TextInput
|
||||||
|
label="Title"
|
||||||
|
key={'title'}
|
||||||
|
{...form.getInputProps('title')}
|
||||||
|
maxLength={255}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
required
|
||||||
|
mb={'md'}
|
||||||
|
/>
|
||||||
|
<FileInput
|
||||||
|
accept=".doc,.docx,.xls,.xlsx,.pdf"
|
||||||
|
label="Upload Doc, Excel, PDF file"
|
||||||
|
value={form.values.file}
|
||||||
|
onChange={(file) => form.setFieldValue('file', file || null)}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
mb={'md'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.values.file ? (
|
||||||
|
''
|
||||||
|
) : (
|
||||||
|
<Group
|
||||||
|
w="fit-content"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
mb={'md'}
|
||||||
|
title={`File ${form.values.uri
|
||||||
|
.split('.')
|
||||||
|
.pop()
|
||||||
|
?.toLowerCase()} detail`}
|
||||||
|
>
|
||||||
|
{getFileTypeIcon(form.values.uri)}
|
||||||
|
|
||||||
|
<Text>.{form.values.uri.split('.').pop()?.toLowerCase()}</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
<TextInput
|
||||||
|
label="Title"
|
||||||
|
key={'title'}
|
||||||
|
{...form.getInputProps('title')}
|
||||||
|
maxLength={255}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
required
|
||||||
|
mb={'md'}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Enter URI"
|
||||||
|
key={'uri'}
|
||||||
|
{...form.getInputProps('uri')}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
required
|
||||||
|
mb={'md'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Is Active"
|
||||||
|
mb={'md'}
|
||||||
|
checked={form.values.is_active}
|
||||||
|
onChange={(event) =>
|
||||||
|
form.setFieldValue('is_active', event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
disabled={loadingSubmit}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box ta="center" mt="lg">
|
||||||
|
<Button color="green" type="submit" loading={loadingSubmit}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{openedModalFile && (
|
||||||
|
<ModalFileDocument
|
||||||
|
opened={openedModalFile}
|
||||||
|
close={closeModalFile}
|
||||||
|
selectDataRow={selectDataFileRow}
|
||||||
|
setSelectDataRow={setSelectDataFileRow}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalEditDocument
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { getDownloadFile } from '@/rtk/helpers/apiService'
|
||||||
|
import { Modal, Text, Box, Loader, Paper, Button, Group } from '@mantine/core'
|
||||||
|
import FileViewer from 'react-file-viewer'
|
||||||
|
import { IconDownload } from '@tabler/icons-react'
|
||||||
|
import { downloadFile } from '@/api/Admin'
|
||||||
|
|
||||||
|
type MProps = {
|
||||||
|
opened: boolean
|
||||||
|
close: () => void
|
||||||
|
selectDataRow: any
|
||||||
|
setSelectDataRow: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TDocumentFile {
|
||||||
|
uri: string
|
||||||
|
fileType: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalFileDocument = ({
|
||||||
|
opened,
|
||||||
|
close,
|
||||||
|
selectDataRow,
|
||||||
|
setSelectDataRow,
|
||||||
|
}: MProps) => {
|
||||||
|
const [fileDoc, setFileDoc] = useState<TDocumentFile>()
|
||||||
|
const [loader, setLoader] = useState<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getFile()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getFile = async () => {
|
||||||
|
try {
|
||||||
|
setLoader(true)
|
||||||
|
const params = {}
|
||||||
|
const fileUri = selectDataRow?.uri.replace('storage/uploads/', '')
|
||||||
|
const res = await getDownloadFile(`${downloadFile}/${fileUri}`, params)
|
||||||
|
|
||||||
|
setFileDoc({
|
||||||
|
uri: URL.createObjectURL(res.data),
|
||||||
|
fileType: getFileType(selectDataRow?.uri) || 'default',
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error)
|
||||||
|
} finally {
|
||||||
|
setLoader(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportedFileTypes = ['pdf', 'xlsx', 'xls', 'docx', 'doc']
|
||||||
|
const getFileType = (fileName: string) => {
|
||||||
|
const extension = fileName.split('.').pop()?.toLowerCase()
|
||||||
|
|
||||||
|
return supportedFileTypes.includes(extension!) ? extension : 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={() => {
|
||||||
|
close()
|
||||||
|
setSelectDataRow({})
|
||||||
|
}}
|
||||||
|
size="65%"
|
||||||
|
title={
|
||||||
|
<Text fw={700} fz={'lg'}>
|
||||||
|
File Detail: {selectDataRow?.title}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Group justify="flex-end" mb={'md'}>
|
||||||
|
<a
|
||||||
|
href={`${import.meta.env.VITE_BACKEND_URL}${
|
||||||
|
import.meta.env.VITE_BACKEND_URL?.includes('localhost')
|
||||||
|
? ''
|
||||||
|
: 'image/'
|
||||||
|
}${selectDataRow.uri}`}
|
||||||
|
download="Document Download"
|
||||||
|
target={
|
||||||
|
getFileType(selectDataRow?.uri) === 'pdf' ? '_blank' : '_self'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
leftSection={<IconDownload size={18} />}
|
||||||
|
color={
|
||||||
|
getFileType(selectDataRow?.uri) === 'pdf'
|
||||||
|
? 'red'
|
||||||
|
: getFileType(selectDataRow?.uri) === 'xlsx' ||
|
||||||
|
getFileType(selectDataRow?.uri) === 'xls'
|
||||||
|
? 'green'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Download .{getFileType(selectDataRow?.uri)}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper withBorder>
|
||||||
|
{loader ? (
|
||||||
|
<Box ta={'center'} my={20}>
|
||||||
|
<Loader size={40} />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box w="100%">
|
||||||
|
<FileViewer fileType={fileDoc?.fileType} filePath={fileDoc?.uri} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalFileDocument
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
import {
|
import {
|
||||||
deleteFile,
|
getFiles,
|
||||||
getAllFilesInProfiles,
|
|
||||||
getProfilesData,
|
getProfilesData,
|
||||||
listUserTechnical,
|
listUserTechnical,
|
||||||
updateProfileFolder,
|
|
||||||
updateProfilesData,
|
updateProfilesData,
|
||||||
updateUserTechnical,
|
updateUserTechnical,
|
||||||
|
uploadFiles
|
||||||
} from '@/api/Admin'
|
} from '@/api/Admin'
|
||||||
import { changePassword } from '@/api/Auth'
|
import { changePassword } from '@/api/Auth'
|
||||||
import DataTableAll from '@/components/DataTable/DataTable'
|
import DataTableAll from '@/components/DataTable/DataTable'
|
||||||
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
|
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
|
||||||
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
|
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
|
||||||
import { logout } from '@/rtk/dispatches/auth'
|
import { logout } from '@/rtk/dispatches/auth'
|
||||||
import { Xdelete } from '@/rtk/helpers/CRUD'
|
|
||||||
import { get, post, postImage } from '@/rtk/helpers/apiService'
|
import { get, post, postImage } from '@/rtk/helpers/apiService'
|
||||||
import { requirementsPassword } from '@/rtk/helpers/variables'
|
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
||||||
|
|
@ -22,16 +20,13 @@ import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
FileInput,
|
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
|
||||||
Loader,
|
Loader,
|
||||||
Modal,
|
Modal,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
Stack,
|
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title
|
||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
import { notifications } from '@mantine/notifications'
|
import { notifications } from '@mantine/notifications'
|
||||||
import {
|
import {
|
||||||
|
|
@ -46,15 +41,26 @@ import moment from 'moment'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import classes from './Profile.module.css'
|
import classes from './Profile.module.css'
|
||||||
|
import FileUploadForm from './components/FileUploadForm'
|
||||||
|
|
||||||
const isCompactMenu = false
|
const isCompactMenu = false
|
||||||
|
|
||||||
type TFileProfile = {
|
// type TFileProfile = {
|
||||||
label: string
|
// label: string
|
||||||
type: string
|
// type: string
|
||||||
value: string
|
// value: string
|
||||||
children?: TFileProfile[]
|
// children?: TFileProfile[]
|
||||||
|
// }
|
||||||
|
|
||||||
|
interface FileData {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const user = useAppSelector((state) => state.authentication.user)
|
const user = useAppSelector((state) => state.authentication.user)
|
||||||
const userData = getUser()
|
const userData = getUser()
|
||||||
|
|
@ -77,6 +83,11 @@ const Profile = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [data, setData] = useState<FileData[]>([])
|
||||||
|
const [openedProfile, setOpenedProfile] = useState(false)
|
||||||
|
|
||||||
const updateAvatar = async (file: File) => {
|
const updateAvatar = async (file: File) => {
|
||||||
try {
|
try {
|
||||||
const res = await postImage(updateProfilesData, file, 'post')
|
const res = await postImage(updateProfilesData, file, 'post')
|
||||||
|
|
@ -137,102 +148,113 @@ const Profile = () => {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const [cv, setCv] = useState<File>()
|
// const [cv, setCv] = useState<File>()
|
||||||
const [idCard, setIdCard] = useState<File>()
|
// const [idCard, setIdCard] = useState<File>()
|
||||||
const [transcript, setTranscript] = useState<File>()
|
// const [transcript, setTranscript] = useState<File>()
|
||||||
const [universityDiploma, setUniversityDiploma] = useState<File>()
|
// const [universityDiploma, setUniversityDiploma] = useState<File>()
|
||||||
const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
|
// const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
|
||||||
const [data, setData] = useState<TFileProfile[]>([])
|
// const handleOtherFileChange = (
|
||||||
const [openedProfile, setOpenedProfile] = useState(false)
|
// index: number,
|
||||||
const handleOtherFileChange = (
|
// field: string,
|
||||||
index: number,
|
// value: File | string,
|
||||||
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()
|
e.preventDefault()
|
||||||
|
setIsLoading(true)
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
// Append each selected file to FormData
|
if (selectedFile) {
|
||||||
for (let i = 0; i < otherFiles.length; i++) {
|
formData.append('file', selectedFile)
|
||||||
if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
|
formData.append('name', fileName)
|
||||||
formData.append(
|
formData.append('description', description)
|
||||||
'files[]',
|
formData.append('user_name', user.user.name)
|
||||||
handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
|
formData.append('user_id', user.user.id.toString())
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
const response = await axios.post(updateProfileFolder, formData, {
|
const token = await getAccessToken()
|
||||||
|
const response = await axios.post(uploadFiles, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if(response.status === 200){
|
if (response.status === 200) {
|
||||||
getAllFile()
|
await getAllFile()
|
||||||
setOtherFiles([])
|
setSelectedFile(null)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error uploading files', error)
|
console.error('Error uploading file:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllFile = async () => {
|
const getAllFile = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await get(getAllFilesInProfiles, {
|
const res = await get(getFiles)
|
||||||
root_folder: '/storage/profiles/' + JSON.parse(getUser())?.user?.name,
|
|
||||||
})
|
|
||||||
if (res.status === true) {
|
if (res.status === true) {
|
||||||
setData(res.data)
|
const userFiles = res.data[user.user.name] || [];
|
||||||
|
setData(userFiles);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(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 {
|
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) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error);
|
||||||
}
|
notifications.show({
|
||||||
|
title: 'Lỗi',
|
||||||
|
message: 'Không thể xóa file',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const result = await getListProfilesData()
|
const result = await getListProfilesData()
|
||||||
setDataProfile(result ?? [])
|
setDataProfile(result ?? [])
|
||||||
|
await getAllFile()
|
||||||
}
|
}
|
||||||
fetchData()
|
fetchData()
|
||||||
getAllFile()
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleChangePassword = async () => {
|
const handleChangePassword = async () => {
|
||||||
|
|
@ -306,23 +328,19 @@ const Profile = () => {
|
||||||
dispatch(logout(navigate))
|
dispatch(logout(navigate))
|
||||||
}, [dispatch, navigate])
|
}, [dispatch, navigate])
|
||||||
|
|
||||||
const handleChangeFileName = (e: File, newName: string) => {
|
// const handleChangeFileName = (e: File, newName: string): File => {
|
||||||
const originalFile = e // Get the original file
|
// const originalFile = e;
|
||||||
const extend = originalFile.name.split('.')[1]
|
// const extend = originalFile.name.split('.')[1];
|
||||||
if (originalFile) {
|
// const newFileName = `${newName}.${extend}`;
|
||||||
const newFileName = `${newName}.${extend}` // Create new file name
|
// return new File([originalFile], newFileName, {
|
||||||
const newFile = new File([originalFile], newFileName, {
|
// type: originalFile.type,
|
||||||
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.name.includes(nameField));
|
||||||
}
|
// return file;
|
||||||
|
// };
|
||||||
const checkFileExist = (nameField: string) => {
|
|
||||||
const file = data.find((f) => f.label.includes(nameField))
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -578,162 +596,14 @@ const Profile = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<form onSubmit={handleSubmit}>
|
<FileUploadForm
|
||||||
<Stack>
|
data={data}
|
||||||
<Box
|
handleSubmit={handleSubmit}
|
||||||
style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
|
handleFileChange={(file) => file && setSelectedFile(file)}
|
||||||
>
|
removeFile={removeFile}
|
||||||
<Text>CV</Text>
|
isLoading={isLoading}
|
||||||
<a href={checkFileExist('cv')?.value} target="_blank">
|
currentUser={user.user.name}
|
||||||
<Text>{`: ${checkFileExist('cv')?.label}`}</Text>
|
|
||||||
</a>
|
|
||||||
<Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('cv')?.value!)}}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<FileInput
|
|
||||||
label={'CV'}
|
|
||||||
placeholder="Tải lên CV"
|
|
||||||
style={{ display: checkFileExist('cv') ? 'none' : 'block' }}
|
|
||||||
onChange={(e) => {
|
|
||||||
0
|
|
||||||
setCv(handleChangeFileName(e!, 'cv'))
|
|
||||||
}}
|
|
||||||
accept=".pdf,.doc,.docx"
|
|
||||||
/>
|
/>
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: checkFileExist('idCard') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>CCCD</Text>
|
|
||||||
<a href={checkFileExist('idCard')?.value} target="_blank">
|
|
||||||
<Text>{`: ${checkFileExist('idCard')?.label}`}</Text>
|
|
||||||
</a>
|
|
||||||
<Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('idCard')?.value!)}}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<FileInput
|
|
||||||
label="CCCD"
|
|
||||||
style={{ display: checkFileExist('idCard') ? 'none' : 'block' }}
|
|
||||||
placeholder="Tải lên CCCD"
|
|
||||||
onChange={(e) => {
|
|
||||||
setIdCard(handleChangeFileName(e!, 'idCard'))
|
|
||||||
}}
|
|
||||||
accept=".jpg,.jpeg,.png,.pdf"
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: checkFileExist('transcript') ? 'flex' : 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>Bảng điểm</Text>
|
|
||||||
<a href={checkFileExist('transcript')?.value} target="_blank">
|
|
||||||
<Text>{`: ${checkFileExist('transcript')?.label}`}</Text>
|
|
||||||
</a>
|
|
||||||
<Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('transcript')?.value!)}}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<FileInput
|
|
||||||
label="Bảng điểm"
|
|
||||||
style={{ display: checkFileExist('transcript') ? 'none' : 'block' }}
|
|
||||||
placeholder="Tải lên bảng điểm"
|
|
||||||
onChange={(e) => {
|
|
||||||
setTranscript(handleChangeFileName(e!, 'transcript'))
|
|
||||||
}}
|
|
||||||
accept=".pdf"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: checkFileExist('universityDiploma')
|
|
||||||
? 'flex'
|
|
||||||
: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>Bằng đại học</Text>
|
|
||||||
<a
|
|
||||||
href={checkFileExist('universityDiploma')?.value}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Text>{`: ${
|
|
||||||
checkFileExist('universityDiploma')?.label
|
|
||||||
}`}</Text>
|
|
||||||
</a>
|
|
||||||
<Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('universityDiploma')?.value!)}}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<FileInput
|
|
||||||
label="Bằng đại học"
|
|
||||||
style={{ display: checkFileExist('universityDiploma') ? 'none' : 'block' }}
|
|
||||||
placeholder="Tải lên bằng đại học"
|
|
||||||
onChange={(e) => {
|
|
||||||
setUniversityDiploma(
|
|
||||||
handleChangeFileName(e!, 'universityDiploma'),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
accept=".pdf,.jpg,.jpeg,.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text>Danh sách file khác:</Text>
|
|
||||||
<Box>
|
|
||||||
{data.find((f)=>f.label === 'others')?.children?.map((c, index)=>{
|
|
||||||
return <Box key={index}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
marginBottom:'5px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={c?.value}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Text>{`${
|
|
||||||
c?.label
|
|
||||||
}`}</Text>
|
|
||||||
</a>
|
|
||||||
<Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(c?.value!)}}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
{otherFiles.map((fileInput, index) => (
|
|
||||||
<Group key={index}>
|
|
||||||
<FileInput
|
|
||||||
placeholder="Chọn file"
|
|
||||||
onChange={(file) =>
|
|
||||||
handleOtherFileChange(index, 'file', file!)
|
|
||||||
}
|
|
||||||
w={'30%'}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
placeholder="Nhập tên file (không cần phần mở rộng)"
|
|
||||||
value={fileInput.type}
|
|
||||||
w={'65%'}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleOtherFileChange(
|
|
||||||
index,
|
|
||||||
'type',
|
|
||||||
e.currentTarget.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
<Button type="button" onClick={addOtherFileInput}>
|
|
||||||
Thêm file khác
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button type="submit" color="blue">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</form>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -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); }
|
||||||
|
}
|
||||||
|
|
@ -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<boolean | void>;
|
||||||
|
handleFileChange: (file: File | null) => void;
|
||||||
|
removeFile: (id: number) => Promise<void>;
|
||||||
|
isLoading: boolean;
|
||||||
|
currentUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileUploadForm = ({
|
||||||
|
data,
|
||||||
|
handleSubmit,
|
||||||
|
handleFileChange,
|
||||||
|
removeFile,
|
||||||
|
isLoading,
|
||||||
|
currentUser,
|
||||||
|
}: FileUploadFormProps) => {
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(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 <IconFileTypeDocx size={16} />
|
||||||
|
case 'image':
|
||||||
|
return <IconPhoto size={16} />
|
||||||
|
case 'spreadsheet':
|
||||||
|
return <IconFileTypeXls size={16} />
|
||||||
|
default:
|
||||||
|
return <IconFileTypePdf size={16} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredFiles = data.filter(
|
||||||
|
(file) =>
|
||||||
|
file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(file.description &&
|
||||||
|
file.description.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading && (
|
||||||
|
<div className={classes.loadingOverlay}>
|
||||||
|
<div className={classes.loadingSpinner} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleFormSubmit}>
|
||||||
|
<Box>
|
||||||
|
<Text className={classes.sectionTitle}>Tài liệu</Text>
|
||||||
|
|
||||||
|
<Box className={classes.fileInputGroup}>
|
||||||
|
<FileInput
|
||||||
|
label="Chọn file"
|
||||||
|
placeholder="Chọn file để tải lên"
|
||||||
|
accept="image/png,image/jpeg,image/jpg,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.spreadsheet"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
value={selectedFile}
|
||||||
|
className={classes.fileInput}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Tên file"
|
||||||
|
placeholder="Nhập tên file"
|
||||||
|
value={fileName}
|
||||||
|
onChange={(e) => setFileName(e.target.value)}
|
||||||
|
className={classes.fileNameInput}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
label="Mô tả"
|
||||||
|
placeholder="Nhập mô tả cho file"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
className={classes.descriptionInput}
|
||||||
|
minRows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="blue"
|
||||||
|
className={classes.saveButton}
|
||||||
|
disabled={isLoading || isUploading || !selectedFile || !fileName}
|
||||||
|
loading={isLoading || isUploading}
|
||||||
|
>
|
||||||
|
{isLoading || isUploading ? 'Đang xử lý...' : 'Lưu thay đổi'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className={classes.fileListContainer}>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Tìm kiếm theo tên hoặc mô tả..."
|
||||||
|
leftSection={<IconSearch size={14} />}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className={classes.searchInput}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack className={classes.fileList} gap="xs">
|
||||||
|
{filteredFiles.map((file) => (
|
||||||
|
<Card
|
||||||
|
key={file.id}
|
||||||
|
shadow="xs"
|
||||||
|
padding="xs"
|
||||||
|
radius="sm"
|
||||||
|
withBorder
|
||||||
|
>
|
||||||
|
<Group justify="space-between" gap="xs" wrap="nowrap">
|
||||||
|
<Group gap="xs" className={classes.cardContent}>
|
||||||
|
{getFileIcon(file.type)}
|
||||||
|
<Box style={{ minWidth: 0 }}>
|
||||||
|
<Text size="xs" fw={500} className={classes.cardTitle}>
|
||||||
|
{file.name}
|
||||||
|
</Text>
|
||||||
|
{file.description && (
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="dimmed"
|
||||||
|
className={classes.cardDescription}
|
||||||
|
>
|
||||||
|
{file.description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Uploaded:{' '}
|
||||||
|
{new Date(file.created_at).toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs" wrap="nowrap">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="blue"
|
||||||
|
component="a"
|
||||||
|
href={`${import.meta.env.VITE_BACKEND_URL}${
|
||||||
|
import.meta.env.VITE_BACKEND_URL?.includes(
|
||||||
|
'localhost',
|
||||||
|
)
|
||||||
|
? ''
|
||||||
|
: 'image/'
|
||||||
|
}${file.url.slice(1)}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Group gap={2}>
|
||||||
|
<IconDownload size={12} />
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
onClick={() => removeFile(file.id)}
|
||||||
|
>
|
||||||
|
<Group gap={2}>
|
||||||
|
<IconTrash size={12} />
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileUploadForm
|
||||||
|
|
@ -5,6 +5,7 @@ import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
|
||||||
import AllProfiles from '@/pages/AllProfiles/AllProfiles'
|
import AllProfiles from '@/pages/AllProfiles/AllProfiles'
|
||||||
import Allocation from '@/pages/Allocation/Allocation'
|
import Allocation from '@/pages/Allocation/Allocation'
|
||||||
import PageLogin from '@/pages/Auth/Login/Login'
|
import PageLogin from '@/pages/Auth/Login/Login'
|
||||||
|
import Document from '@/pages/Document/Document'
|
||||||
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
|
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
|
||||||
import PageNotFound from '@/pages/NotFound/NotFound'
|
import PageNotFound from '@/pages/NotFound/NotFound'
|
||||||
import OrganizationSettings from '@/pages/OrganizationSettings/OrganizationSettings'
|
import OrganizationSettings from '@/pages/OrganizationSettings/OrganizationSettings'
|
||||||
|
|
@ -81,6 +82,20 @@ const mainRoutes = [
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/documents',
|
||||||
|
element: (
|
||||||
|
<ProtectedRoute mode="home" permission="staff,accountant">
|
||||||
|
<BasePage
|
||||||
|
main={
|
||||||
|
<>
|
||||||
|
<Document />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
></BasePage>
|
||||||
|
</ProtectedRoute>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/timekeeping',
|
path: '/timekeeping',
|
||||||
element: (
|
element: (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue