update logic upload file
This commit is contained in:
parent
6e2a8c2578
commit
4da48df8d7
|
|
@ -10,9 +10,14 @@ use App\Traits\HasFilterRequest;
|
|||
use App\Traits\HasOrderByRequest;
|
||||
use App\Traits\HasSearchRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Admin\app\Models\Admin;
|
||||
use Modules\Admin\app\Models\Sprint;
|
||||
use Modules\Admin\app\Models\UserCriteria;
|
||||
use App\Models\Files;
|
||||
use App\DataTransferObjects\FileData;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
|
|
@ -28,6 +33,7 @@ class ProfileController extends Controller
|
|||
$this->jiraService = $jiraService;
|
||||
}
|
||||
|
||||
|
||||
public function getProfilesData(Request $request)
|
||||
{
|
||||
$user = auth('admins')->user();
|
||||
|
|
@ -125,7 +131,7 @@ class ProfileController extends Controller
|
|||
$rootFolder = rtrim($rootFolder, '/') . '/';
|
||||
|
||||
// Get all files and directories in the specified root folder
|
||||
$fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder: 'image'.$rootFolder);
|
||||
$fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder : 'image' . $rootFolder);
|
||||
|
||||
return response()->json(['data' => $fileList, 'status' => true]);
|
||||
}
|
||||
|
|
@ -185,6 +191,10 @@ class ProfileController extends Controller
|
|||
if (!Storage::disk('public')->exists($othersDirectory)) {
|
||||
Storage::disk('public')->makeDirectory($othersDirectory);
|
||||
}
|
||||
|
||||
$adminEmails = Admin::where('permission', 'like', '%admin%')->pluck('email')->toArray();
|
||||
$currentUser = auth('admins')->user();
|
||||
|
||||
if ($request->hasFile('files')) {
|
||||
foreach ($request->file('files') as $file) {
|
||||
// Store the file and get its path
|
||||
|
|
@ -197,6 +207,32 @@ class ProfileController extends Controller
|
|||
$path = $file->storeAs($baseDirectory, $originalFilename, 'public');
|
||||
}
|
||||
$uploadedFiles[] = $path;
|
||||
|
||||
// Tạo URL đầy đủ cho file
|
||||
$fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production')
|
||||
? env('APP_URL') . '/image/' . str_replace('/storage/', '', Storage::url($path))
|
||||
: env('APP_URL') . str_replace('/storage/', '', Storage::url($path));
|
||||
|
||||
// // Gửi email thông báo cho admin
|
||||
// foreach ($adminEmails as $adminEmail) {
|
||||
// $admin = Admin::where('email', $adminEmail)->first();
|
||||
// if ($admin) {
|
||||
// $this->sendFileUploadNotification(
|
||||
// $admin,
|
||||
// "File {$originalFilename} đã được tải lên bởi {$currentUser->name}",
|
||||
// $fileUrl,
|
||||
// "[APAC Tech] {$currentUser->name} - Đã tải lên file mới"
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Gửi email xác nhận cho người tải lên
|
||||
// $this->sendFileUploadNotification(
|
||||
// $currentUser,
|
||||
// "Bạn đã tải lên file {$originalFilename} thành công",
|
||||
// $fileUrl,
|
||||
// "[APAC Tech] {$currentUser->name} - Tải file thành công"
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -237,4 +273,195 @@ class ProfileController extends Controller
|
|||
'message' => 'File not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
public function sendFileUploadNotification($user, $description, $url, $subject, $note)
|
||||
{
|
||||
try {
|
||||
// Gửi email bất đồng bộ không cần job
|
||||
dispatch(function() use ($user, $description, $url, $subject, $note) {
|
||||
Mail::send('emails.file_upload_notification', [
|
||||
'user' => $user,
|
||||
'description' => $description,
|
||||
'url' => $url,
|
||||
'note' => $note
|
||||
], function ($message) use ($user, $subject) {
|
||||
$message->to($user->email)
|
||||
->subject($subject);
|
||||
});
|
||||
})->afterResponse();
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error dispatching file upload notification email: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadFiles(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:jpg,jpeg,png,pdf,doc,docx,xls,xlsx,csv|max:5120',
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'user_name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$user = auth('admins')->user();
|
||||
|
||||
// Tạo thư mục cho user nếu chưa tồn tại
|
||||
$userFolder = 'files/' . $request->user_name;
|
||||
if (!Storage::disk('public')->exists($userFolder)) {
|
||||
Storage::disk('public')->makeDirectory($userFolder);
|
||||
}
|
||||
|
||||
$path = $file->store($userFolder, 'public');
|
||||
|
||||
$fileRecord = Files::create([
|
||||
'name' => $request->name,
|
||||
'url' => $path,
|
||||
'type' => $this->getFileType($file->getClientOriginalName()),
|
||||
'description' => $request->description,
|
||||
'user_id' => Admin::where('name', $request->user_name)->first()->id
|
||||
]);
|
||||
|
||||
$currentUser = Admin::where('name', $request->user_name)->first();
|
||||
// Gửi email thông báo cho người upload
|
||||
$fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production')
|
||||
? env('APP_URL') . '/image' . Storage::url($path)
|
||||
: env('APP_URL') . Storage::url($path);
|
||||
$this->sendFileUploadNotification(
|
||||
$user,
|
||||
'Bạn đã tải lên file "' . $request->name . '" thành công',
|
||||
$fileUrl,
|
||||
"[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
|
||||
$request->description ?? 'No description'
|
||||
);
|
||||
|
||||
// Gửi email thông báo cho tất cả admin khác
|
||||
$otherAdmins = Admin::where('permission', 'like', '%admin%')->get();
|
||||
foreach ($otherAdmins as $admin) {
|
||||
$this->sendFileUploadNotification(
|
||||
$admin,
|
||||
'File "' . $request->name . '" đã được tải lên bởi ' . $user->name,
|
||||
$fileUrl,
|
||||
"[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
|
||||
$request->description ?? 'No description'
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'File uploaded successfully',
|
||||
'data' => [
|
||||
'id' => $fileRecord->id,
|
||||
'name' => $fileRecord->name,
|
||||
'url' => Storage::url($path),
|
||||
'type' => $fileRecord->type,
|
||||
'description' => $fileRecord->description
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getFiles()
|
||||
{
|
||||
try {
|
||||
$files = Files::with('user')->get()
|
||||
->map(function($file) {
|
||||
return [
|
||||
'id' => $file->id,
|
||||
'name' => $file->name,
|
||||
'url' => Storage::url($file->url),
|
||||
'type' => $file->type,
|
||||
'description' => $file->description,
|
||||
'created_at' => $file->created_at,
|
||||
'user_id' => $file->user_id,
|
||||
'user_name' => $file->user->name
|
||||
];
|
||||
});
|
||||
|
||||
// Gom nhóm files theo tên user
|
||||
$groupedFiles = $files->groupBy('user_name')
|
||||
->map(function($files) {
|
||||
return $files->map(function(array $file) {
|
||||
return (object)[
|
||||
'id' => $file['id'],
|
||||
'name' => $file['name'],
|
||||
'url' => $file['url'],
|
||||
'type' => $file['type'],
|
||||
'description' => $file['description'],
|
||||
'created_at' => $file['created_at'],
|
||||
'user_id' => $file['user_id']
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'data' => $groupedFiles
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteFile($id)
|
||||
{
|
||||
try {
|
||||
$file = Files::findOrFail($id);
|
||||
$user = auth('admins')->user();
|
||||
|
||||
if ($file->user_id !== $user->id) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Unauthorized'
|
||||
], 403);
|
||||
}
|
||||
|
||||
Storage::disk('public')->delete($file->url);
|
||||
$file->delete();
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'File deleted successfully'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function getFileType($filename)
|
||||
{
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
|
||||
$typeMap = [
|
||||
'pdf' => 'document',
|
||||
'doc' => 'document',
|
||||
'docx' => 'document',
|
||||
'jpg' => 'image',
|
||||
'jpeg' => 'image',
|
||||
'png' => 'image',
|
||||
'xls' => 'spreadsheet',
|
||||
'xlsx' => 'spreadsheet',
|
||||
'csv' => 'spreadsheet'
|
||||
];
|
||||
|
||||
return $typeMap[$extension] ?? 'other';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,6 +174,9 @@ Route::middleware('api')
|
|||
Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/update-profile', [ProfileController::class, 'updateProfile'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/delete-profile-file', [ProfileController::class, 'removeFile'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::get('/files', [ProfileController::class, 'getFiles'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/upload-files', [ProfileController::class, 'uploadFiles'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::delete('/files/{id}', [ProfileController::class, 'deleteFile'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
// '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>
|
||||
|
|
@ -119,3 +119,8 @@ export const deleteDocument = API_URL + 'v1/admin/document/delete'
|
|||
|
||||
// Download File
|
||||
export const downloadFile = API_URL + 'v1/admin/download-file'
|
||||
|
||||
// Files APIs
|
||||
export const getFiles = API_URL + 'v1/admin/profile/files'
|
||||
export const uploadFiles = API_URL + 'v1/admin/profile/upload-files'
|
||||
export const deleteFileById = API_URL + 'v1/admin/profile/files'
|
||||
|
|
|
|||
|
|
@ -1,498 +1,330 @@
|
|||
import {
|
||||
deleteFile,
|
||||
getAllFilesInProfiles,
|
||||
updateProfileFolder,
|
||||
} from '@/api/Admin'
|
||||
import { Xdelete } from '@/rtk/helpers/CRUD'
|
||||
import { getFiles, uploadFiles } from '@/api/Admin'
|
||||
import { get } from '@/rtk/helpers/apiService'
|
||||
import { getAccessToken } from '@/rtk/localStorage'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FileInput,
|
||||
Card,
|
||||
Collapse,
|
||||
Group,
|
||||
Modal,
|
||||
RenderTreeNodePayload,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
Tree,
|
||||
Title,
|
||||
} from '@mantine/core'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconDownload,
|
||||
IconFileTypeDocx,
|
||||
IconFileTypePdf,
|
||||
IconFolder,
|
||||
IconFolderOpen,
|
||||
IconFolderX,
|
||||
IconListCheck,
|
||||
IconPhoto,
|
||||
IconSearch,
|
||||
IconTrash,
|
||||
} from '@tabler/icons-react'
|
||||
import axios from 'axios'
|
||||
import { useEffect, useState } from 'react'
|
||||
import FileUploadForm from '../Profile/components/FileUploadForm'
|
||||
import classes from './AllProfiles.module.css'
|
||||
|
||||
interface FileIconProps {
|
||||
interface FileData {
|
||||
id: number
|
||||
name: string
|
||||
isFolder: boolean
|
||||
expanded: boolean
|
||||
url: string
|
||||
type: string
|
||||
description?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type TFileProfile = {
|
||||
label: string
|
||||
type: string
|
||||
value: string
|
||||
children?: TFileProfile[]
|
||||
interface GroupedFiles {
|
||||
[key: string]: FileData[]
|
||||
}
|
||||
|
||||
const AllProfiles = () => {
|
||||
const [treeData, setTreeData] = useState([])
|
||||
const [cv, setCv] = useState<File>()
|
||||
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 [groupedFiles, setGroupedFiles] = useState<GroupedFiles>({})
|
||||
const [currentUser, setCurrentUser] = useState<string>('')
|
||||
const [openedProfile, setOpenedProfile] = useState(false)
|
||||
function FileIcon({ name, isFolder, expanded }: FileIconProps) {
|
||||
if (name.endsWith('.pdf')) {
|
||||
return <IconFileTypePdf size={14} />
|
||||
}
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [expandedFolders, setExpandedFolders] = useState<{
|
||||
[key: string]: boolean
|
||||
}>({})
|
||||
const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
|
||||
|
||||
if (name.endsWith('.doc') || name.endsWith('.docx')) {
|
||||
return <IconFileTypeDocx size={14} />
|
||||
}
|
||||
|
||||
if (
|
||||
name.endsWith('.jpg') ||
|
||||
name.endsWith('.png') ||
|
||||
name.endsWith('.jpeg') ||
|
||||
name.endsWith('.webp')
|
||||
) {
|
||||
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} />
|
||||
)
|
||||
const toggleFolder = (userName: string) => {
|
||||
setExpandedFolders((prev) => ({
|
||||
...prev,
|
||||
[userName]: !prev[userName],
|
||||
}))
|
||||
}
|
||||
|
||||
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 getFileIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'document':
|
||||
return <IconFileTypeDocx size={16} />
|
||||
case 'image':
|
||||
return <IconPhoto size={16} />
|
||||
default:
|
||||
return <IconFileTypePdf size={16} />
|
||||
}
|
||||
}
|
||||
|
||||
const handleOtherFileChange = (
|
||||
index: number,
|
||||
field: string,
|
||||
value: File | string,
|
||||
const handleSubmit = async (
|
||||
e: React.FormEvent,
|
||||
fileName: string,
|
||||
description: string,
|
||||
currentUser: string
|
||||
) => {
|
||||
const updatedFiles: any = [...otherFiles]
|
||||
updatedFiles[index][field] = value
|
||||
setOtherFiles(updatedFiles)
|
||||
}
|
||||
|
||||
const addOtherFileInput = () => {
|
||||
setOtherFiles([...otherFiles, { file: null, type: '' }])
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
const formData = new FormData()
|
||||
if (selectedFile) {
|
||||
formData.append('file', selectedFile)
|
||||
formData.append('name', fileName)
|
||||
formData.append('description', description)
|
||||
formData.append('user_name', currentUser)
|
||||
|
||||
// Append each selected file to FormData
|
||||
for (let i = 0; i < otherFiles.length; i++) {
|
||||
if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
|
||||
formData.append(
|
||||
'files[]',
|
||||
handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
|
||||
)
|
||||
try {
|
||||
const token = await getAccessToken()
|
||||
const response = await axios.post(uploadFiles, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
setSelectedFile(null)
|
||||
await getAllFiles()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error)
|
||||
throw error
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (cv) {
|
||||
formData.append('files[]', cv)
|
||||
}
|
||||
|
||||
if (idCard) {
|
||||
formData.append('files[]', idCard)
|
||||
}
|
||||
|
||||
if (transcript) {
|
||||
formData.append('files[]', transcript)
|
||||
}
|
||||
|
||||
if (universityDiploma) {
|
||||
formData.append('files[]', universityDiploma)
|
||||
}
|
||||
|
||||
formData.append('name', currentName)
|
||||
|
||||
const token = await getAccessToken()
|
||||
const getAllFiles = async () => {
|
||||
try {
|
||||
const response = await axios.post(updateProfileFolder, formData, {
|
||||
const res = await get(getFiles)
|
||||
if (res.status === true) {
|
||||
setGroupedFiles(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const removeFile = async (id: number) => {
|
||||
try {
|
||||
const token = await getAccessToken();
|
||||
const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
getAllFile()
|
||||
getTree()
|
||||
setOtherFiles([])
|
||||
notifications.show({
|
||||
title: 'Thành công',
|
||||
message: 'Xóa file thành công',
|
||||
color: 'green',
|
||||
});
|
||||
await getAllFiles();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading files', error)
|
||||
console.log(error);
|
||||
notifications.show({
|
||||
title: 'Lỗi',
|
||||
message: 'Không thể xóa file',
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const getAllFile = async () => {
|
||||
try {
|
||||
const res = await get(getAllFilesInProfiles, {
|
||||
root_folder: '/storage/profiles/' + currentName,
|
||||
})
|
||||
if (res.status === true) {
|
||||
setData(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const removeFile = async (url: string) => {
|
||||
try {
|
||||
await Xdelete(deleteFile, { file_url: url }, getAllFile)
|
||||
getTree()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
const getTree = async () => {
|
||||
try {
|
||||
const res = await get(getAllFilesInProfiles, {
|
||||
root_folder: '/storage/profiles',
|
||||
})
|
||||
if (res.status === true) {
|
||||
setTreeData(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChangeFileName = (e: File, newName: string) => {
|
||||
const originalFile = e // Get the original file
|
||||
const extend = originalFile.name.split('.')[1]
|
||||
if (originalFile) {
|
||||
const newFileName = `${newName}.${extend}` // Create new file name
|
||||
const newFile = new File([originalFile], newFileName, {
|
||||
type: originalFile.type,
|
||||
}) // Create new file object
|
||||
|
||||
return newFile // Save the new file object for further processing
|
||||
}
|
||||
}
|
||||
|
||||
const checkFileExist = (nameField: string) => {
|
||||
const file = data.find((f) => f.label.includes(nameField))
|
||||
return file
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getTree()
|
||||
getAllFiles()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
getAllFile()
|
||||
}, [currentName])
|
||||
const filterFiles = (files: FileData[], searchTerm: string) => {
|
||||
return files.filter(
|
||||
(file) =>
|
||||
file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(file.description &&
|
||||
file.description.toLowerCase().includes(searchTerm.toLowerCase())),
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.title}>
|
||||
<h3>
|
||||
<Text>Admin/</Text>
|
||||
Profiles
|
||||
<Text size="sm">Admin/</Text>
|
||||
Files Management
|
||||
</h3>
|
||||
</div>
|
||||
<Box ml={'lg'}>
|
||||
<Tree
|
||||
classNames={classes}
|
||||
selectOnClick
|
||||
clearSelectionOnOutsideClick
|
||||
data={treeData}
|
||||
renderNode={(payload) => <Leaf {...payload} />}
|
||||
/>
|
||||
<Box ml={'md'}>
|
||||
<Stack gap="xs">
|
||||
{Object.entries(groupedFiles).map(([userName, files]) => (
|
||||
<Card key={userName} shadow="xs" radius="sm" withBorder p="xs">
|
||||
<Group
|
||||
justify="space-between"
|
||||
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
|
||||
size={'lg'}
|
||||
size="lg"
|
||||
opened={openedProfile}
|
||||
onClose={() => {
|
||||
setOpenedProfile(false)
|
||||
setCurrentUser('')
|
||||
setSelectedFile(null)
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack>
|
||||
<Box
|
||||
style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
|
||||
>
|
||||
<Text>CV</Text>
|
||||
<a href={checkFileExist('cv')?.value} target="_blank">
|
||||
<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>
|
||||
<FileUploadForm
|
||||
data={groupedFiles[currentUser] || []}
|
||||
handleSubmit={handleSubmit}
|
||||
handleFileChange={(file) => file && setSelectedFile(file)}
|
||||
removeFile={removeFile}
|
||||
isLoading={isLoading}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import {
|
||||
deleteFile,
|
||||
getAllFilesInProfiles,
|
||||
getFiles,
|
||||
getProfilesData,
|
||||
listUserTechnical,
|
||||
updateProfileFolder,
|
||||
updateProfilesData,
|
||||
updateUserTechnical,
|
||||
uploadFiles
|
||||
} from '@/api/Admin'
|
||||
import { changePassword } from '@/api/Auth'
|
||||
import DataTableAll from '@/components/DataTable/DataTable'
|
||||
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
|
||||
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
|
||||
import { logout } from '@/rtk/dispatches/auth'
|
||||
import { Xdelete } from '@/rtk/helpers/CRUD'
|
||||
import { get, post, postImage } from '@/rtk/helpers/apiService'
|
||||
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
||||
|
|
@ -22,16 +20,13 @@ import {
|
|||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
FileInput,
|
||||
Flex,
|
||||
Group,
|
||||
Loader,
|
||||
Modal,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Title
|
||||
} from '@mantine/core'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import {
|
||||
|
|
@ -46,15 +41,26 @@ import moment from 'moment'
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import classes from './Profile.module.css'
|
||||
import FileUploadForm from './components/FileUploadForm'
|
||||
|
||||
const isCompactMenu = false
|
||||
|
||||
type TFileProfile = {
|
||||
label: string
|
||||
type: string
|
||||
value: string
|
||||
children?: TFileProfile[]
|
||||
// type TFileProfile = {
|
||||
// label: string
|
||||
// type: string
|
||||
// value: string
|
||||
// children?: TFileProfile[]
|
||||
// }
|
||||
|
||||
interface FileData {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const Profile = () => {
|
||||
const user = useAppSelector((state) => state.authentication.user)
|
||||
const userData = getUser()
|
||||
|
|
@ -77,6 +83,11 @@ const Profile = () => {
|
|||
const navigate = useNavigate()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [data, setData] = useState<FileData[]>([])
|
||||
const [openedProfile, setOpenedProfile] = useState(false)
|
||||
|
||||
const updateAvatar = async (file: File) => {
|
||||
try {
|
||||
const res = await postImage(updateProfilesData, file, 'post')
|
||||
|
|
@ -137,102 +148,113 @@ const Profile = () => {
|
|||
return []
|
||||
}
|
||||
|
||||
const [cv, setCv] = useState<File>()
|
||||
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 [openedProfile, setOpenedProfile] = useState(false)
|
||||
const handleOtherFileChange = (
|
||||
index: number,
|
||||
field: string,
|
||||
value: File | string,
|
||||
// const [cv, setCv] = useState<File>()
|
||||
// const [idCard, setIdCard] = useState<File>()
|
||||
// const [transcript, setTranscript] = useState<File>()
|
||||
// const [universityDiploma, setUniversityDiploma] = useState<File>()
|
||||
// const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
|
||||
// const handleOtherFileChange = (
|
||||
// index: number,
|
||||
// field: string,
|
||||
// value: File | string,
|
||||
// ) => {
|
||||
// const updatedFiles: any = [...otherFiles]
|
||||
// updatedFiles[index][field] = value
|
||||
// setOtherFiles(updatedFiles)
|
||||
// }
|
||||
|
||||
// const addOtherFileInput = () => {
|
||||
// setOtherFiles([...otherFiles, { file: null, type: '' }])
|
||||
// }
|
||||
|
||||
const handleSubmit = async (
|
||||
e: React.FormEvent,
|
||||
fileName: string,
|
||||
description: string,
|
||||
) => {
|
||||
const updatedFiles: any = [...otherFiles]
|
||||
updatedFiles[index][field] = value
|
||||
setOtherFiles(updatedFiles)
|
||||
}
|
||||
|
||||
const addOtherFileInput = () => {
|
||||
setOtherFiles([...otherFiles, { file: null, type: '' }])
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault()
|
||||
setIsLoading(true)
|
||||
const formData = new FormData()
|
||||
|
||||
// Append each selected file to FormData
|
||||
for (let i = 0; i < otherFiles.length; i++) {
|
||||
if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
|
||||
formData.append(
|
||||
'files[]',
|
||||
handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
|
||||
)
|
||||
if (selectedFile) {
|
||||
formData.append('file', selectedFile)
|
||||
formData.append('name', fileName)
|
||||
formData.append('description', description)
|
||||
formData.append('user_name', user.user.name)
|
||||
formData.append('user_id', user.user.id.toString())
|
||||
|
||||
try {
|
||||
const token = await getAccessToken()
|
||||
const response = await axios.post(uploadFiles, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
await getAllFile()
|
||||
setSelectedFile(null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (cv) {
|
||||
formData.append('files[]', cv)
|
||||
}
|
||||
|
||||
if (idCard) {
|
||||
formData.append('files[]', idCard)
|
||||
}
|
||||
|
||||
if (transcript) {
|
||||
formData.append('files[]', transcript)
|
||||
}
|
||||
|
||||
if (universityDiploma) {
|
||||
formData.append('files[]', universityDiploma)
|
||||
}
|
||||
|
||||
const token = await getAccessToken()
|
||||
try {
|
||||
const response = await axios.post(updateProfileFolder, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if(response.status === 200){
|
||||
getAllFile()
|
||||
setOtherFiles([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading files', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getAllFile = async () => {
|
||||
try {
|
||||
const res = await get(getAllFilesInProfiles, {
|
||||
root_folder: '/storage/profiles/' + JSON.parse(getUser())?.user?.name,
|
||||
})
|
||||
const res = await get(getFiles)
|
||||
if (res.status === true) {
|
||||
setData(res.data)
|
||||
const userFiles = res.data[user.user.name] || [];
|
||||
setData(userFiles);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
notifications.show({
|
||||
title: 'Lỗi',
|
||||
message: 'Không thể tải danh sách file',
|
||||
color: 'red',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const removeFile = async (url: string) => {
|
||||
const removeFile = async (id: number) => {
|
||||
try {
|
||||
await Xdelete(deleteFile, {file_url: url}, getAllFile)
|
||||
const token = await getAccessToken();
|
||||
const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
notifications.show({
|
||||
title: 'Thành công',
|
||||
message: 'Xóa file thành công',
|
||||
color: 'green',
|
||||
});
|
||||
await getAllFile();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.log(error);
|
||||
notifications.show({
|
||||
title: 'Lỗi',
|
||||
message: 'Không thể xóa file',
|
||||
color: 'red',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const result = await getListProfilesData()
|
||||
setDataProfile(result ?? [])
|
||||
await getAllFile()
|
||||
}
|
||||
fetchData()
|
||||
getAllFile()
|
||||
}, [])
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
|
|
@ -306,23 +328,19 @@ const Profile = () => {
|
|||
dispatch(logout(navigate))
|
||||
}, [dispatch, navigate])
|
||||
|
||||
const handleChangeFileName = (e: File, newName: string) => {
|
||||
const originalFile = e // Get the original file
|
||||
const extend = originalFile.name.split('.')[1]
|
||||
if (originalFile) {
|
||||
const newFileName = `${newName}.${extend}` // Create new file name
|
||||
const newFile = new File([originalFile], newFileName, {
|
||||
type: originalFile.type,
|
||||
}) // Create new file object
|
||||
// const handleChangeFileName = (e: File, newName: string): File => {
|
||||
// const originalFile = e;
|
||||
// const extend = originalFile.name.split('.')[1];
|
||||
// const newFileName = `${newName}.${extend}`;
|
||||
// return new File([originalFile], newFileName, {
|
||||
// type: originalFile.type,
|
||||
// });
|
||||
// };
|
||||
|
||||
return newFile // Save the new file object for further processing
|
||||
}
|
||||
}
|
||||
|
||||
const checkFileExist = (nameField: string) => {
|
||||
const file = data.find((f) => f.label.includes(nameField))
|
||||
return file
|
||||
}
|
||||
// const checkFileExist = (nameField: string) => {
|
||||
// const file = data.find((f) => f.name.includes(nameField));
|
||||
// return file;
|
||||
// };
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -578,162 +596,14 @@ const Profile = () => {
|
|||
}}
|
||||
>
|
||||
<Box>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack>
|
||||
<Box
|
||||
style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
|
||||
>
|
||||
<Text>CV</Text>
|
||||
<a href={checkFileExist('cv')?.value} target="_blank">
|
||||
<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>
|
||||
<FileUploadForm
|
||||
data={data}
|
||||
handleSubmit={handleSubmit}
|
||||
handleFileChange={(file) => file && setSelectedFile(file)}
|
||||
removeFile={removeFile}
|
||||
isLoading={isLoading}
|
||||
currentUser={user.user.name}
|
||||
/>
|
||||
</Box>
|
||||
</Modal>
|
||||
</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
|
||||
Loading…
Reference in New Issue