Merge pull request 'BE, FE Document' (#113) from vi.document into dev
Reviewed-on: #113
This commit is contained in:
commit
d8cbb7e01b
|
|
@ -0,0 +1,167 @@
|
|||
<?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([
|
||||
'title' => 'required|string|max:255',
|
||||
'type' => 'required|in:file,link',
|
||||
'uri' => 'nullable|array',
|
||||
'uri.*' => 'nullable|url',
|
||||
'files' => 'nullable|array',
|
||||
'files.*' => 'file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
|
||||
'is_active' => 'required|boolean',
|
||||
]);
|
||||
|
||||
if($request->type == "file") {
|
||||
$uploadedFiles = [];
|
||||
if ($request->hasFile('files')) {
|
||||
foreach ($request->file('files') as $file) {
|
||||
$path = $file->store('uploads', options: 'public');
|
||||
$uploadedFiles[] = "storage/{$path}";
|
||||
}
|
||||
}
|
||||
|
||||
$document = Document::create([
|
||||
'title' => $request->title,
|
||||
'type' => $request->type,
|
||||
'uri' => implode(',', $uploadedFiles),
|
||||
'is_active' => $request->is_active,
|
||||
]);
|
||||
|
||||
return AbstractController::ResultSuccess($document, "Document created successfully!");
|
||||
}
|
||||
|
||||
$document = Document::create([
|
||||
'title' => $request->title,
|
||||
'type' => $request->type,
|
||||
'uri' => implode(',', $request->uri),
|
||||
'is_active' => $request->is_active,
|
||||
]);
|
||||
|
||||
return AbstractController::ResultSuccess($document, "Document 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|array',
|
||||
'uri.*' => 'nullable|url',
|
||||
'files' => 'nullable|array',
|
||||
'files.*' => 'file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
|
||||
'existing_files' => 'nullable|array',
|
||||
'is_active' => 'required|boolean',
|
||||
]);
|
||||
|
||||
$document = Document::find($request->input('id'));
|
||||
if (!$document) {
|
||||
return AbstractController::ResultError("Document not found.");
|
||||
}
|
||||
|
||||
if ($request->type === "file") {
|
||||
$existingFiles = explode(',', $document->uri);
|
||||
$selectedExistingFiles = $request->existing_files ?? [];
|
||||
|
||||
$filesToDelete = array_diff($existingFiles, $selectedExistingFiles);
|
||||
foreach ($filesToDelete as $file) {
|
||||
$filePath = str_replace('storage/', 'public/', $file);
|
||||
if (Storage::exists($filePath)) {
|
||||
Storage::delete($filePath);
|
||||
}
|
||||
}
|
||||
|
||||
$uploadedFiles = $selectedExistingFiles;
|
||||
if ($request->hasFile('files')) {
|
||||
foreach ($request->file('files') as $file) {
|
||||
$path = $file->store('uploads', 'public');
|
||||
$uploadedFiles[] = "storage/{$path}";
|
||||
}
|
||||
}
|
||||
|
||||
$document->update([
|
||||
'title' => $request->title,
|
||||
'type' => $request->type,
|
||||
'uri' => implode(',', $uploadedFiles),
|
||||
'is_active' => $request->is_active,
|
||||
]);
|
||||
|
||||
return AbstractController::ResultSuccess($document, "Document updated successfully!");
|
||||
}
|
||||
|
||||
$document->update([
|
||||
'title' => $request->title,
|
||||
'type' => $request->type,
|
||||
'uri' => implode(',', $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") {
|
||||
$existingFiles = explode(',', $document->uri);
|
||||
foreach ($existingFiles as $file) {
|
||||
$filePath = str_replace('storage/', 'public/', $file);
|
||||
if (Storage::exists($filePath)) {
|
||||
Storage::delete($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$document->delete();
|
||||
return AbstractController::ResultSuccess("Document deleted successfully!");
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Admin\app\Http\Controllers\AdminController;
|
||||
use Modules\Admin\app\Http\Controllers\BannerController;
|
||||
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\CustomThemeController;
|
||||
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\LeaveManagementController;
|
||||
use Modules\Admin\app\Http\Controllers\SettingController;
|
||||
|
|
@ -214,6 +216,32 @@ Route::middleware('api')
|
|||
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::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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ class JiraService
|
|||
$groupedIssues = [];
|
||||
$users_data = [];
|
||||
$user_warning = [];
|
||||
$ignore_projects = ['PJ_tracking'];
|
||||
foreach ($users as $user) {
|
||||
$user = (array) $user[0];
|
||||
$users_data[$user['displayName']]['user'] = $user;
|
||||
|
|
@ -252,32 +253,42 @@ class JiraService
|
|||
|
||||
$issues = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
// Lọc các issue không thuộc các project bị ignore
|
||||
$filtered_issues = array_filter($issues['issues'], function ($issue) use ($ignore_projects) {
|
||||
return !in_array($issue['fields']['project']['name'], $ignore_projects);
|
||||
});
|
||||
|
||||
$issues['issues'] = $filtered_issues;
|
||||
|
||||
if (count($issues['issues']) == 0) {
|
||||
$user_warning[] = $user;
|
||||
}
|
||||
|
||||
foreach ($issues['issues'] as $issue) {
|
||||
$projectName = $issue['fields']['project']['name'];
|
||||
$username = $issue['fields']['assignee']['displayName'];
|
||||
$issue['fields']['assignee']['emailAddress'] = $user['emailAddress'];
|
||||
if (!isset($groupedIssues[$projectName])) {
|
||||
$groupedIssues[$projectName] = [];
|
||||
$groupedIssues[$projectName]['project'] = $issue['fields']['project'];
|
||||
if(!in_array($projectName, $ignore_projects)) {
|
||||
$username = $issue['fields']['assignee']['displayName'];
|
||||
$issue['fields']['assignee']['emailAddress'] = $user['emailAddress'];
|
||||
if (!isset($groupedIssues[$projectName])) {
|
||||
$groupedIssues[$projectName] = [];
|
||||
$groupedIssues[$projectName]['project'] = $issue['fields']['project'];
|
||||
}
|
||||
|
||||
if (!isset($groupedIssues[$projectName]['users'][$username])) {
|
||||
$groupedIssues[$projectName]['users'][$username] = [];
|
||||
$groupedIssues[$projectName]['users'][$username]['user'] = $issue['fields']['assignee'];
|
||||
$groupedIssues[$projectName]['users'][$username]['p_total_spent'] = 0;
|
||||
$groupedIssues[$projectName]['users'][$username]['p_total_est'] = 0;
|
||||
}
|
||||
|
||||
$groupedIssues[$projectName]['users'][$username]['issues'][] = $issue;
|
||||
$groupedIssues[$projectName]['users'][$username]['p_total_spent'] = $groupedIssues[$projectName]['users'][$username]['p_total_spent'] + $issue['fields']['timespent'];
|
||||
$groupedIssues[$projectName]['users'][$username]['p_total_est'] = $groupedIssues[$projectName]['users'][$username]['p_total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
if (!isset($groupedIssues[$projectName]['users'][$username])) {
|
||||
$groupedIssues[$projectName]['users'][$username] = [];
|
||||
$groupedIssues[$projectName]['users'][$username]['user'] = $issue['fields']['assignee'];
|
||||
$groupedIssues[$projectName]['users'][$username]['p_total_spent'] = 0;
|
||||
$groupedIssues[$projectName]['users'][$username]['p_total_est'] = 0;
|
||||
}
|
||||
|
||||
$groupedIssues[$projectName]['users'][$username]['issues'][] = $issue;
|
||||
$groupedIssues[$projectName]['users'][$username]['p_total_spent'] = $groupedIssues[$projectName]['users'][$username]['p_total_spent'] + $issue['fields']['timespent'];
|
||||
$groupedIssues[$projectName]['users'][$username]['p_total_est'] = $groupedIssues[$projectName]['users'][$username]['p_total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
|
||||
|
||||
$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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -34,6 +34,7 @@
|
|||
"react": "^18.2.0",
|
||||
"react-doc-viewer": "^0.1.14",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-file-viewer": "^1.2.1",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.19.0",
|
||||
"reactstrap": "^9.2.2",
|
||||
|
|
|
|||
|
|
@ -110,3 +110,12 @@ export const deleteFile = API_URL + 'v1/admin/profile/delete-profile-file'
|
|||
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
|
||||
export const createTechnical = API_URL + 'v1/admin/technical/create'
|
||||
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'
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
IconCalendarClock,
|
||||
IconChartDots2,
|
||||
IconDevices,
|
||||
IconFileInvoice,
|
||||
IconFolders,
|
||||
IconLayoutSidebarLeftExpand,
|
||||
IconLayoutSidebarRightExpand,
|
||||
|
|
@ -41,7 +42,7 @@ import {
|
|||
IconSun,
|
||||
IconTicket,
|
||||
IconUsersGroup,
|
||||
IconZoomExclamation
|
||||
IconZoomExclamation,
|
||||
} from '@tabler/icons-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
|
@ -71,6 +72,13 @@ const data = [
|
|||
permissions: 'admin,hr,staff,tester',
|
||||
group: 'staff',
|
||||
},
|
||||
{
|
||||
link: '/documents',
|
||||
label: 'Documents',
|
||||
icon: IconFileInvoice,
|
||||
permissions: 'admin,hr,staff,tester,accountant',
|
||||
group: 'staff',
|
||||
},
|
||||
{
|
||||
link: '/leave-management',
|
||||
label: 'Leave Management',
|
||||
|
|
@ -239,7 +247,11 @@ const Navbar = ({
|
|||
// })
|
||||
|
||||
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: 'other', label: 'Other', permissions: 'admin,hr' },
|
||||
{ name: 'test', label: 'Test', permissions: 'admin,tester' },
|
||||
|
|
|
|||
|
|
@ -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,311 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { get } from '@/rtk/helpers/apiService'
|
||||
import { deleteDocument, listDocument } from '@/api/Admin'
|
||||
import { Xdelete } from '@/rtk/helpers/CRUD'
|
||||
|
||||
import {
|
||||
Anchor,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
Group,
|
||||
Loader,
|
||||
Text,
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { IconEdit, IconX } from '@tabler/icons-react'
|
||||
import DataTableAll from '@/components/DataTable/DataTable'
|
||||
import ModalAddEditDocument from './ModalAddEditDocument'
|
||||
import ModalFileDocument from './ModalFileDocument'
|
||||
|
||||
import classes from './Document.module.css'
|
||||
|
||||
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 [loader, setLoader] = useState<boolean>(false)
|
||||
const [action, setAction] = useState<string>('')
|
||||
const [rows, setRows] = useState<RequestPagination>({
|
||||
data: [],
|
||||
status: true,
|
||||
})
|
||||
const [selectDataRow, setSelectDataRow] = useState<any>({})
|
||||
|
||||
const [
|
||||
openedModalAddEdit,
|
||||
{ open: openModalAddEdit, close: closeModalAddEdit },
|
||||
] = 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 columns = [
|
||||
{
|
||||
name: 'id',
|
||||
size: '5%',
|
||||
header: 'ID',
|
||||
render: (row: TDocument) => {
|
||||
return <Box>{row?.id ? row.id : ''}</Box>
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
size: '30%',
|
||||
header: 'Title',
|
||||
render: (row: TDocument) => {
|
||||
return <Text ta="start">{row?.title}</Text>
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'uri',
|
||||
size: '60%',
|
||||
header: 'URI',
|
||||
render: (row: TDocument) => {
|
||||
return (
|
||||
<Box>
|
||||
{row.type === 'file' ? (
|
||||
<Group gap={5}>
|
||||
{row?.uri &&
|
||||
row?.uri.split(',')?.map((uriItem) => (
|
||||
<Badge
|
||||
style={{ cursor: 'pointer' }}
|
||||
tt="initial"
|
||||
onClick={() => {
|
||||
setSelectDataRow({ ...row, uri: uriItem })
|
||||
openModalFile()
|
||||
}}
|
||||
color="orange"
|
||||
>
|
||||
{uriItem.replace('storage/uploads/', '')}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
) : (
|
||||
<Group gap={5}>
|
||||
{row?.uri &&
|
||||
row?.uri.split(',')?.map((uriItem) => (
|
||||
<Anchor
|
||||
ta="start"
|
||||
href={uriItem}
|
||||
target="_blank"
|
||||
title={uriItem}
|
||||
>
|
||||
<Badge style={{ cursor: 'pointer' }} tt="initial">
|
||||
{uriItem}
|
||||
</Badge>
|
||||
</Anchor>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '#',
|
||||
size: '5%',
|
||||
header: 'Action',
|
||||
render: (row: TDocument) => {
|
||||
return (
|
||||
<Box className={classes.optionIcon}>
|
||||
<IconEdit
|
||||
className={classes.editIcon}
|
||||
onClick={() => {
|
||||
setAction('edit')
|
||||
setSelectDataRow(row)
|
||||
openModalAddEdit()
|
||||
}}
|
||||
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>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setAction('add')
|
||||
openModalAddEdit()
|
||||
}}
|
||||
disabled={loader}
|
||||
>
|
||||
+ Add
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Box mt={'md'}>
|
||||
<Group wrap="wrap" mb={'md'} px={16}>
|
||||
<Text fw={500}>Note: </Text>
|
||||
|
||||
<Badge style={{ cursor: 'pointer' }} tt="initial">
|
||||
Links
|
||||
</Badge>
|
||||
|
||||
<Badge style={{ cursor: 'pointer' }} tt="initial" color="orange">
|
||||
Files
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
|
||||
{openedModalAddEdit && (
|
||||
<ModalAddEditDocument
|
||||
opened={openedModalAddEdit}
|
||||
close={closeModalAddEdit}
|
||||
action={action}
|
||||
setAction={setAction}
|
||||
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,299 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from '@mantine/form'
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
TextInput,
|
||||
Text,
|
||||
Box,
|
||||
Switch,
|
||||
Checkbox,
|
||||
FileInput,
|
||||
TagsInput,
|
||||
Group,
|
||||
} from '@mantine/core'
|
||||
|
||||
import { create, update } from '@/rtk/helpers/CRUD'
|
||||
import { createDocument, updateDocument } from '@/api/Admin'
|
||||
import { getHeaderInfo } from '@/rtk/helpers/tokenCreator'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
|
||||
type MProps = {
|
||||
opened: boolean
|
||||
close: () => void
|
||||
setAction: (arg0: any) => void
|
||||
selectDataRow: any
|
||||
setSelectDataRow: any
|
||||
action: string
|
||||
getAllData: () => void
|
||||
}
|
||||
|
||||
const ModalAddEditDocument = ({
|
||||
opened,
|
||||
close,
|
||||
setAction,
|
||||
selectDataRow,
|
||||
setSelectDataRow,
|
||||
action,
|
||||
getAllData,
|
||||
}: MProps) => {
|
||||
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
title: '',
|
||||
type: true,
|
||||
files: [] as File[],
|
||||
uri: [],
|
||||
is_active: true,
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
form.setValues({
|
||||
title: selectDataRow?.title ?? '',
|
||||
type: selectDataRow?.type ? selectDataRow?.type === 'file' : true,
|
||||
files: [],
|
||||
uri: selectDataRow?.uri?.split(',') ?? [],
|
||||
is_active: selectDataRow?.is_active ?? true,
|
||||
})
|
||||
}, [selectDataRow])
|
||||
|
||||
const handleCreate = async (data: any) => {
|
||||
try {
|
||||
setLoadingSubmit(true)
|
||||
|
||||
let formdata = {}
|
||||
const header = await getHeaderInfo()
|
||||
|
||||
if (data.type) {
|
||||
if (data.files.length < 1) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Upload at least 1 file',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
header.headers['Content-Type'] = 'multipart/form-data'
|
||||
const tmpFormData = new FormData()
|
||||
|
||||
tmpFormData.append('title', data.title)
|
||||
tmpFormData.append('type', data.type ? 'file' : 'link')
|
||||
tmpFormData.append('is_active', data.is_active ? '1' : '0')
|
||||
for (let i = 0; i < data.files.length; i++) {
|
||||
tmpFormData.append('files[]', data.files[i])
|
||||
}
|
||||
|
||||
formdata = tmpFormData
|
||||
} else {
|
||||
const { files, ...rest } = data
|
||||
formdata = {
|
||||
...rest,
|
||||
type: rest.type ? 'file' : 'link',
|
||||
}
|
||||
}
|
||||
|
||||
const res = await create(createDocument, formdata, getAllData, header)
|
||||
if (res === true) {
|
||||
setAction('')
|
||||
close()
|
||||
form.reset()
|
||||
setSelectDataRow({})
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(error)
|
||||
} finally {
|
||||
setLoadingSubmit(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = async (data: any) => {
|
||||
try {
|
||||
setLoadingSubmit(true)
|
||||
|
||||
let formdata = {}
|
||||
const header = await getHeaderInfo()
|
||||
|
||||
if (data.type) {
|
||||
if (data.files.length < 1 && data.uri.length < 1) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: 'Upload at least 1 file',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
header.headers['Content-Type'] = 'multipart/form-data'
|
||||
const tmpFormData = new FormData()
|
||||
|
||||
tmpFormData.append('title', data.title)
|
||||
tmpFormData.append('type', data.type ? 'file' : 'link')
|
||||
tmpFormData.append('is_active', data.is_active ? '1' : '0')
|
||||
for (let i = 0; i < data.files.length; i++) {
|
||||
tmpFormData.append('files[]', data.files[i])
|
||||
}
|
||||
|
||||
data.uri.forEach((fileUri: string) => {
|
||||
tmpFormData.append('existing_files[]', fileUri)
|
||||
})
|
||||
|
||||
formdata = tmpFormData
|
||||
} else {
|
||||
const { files, ...rest } = data
|
||||
formdata = {
|
||||
...rest,
|
||||
type: rest.type ? 'file' : 'link',
|
||||
}
|
||||
}
|
||||
|
||||
const res = await update(
|
||||
updateDocument + `?id=${selectDataRow?.id}`,
|
||||
formdata,
|
||||
getAllData,
|
||||
header,
|
||||
)
|
||||
if (res === true) {
|
||||
setAction('')
|
||||
close()
|
||||
form.reset()
|
||||
setSelectDataRow({})
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
setLoadingSubmit(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => {
|
||||
setAction('')
|
||||
close()
|
||||
form.reset()
|
||||
setSelectDataRow({})
|
||||
}}
|
||||
size="lg"
|
||||
title={
|
||||
<Text fw={700} fz={'lg'}>
|
||||
{action === 'add' ? 'Add Document' : 'Update Document'}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
if (action === 'add') {
|
||||
handleCreate(values)
|
||||
} else {
|
||||
handleUpdate(values)
|
||||
}
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
label="Title"
|
||||
maxLength={255}
|
||||
key={'title'}
|
||||
mb={'md'}
|
||||
{...form.getInputProps('title')}
|
||||
disabled={loadingSubmit}
|
||||
required
|
||||
/>
|
||||
|
||||
{selectDataRow?.id ? (
|
||||
''
|
||||
) : (
|
||||
<Switch
|
||||
style={{ width: 'fit-content' }}
|
||||
label={form.values.type ? 'Upload files' : 'Enter links'}
|
||||
checked={form.values.type}
|
||||
onChange={(event) =>
|
||||
form.setFieldValue('type', event.currentTarget.checked)
|
||||
}
|
||||
mb={'md'}
|
||||
disabled={loadingSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box mb={'md'}>
|
||||
{form.values.type ? (
|
||||
<Box>
|
||||
<FileInput
|
||||
accept=".doc,.docx,.xls,.xlsx,.pdf"
|
||||
label="Upload Doc, Excel, PDF files"
|
||||
multiple
|
||||
mb="md"
|
||||
value={form.values.files}
|
||||
onChange={(files) => {
|
||||
form.setFieldValue('files', files || [])
|
||||
}}
|
||||
disabled={loadingSubmit}
|
||||
required
|
||||
/>
|
||||
|
||||
{selectDataRow?.uri && form.values.uri.length > 0 && (
|
||||
<Box>
|
||||
<Text fw={500}>Existing Files:</Text>
|
||||
{form.values.uri.map((fileUri: string, index) => (
|
||||
<Group key={index} justify="space-between" mb="sm">
|
||||
<Text size="sm">
|
||||
{fileUri.replace('storage/uploads/', '')}
|
||||
</Text>
|
||||
<Button
|
||||
color="red"
|
||||
size="xs"
|
||||
ml="md"
|
||||
onClick={() =>
|
||||
form.setFieldValue(
|
||||
'uri',
|
||||
form.values.uri.filter((_, i) => i !== index),
|
||||
)
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Group>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
<TagsInput
|
||||
label="Enter uri"
|
||||
key={'uri'}
|
||||
mb={'md'}
|
||||
{...form.getInputProps('uri')}
|
||||
disabled={loadingSubmit}
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</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}>
|
||||
{action === 'add' ? 'Create' : 'Update'}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalAddEditDocument
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
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}${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
|
||||
|
|
@ -5,6 +5,7 @@ import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
|
|||
import AllProfiles from '@/pages/AllProfiles/AllProfiles'
|
||||
import Allocation from '@/pages/Allocation/Allocation'
|
||||
import PageLogin from '@/pages/Auth/Login/Login'
|
||||
import Document from '@/pages/Document/Document'
|
||||
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
|
||||
import PageNotFound from '@/pages/NotFound/NotFound'
|
||||
import OrganizationSettings from '@/pages/OrganizationSettings/OrganizationSettings'
|
||||
|
|
@ -81,6 +82,20 @@ const mainRoutes = [
|
|||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/documents',
|
||||
element: (
|
||||
<ProtectedRoute mode="home" permission="staff,accountant">
|
||||
<BasePage
|
||||
main={
|
||||
<>
|
||||
<Document />
|
||||
</>
|
||||
}
|
||||
></BasePage>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/timekeeping',
|
||||
element: (
|
||||
|
|
|
|||
Loading…
Reference in New Issue