Compare commits

..

No commits in common. "9a9a94e5d49d6586c32b7648b8d2b6a801543b0f" and "04ee5e4081e230edb4b3adf02cfdd94b6079bc7b" have entirely different histories.

20 changed files with 294 additions and 5796 deletions

View File

@ -1,161 +0,0 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Models\Document;
class DocumentController extends Controller
{
use HasOrderByRequest;
use HasFilterRequest;
use HasSearchRequest;
public function all(Request $request)
{
$documents = new Document;
// Order by
$this->orderByRequest($documents, $request);
$documents->orderBy('title', 'asc');
// Filter
$this->filterRequest(
builder: $documents,
request: $request,
filterKeys: [
'title' => self::F_TEXT,
]
);
$this->searchRequest(
builder: $documents,
value: $request->get('search'),
fields: [
'title',
]
);
$responseData = $documents->get();
return AbstractController::ResultSuccess($responseData);
}
public function create(Request $request)
{
$request->validate([
'type' => 'required|in:file,link',
'files' => 'nullable|array',
'files.*.title' => 'required|string|max:255',
'files.*.file' => 'required|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
'links' => 'nullable|array',
'links.*.title' => 'required|string|max:255',
'links.*.uri' => 'required|string|url',
'is_active' => 'required|boolean',
]);
$documents = [];
if ($request->type === "file") {
foreach ($request->file('files') as $index => $file) {
$path = $file['file']->store('uploads', 'public');
$documents[] = [
'title' => $request->input("files.$index.title"),
'type' => 'file',
'uri' => "storage/{$path}",
'is_active' => $request->is_active,
'created_at' => now(),
'updated_at' => now(),
];
}
} else {
foreach ($request->links as $link) {
$documents[] = [
'title' => $link['title'],
'type' => 'link',
'uri' => $link['uri'],
'is_active' => $request->is_active,
'created_at' => now(),
'updated_at' => now(),
];
}
}
if (!empty($documents)) {
Document::insert($documents);
}
return AbstractController::ResultSuccess($documents, "Documents created successfully!");
}
public function update(Request $request)
{
$request->validate([
'id' => 'required|exists:documents,id',
'title' => 'required|string|max:255',
'type' => 'required|in:file,link',
'uri' => 'nullable|url',
'file' => 'nullable|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
'existing_file' => 'nullable|string',
'is_active' => 'required|boolean',
]);
$document = Document::find($request->input('id'));
if (!$document) {
return AbstractController::ResultError("Document not found.");
}
if ($request->type === "file") {
$uri = $request->existing_file;
if ($request->hasFile('file')) {
$filePath = str_replace('storage/', 'public/', $request->existing_file);
if (Storage::exists($filePath)) {
Storage::delete($filePath);
}
$path = $request->file('file')->store('uploads', 'public');
$uri = "storage/{$path}";
}
$document->update([
'title' => $request->title,
'type' => $request->type,
'uri' => $uri,
'is_active' => $request->is_active,
]);
return AbstractController::ResultSuccess($document, "Document updated successfully!");
}
$document->update([
'title' => $request->title,
'type' => $request->type,
'uri' => $request->uri,
'is_active' => $request->is_active,
]);
return AbstractController::ResultSuccess($document, "Document updated successfully!");
}
public function delete(Request $request)
{
$id = $request->input('id');
$document = Document::find($id);
if (!$document) {
return AbstractController::ResultError("Document not found");
}
if ($document->type === "file") {
$filePath = str_replace('storage/', 'public/', $document->uri);
if (Storage::exists($filePath)) {
Storage::delete($filePath);
}
}
$document->delete();
return AbstractController::ResultSuccess("Document deleted successfully!");
}
}

View File

@ -178,27 +178,6 @@ class JiraController extends Controller
}
}
public function getWeeklyReport()
{
try {
$startOfWeek = Carbon::now()->startOfWeek()->format('Y-m-d'); // Mặc định là Thứ Hai
$endOfWeek = Carbon::now()->endOfWeek()->format('Y-m-d'); // Mặc định là Chủ Nhật
// dd($startOfWeek);
$results = [];
$workLogs = $this->jiraService->getAllUserWorkLogs($startOfWeek, $endOfWeek);
foreach($workLogs as $data){
$results[$data['username']] = $data['information']['issues'];
}
return response()->json([
$results
], 200);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
public function sendReport()
{
$dateFormatted = Carbon::yesterday()->setTimezone(env('TIME_ZONE'))->format('Y-m-d');
@ -207,7 +186,7 @@ class JiraController extends Controller
$tasksByUser = $this->formatWorkLogsByUser($workLogs);
// Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
Mail::to(['joseph@apactech.io', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
Mail::to(['luanlt632000@gmail.com', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
// return "Email sent successfully!";
return response()->json([

View File

@ -1,13 +0,0 @@
<?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'];
}

View File

@ -2,7 +2,6 @@
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;
@ -10,7 +9,6 @@ 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;
@ -114,8 +112,6 @@ Route::middleware('api')
Route::get('/get-all-sprint-by-id-board', [JiraController::class, 'getAllSprintByIdBoard'])->middleware('check.permission:admin.tester');
Route::get('/get-all-issue-by-id-sprint', [JiraController::class, 'getAllIssueByIdSprint']);
Route::get('/export-weekly-report', [JiraController::class, 'getWeeklyReport']);
Route::get('/all-issue-by-project', [JiraController::class, 'fetchIssuesByProject']);
Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs'])->middleware('check.permission:admin.staff');
Route::get('/allocation', [JiraController::class, 'getAllUserDoing'])->middleware('check.permission:admin.staff');
@ -218,32 +214,6 @@ 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);
});
});
});

View File

@ -23,9 +23,7 @@ class JiraService
'Authorization' => $this->authHeader,
'Accept' => 'application/json',
'Content-Type' => 'application/json'
],
'timeout' => 60, // Tăng thời gian timeout lên 60 giây
'connect_timeout' => 30 // Tăng thời gian chờ kết nối lên 30 giây
]
]);
}
@ -232,7 +230,6 @@ 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;
@ -255,41 +252,32 @@ 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'];
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);
$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);
}
}
@ -326,18 +314,4 @@ class JiraService
$response = $this->client->get('/rest/agile/1.0/sprint/' . $id . '/issue');
return json_decode($response->getBody()->getContents(), true);
}
public function getWeeklyReport()
{
$body = [
'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'assignee', 'project', 'worklog'],
'jql' => 'worklogDate >= startOfWeek() AND worklogDate < startOfWeek(1) order by created DESC',
'maxResults' => 1000
];
$response = $this->client->post('/rest/api/3/search', [
'body' => json_encode($body)
]);
return json_decode($response->getBody()->getContents(), true);
}
}

View File

@ -1,32 +0,0 @@
<?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');
}
};

View File

@ -1,3 +1 @@
VITE_BACKEND_URL=http://localhost:8000/
VITE_URL_DRAWIO="https://viewer.diagrams.net/?tags=%7B%7D&lightbox=1&highlight=0000ff&edit=_blank&layers=1&nav=1&title=Test%20Draw.drawio&dark=auto#Uhttps%3A%2F%2Fdrive.google.com%2Fuc%3Fid%3D1LmB9wCac9DonQPFU-53g1nhI9SfvWuWK%26export%3Ddownload"
VITE_BACKEND_URL=http://localhost:8000/

File diff suppressed because it is too large Load Diff

View File

@ -34,7 +34,6 @@
"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",

View File

@ -110,12 +110,3 @@ 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'

View File

@ -250,7 +250,12 @@ export const DataTableAll = ({
if (query !== '') {
setTData(
data.filter((obj) =>
Object.values(obj)?.find((c: any) => c.toString().normalize('NFC').toLowerCase().includes(query.normalize('NFC').toLowerCase())))
Object.values(obj).some(
(value: any) =>
value !== null &&
value.toString().toLowerCase().includes(query.toLowerCase()),
),
),
)
} else {
if (pagination) {

View File

@ -27,7 +27,6 @@ import {
IconCalendarClock,
IconChartDots2,
IconDevices,
IconFileInvoice,
IconFolders,
IconLayoutSidebarLeftExpand,
IconLayoutSidebarRightExpand,
@ -42,7 +41,7 @@ import {
IconSun,
IconTicket,
IconUsersGroup,
IconZoomExclamation,
IconZoomExclamation
} from '@tabler/icons-react'
import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
@ -72,13 +71,6 @@ 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',
@ -247,11 +239,7 @@ 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' },

View File

@ -3,8 +3,3 @@ declare module '*.css'
declare module '@codemirror/lang-javascript'
declare const __VITE_BACKEND_URL__: string
declare module 'react-file-viewer' {
const FileViewer: any
export default FileViewer
}

View File

@ -10,7 +10,6 @@ import {
Popover,
Text,
Tooltip,
Switch,
} from '@mantine/core'
import { IconInnerShadowTopRightFilled } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
@ -81,31 +80,6 @@ const Allocation = () => {
const [opened, setOpened] = useState(false)
const [issDetail, setIssDetail] = useState('')
const [data, setData] = useState<any>({})
const [showDrawio, setShowDrawio] = useState(true)
useEffect(() => {
// Check if iframe already exists
const existingIframe = document.querySelector('#drawio iframe')
if (existingIframe) return
// Add iframe only if it doesn't exist
const iframe = document.createElement('iframe')
iframe.src = import.meta.env.VITE_URL_DRAWIO ?? ''
iframe.style.width = '100%'
iframe.style.height = '500px'
const drawioDiv = document.getElementById('drawio')
if (drawioDiv) {
drawioDiv.appendChild(iframe)
}
return () => {
const iframe = document.querySelector('#drawio iframe')
if (iframe) {
iframe.remove()
}
}
}, [])
const getAll = async () => {
try {
const res = await get(getAllUserDoing)
@ -143,14 +117,6 @@ const Allocation = () => {
}, [])
return (
<div>
<Box style={{ display: 'flex', alignItems: 'center', margin: '10px' }}>
<Switch
label="Show Diagram"
checked={showDrawio}
onChange={(event) => setShowDrawio(event.currentTarget.checked)}
/>
</Box>
<div id="drawio" style={{ display: showDrawio ? 'block' : 'none' }}></div>
<div>
<Box
style={{
@ -172,12 +138,12 @@ const Allocation = () => {
}}
>
<Box>
{/* <Text fw={600} fz={'md'}>
<Text fw={600} fz={'md'}>
Admin/
</Text>
<Text fw={700} fz={'lg'}>
Personnel Allocation
</Text> */}
</Text>
<Text fw={600} fz={'sm'} c={'gray'} fs={'italic'} ml={'md'}>
"P:" is the timspent/timeestimate number within the project itself
</Text>
@ -258,38 +224,27 @@ const Allocation = () => {
alignItems: 'center',
}}
>
<Box w="10%">
<IconInnerShadowTopRightFilled
style={{ color: 'orange' }}
height={20}
width={20}
display={
userData.issues?.filter(
(iss: Issue) =>
iss.fields.status.name ===
'In Progress' &&
Date.now() -
new Date(
iss.changelog?.histories[0]?.created,
).getTime() >
172800000,
).length > 0
? 'block'
: 'none'
}
/>
<Box w='10%'>
<IconInnerShadowTopRightFilled
style={{ color: 'orange' }}
height={20}
width={20}
display={userData.issues?.filter(
(iss: Issue) =>
iss.fields.status.name === 'In Progress' &&
((Date.now() - (new Date(iss.changelog?.histories[0]?.created)).getTime()) > 172800000)
).length > 0 ? 'block' :'none'}
/>
</Box>
<Box display={'flex'}>
<Avatar
size={'sm'}
ml={'5px'}
src={
userData.user.avatarUrls['48x48']
}
/>
<Text ml={'md'} fw={600}>
{user}
</Text>
<Avatar
size={'sm'}
ml={'5px'}
src={userData.user.avatarUrls['48x48']}
/>
<Text ml={'md'} fw={600}>
{user}
</Text>
</Box>
</Box>
<Box
@ -305,12 +260,9 @@ const Allocation = () => {
}}
ml={'md'}
p="0 20px"
>
<Text
ml={'md'}
fw={700}
fz={'sm'}
>{`P: `}</Text>
<Text ml={'md'} fw={700} fz={'sm'}>{`P: `}</Text>
<Text fw={700} c="green" fz={'sm'}>{`${
userData.p_total_spent / 60 / 60
}h/`}</Text>

View File

@ -1,37 +0,0 @@
.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);
}

View File

@ -1,338 +0,0 @@
import { useEffect, useState } from 'react'
import { get } from '@/rtk/helpers/apiService'
import { deleteDocument, listDocument } from '@/api/Admin'
import { Xdelete } from '@/rtk/helpers/CRUD'
import { useSelector } from 'react-redux'
import { RootState } from '@/rtk/store'
import { Anchor, Box, Button, Dialog, Group, Loader, Text } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { notifications } from '@mantine/notifications'
import {
IconEdit,
IconFileTypeDoc,
IconFileTypePdf,
IconFileTypeXls,
IconLink,
IconX,
} from '@tabler/icons-react'
import DataTableAll from '@/components/DataTable/DataTable'
import ModalFileDocument from './ModalFileDocument'
import classes from './Document.module.css'
import ModalAddDocument from './ModalAddDocument'
import ModalEditDocument from './ModalEditDocument'
interface TDocument {
id: number
title: string
uri: string
type: string
is_active: boolean
created_at: string
updated_at: string
}
type RequestPagination = {
data: TDocument[]
status: boolean
}
const Document = () => {
const user = useSelector((state: RootState) => state.authentication)
const [loader, setLoader] = useState<boolean>(false)
const [rows, setRows] = useState<RequestPagination>({
data: [],
status: true,
})
const [selectDataRow, setSelectDataRow] = useState<any>({})
const [openedModalAdd, { open: openModalAdd, close: closeModalAdd }] =
useDisclosure(false)
const [openedModalEdit, { open: openModalEdit, close: closeModalEdit }] =
useDisclosure(false)
const [openedModalFile, { open: openModalFile, close: closeModalFile }] =
useDisclosure(false)
const [openedDialogDelete, setOpenedDialogDelete] = useState(false)
const [deleteLoader, setDeleteLoader] = useState<boolean>(false)
useEffect(() => {
getAllData()
}, [])
const getAllData = async () => {
try {
setLoader(true)
const params = {}
const res = await get(listDocument, params)
if (res.status) {
setRows(res)
}
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
} finally {
setLoader(false)
}
}
const handleDelete = async (id: number | undefined) => {
try {
setDeleteLoader(true)
await Xdelete(deleteDocument, { id: id }, getAllData)
setSelectDataRow({})
setOpenedDialogDelete(false)
} catch (error) {
console.log(error)
} finally {
setDeleteLoader(false)
}
}
const getFileTypeIcon = (row: TDocument) => {
const uri = row?.uri
if (!uri) return null
const extension = uri.split('.').pop()?.toLowerCase()
if (['doc'].includes(extension!)) {
return (
<a
href={`${import.meta.env.VITE_BACKEND_URL}${uri}`}
download="Document Download"
target="_self"
>
<IconFileTypeDoc style={{ color: '#1e62c1' }} />
</a>
)
}
if (['xls', 'xlsx'].includes(extension!)) {
return (
<a
href={`${import.meta.env.VITE_BACKEND_URL}${uri}`}
download="Document Download"
target="_self"
>
<IconFileTypeXls style={{ color: '#0e864b' }} />
</a>
)
}
if (['docx'].includes(extension!)) {
return (
<IconFileTypeDoc
onClick={() => {
openModalFile()
setSelectDataRow(row)
}}
style={{ color: '#1e62c1' }}
/>
)
}
return (
<IconFileTypePdf
onClick={() => {
openModalFile()
setSelectDataRow(row)
}}
style={{ color: '#ff1b0e' }}
/>
)
}
const columns = [
{
name: 'title',
size: '50%',
header: 'Title',
},
{
name: 'uri',
size: '45%',
header: 'URI',
render: (row: TDocument) => {
return (
<Box>
{row.type === 'file' ? (
<Box
w="fit-content"
style={{ cursor: 'pointer' }}
title={`File .${row?.uri
.split('.')
.pop()
?.toLowerCase()} detail`}
>
{getFileTypeIcon(row)}
</Box>
) : (
<Anchor
ta="start"
href={row?.uri}
target="_blank"
title={row?.uri}
>
<IconLink />
</Anchor>
)}
</Box>
)
},
},
{
name: '#',
size: '5%',
header: 'Action',
render: (row: TDocument) => {
if (!user.user.user.permission.includes('admin')) {
return ''
}
return (
<Box className={classes.optionIcon}>
<IconEdit
className={classes.editIcon}
onClick={() => {
setSelectDataRow(row)
openModalEdit()
}}
width={20}
height={20}
/>
<IconX
className={classes.deleteIcon}
onClick={() => {
setOpenedDialogDelete(true)
setSelectDataRow(row)
}}
width={20}
height={20}
/>
</Box>
)
},
},
]
return (
<Box>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Documents
</h3>
{user.user.user.permission.includes('admin') ? (
<Button onClick={() => openModalAdd()} disabled={loader}>
+ Add
</Button>
) : (
''
)}
</div>
<Box mt={'md'}>
{loader ? (
<Box ta={'center'}>
<Loader size={40} mt={'15%'} />
</Box>
) : (
<DataTableAll
data={rows.data}
columns={columns}
size=""
searchInput
/>
)}
</Box>
{openedModalFile && (
<ModalFileDocument
opened={openedModalFile}
close={closeModalFile}
selectDataRow={selectDataRow}
setSelectDataRow={setSelectDataRow}
/>
)}
{openedModalAdd && (
<ModalAddDocument
opened={openedModalAdd}
close={closeModalAdd}
getAllData={getAllData}
/>
)}
{openedModalEdit && (
<ModalEditDocument
opened={openedModalEdit}
close={closeModalEdit}
selectDataRow={selectDataRow}
setSelectDataRow={setSelectDataRow}
getAllData={getAllData}
/>
)}
{openedDialogDelete && (
<Dialog
opened={openedDialogDelete}
className={classes.dialog}
withCloseButton
onClose={() => {
setSelectDataRow({})
setOpenedDialogDelete(false)
}}
size="lg"
radius="md"
position={{ top: 30, right: 10 }}
>
<Box
className={classes.dialogText}
size="sm"
mb="xs"
fw={500}
pr={20}
>
<Text ta="center">
Do you want to delete the document <b>{selectDataRow?.title}</b> ?
</Text>
<Group justify="center" m={10}>
<Button
fw={700}
size="xs"
variant="filled"
color="red"
onClick={async () => handleDelete(selectDataRow?.id)}
disabled={deleteLoader}
>
Yes
</Button>
<Button
fw={700}
size="xs"
variant="filled"
color="gray"
onClick={() => {
setSelectDataRow({})
setOpenedDialogDelete(false)
}}
disabled={deleteLoader}
>
Cancel
</Button>
</Group>
</Box>
</Dialog>
)}
</Box>
)
}
export default Document

View File

@ -1,254 +0,0 @@
import { useEffect, useState } from 'react'
import { useForm } from '@mantine/form'
import {
Modal,
Button,
Text,
Box,
Switch,
Checkbox,
FileInput,
TextInput,
Group,
ActionIcon,
} from '@mantine/core'
import { IconPlus, IconTrash } from '@tabler/icons-react'
import { create } from '@/rtk/helpers/CRUD'
import { createDocument } from '@/api/Admin'
import { getHeaderInfo } from '@/rtk/helpers/tokenCreator'
import { notifications } from '@mantine/notifications'
type MProps = {
opened: boolean
close: () => void
getAllData: () => void
}
const ModalAddDocument = ({ opened, close, getAllData }: MProps) => {
const [loadingSubmit, setLoadingSubmit] = useState(false)
const form = useForm({
initialValues: {
type: 'file',
files: [] as { title: string; file: File }[],
links: [] as { title: string; uri: string }[],
is_active: true,
},
})
useEffect(() => {
form.reset()
}, [])
const handleCreate = async (values: any) => {
try {
if (values.type === 'file' && values.files.length === 0) {
notifications.show({
title: 'Error',
message: 'No files uploaded!!!',
color: 'red',
})
return
}
if (values.type === 'link' && values.links.length === 0) {
notifications.show({
title: 'Error',
message: 'No links provided!!!',
color: 'red',
})
return
}
setLoadingSubmit(true)
const header = await getHeaderInfo()
const formData = new FormData()
header.headers['Content-Type'] = 'multipart/form-data'
formData.append('type', values.type)
formData.append('is_active', values.is_active ? '1' : '0')
if (values.type === 'file') {
values.files.forEach((item: any, index: number) => {
formData.append(`files[${index}][title]`, item.title)
formData.append(`files[${index}][file]`, item.file)
})
} else {
values.links.forEach((item: any, index: number) => {
formData.append(`links[${index}][title]`, item.title)
formData.append(`links[${index}][uri]`, item.uri)
})
}
const res = await create(createDocument, formData, getAllData, header)
if (res === true) {
resetForm()
}
} catch (error) {
console.error(error)
} finally {
setLoadingSubmit(false)
}
}
const resetForm = () => {
close()
form.reset()
}
return (
<Modal
opened={opened}
onClose={resetForm}
size="lg"
title={
<Text fw={700} fz={'lg'}>
Add Document
</Text>
}
>
<form onSubmit={form.onSubmit(handleCreate)}>
<Switch
style={{ width: 'fit-content' }}
label={form.values.type === 'file' ? 'Upload files' : 'Enter links'}
checked={form.values.type === 'file'}
onChange={(event) =>
form.setFieldValue(
'type',
event.currentTarget.checked ? 'file' : 'link',
)
}
mb={'md'}
disabled={loadingSubmit}
/>
{form.values.type === 'file' ? (
<Box>
<FileInput
accept=".doc,.docx,.xls,.xlsx,.pdf"
label="Upload Doc, Excel, PDF files"
multiple
mb="md"
value={form.values.files.map((file) => file.file)}
onChange={(files) => {
if (files) {
const newFiles = files.map((file) => ({
title: file.name.split('.')[0],
file,
}))
form.setFieldValue('files', newFiles)
}
}}
disabled={loadingSubmit}
required
/>
{form.values.files.map((item, index) => (
<Group key={index} mb={'md'}>
<TextInput
w="50%"
label="Title"
value={form.values.files[index].title}
onChange={(event) =>
form.setFieldValue(
`files.${index}.title`,
event.currentTarget.value,
)
}
maxLength={255}
disabled={loadingSubmit}
required
/>
<Text mt={24} w="30%">
{item.file?.name}
</Text>
</Group>
))}
</Box>
) : (
<Box>
{form.values.links.map((_, index) => (
<Group key={index} mb={'md'}>
<TextInput
w="35%"
label="Title"
value={form.values.links[index].title}
onChange={(event) =>
form.setFieldValue(
`links.${index}.title`,
event.currentTarget.value,
)
}
maxLength={255}
disabled={loadingSubmit}
required
/>
<TextInput
w="50%"
label="URL"
value={form.values.links[index].uri}
onChange={(event) =>
form.setFieldValue(
`links.${index}.uri`,
event.currentTarget.value,
)
}
disabled={loadingSubmit}
required
/>
<ActionIcon
color="red"
variant="light"
onClick={() =>
form.setFieldValue(
'links',
form.values.links.filter((_, i) => i !== index),
)
}
disabled={loadingSubmit}
mt={24}
>
<IconTrash size={16} />
</ActionIcon>
</Group>
))}
<Button
leftSection={<IconPlus size={16} />}
variant="light"
onClick={() =>
form.setFieldValue('links', [
...form.values.links,
{ title: '', uri: '' },
])
}
disabled={loadingSubmit}
mb="md"
>
Add Link
</Button>
</Box>
)}
<Checkbox
label="Is Active"
mb={'md'}
checked={form.values.is_active}
onChange={(event) =>
form.setFieldValue('is_active', event.currentTarget.checked)
}
disabled={loadingSubmit}
/>
<Box ta="center" mt="lg">
<Button color="green" type="submit" loading={loadingSubmit}>
Create
</Button>
</Box>
</form>
</Modal>
)
}
export default ModalAddDocument

View File

@ -1,285 +0,0 @@
import { useEffect, useState } from 'react'
import { useForm } from '@mantine/form'
import {
Modal,
Button,
Text,
Box,
Checkbox,
FileInput,
Badge,
TextInput,
Group,
} from '@mantine/core'
import { update } from '@/rtk/helpers/CRUD'
import { updateDocument } from '@/api/Admin'
import { getHeaderInfo } from '@/rtk/helpers/tokenCreator'
import {
IconFileTypeDoc,
IconFileTypePdf,
IconFileTypeXls,
} from '@tabler/icons-react'
import ModalFileDocument from './ModalFileDocument'
import { useDisclosure } from '@mantine/hooks'
type MProps = {
opened: boolean
close: () => void
selectDataRow: any
setSelectDataRow: any
getAllData: () => void
}
const ModalEditDocument = ({
opened,
close,
selectDataRow,
setSelectDataRow,
getAllData,
}: MProps) => {
const [loadingSubmit, setLoadingSubmit] = useState(false)
const [selectDataFileRow, setSelectDataFileRow] = useState<any>({})
const [openedModalFile, { open: openModalFile, close: closeModalFile }] =
useDisclosure(false)
const form = useForm({
initialValues: {
title: '',
type: true,
file: null as File | null,
uri: '',
is_active: true,
},
})
useEffect(() => {
form.setValues({
title: selectDataRow?.title || '',
type: selectDataRow?.type === 'file',
uri: selectDataRow?.uri || '',
is_active: selectDataRow?.is_active || true,
})
}, [selectDataRow])
const handleUpdate = async (data: any) => {
try {
setLoadingSubmit(true)
let formData: any = {}
const header = await getHeaderInfo()
if (data.type) {
header.headers['Content-Type'] = 'multipart/form-data'
const tmpFormData = new FormData()
tmpFormData.append('title', data.title)
tmpFormData.append('type', 'file')
tmpFormData.append('is_active', data.is_active ? '1' : '0')
tmpFormData.append('existing_file', data.uri)
if (data.file) {
tmpFormData.append('file', data.file)
}
formData = tmpFormData
} else {
formData = {
title: data.title,
type: 'link',
uri: data.uri,
is_active: data.is_active ? '1' : '0',
}
}
const res = await update(
updateDocument + `?id=${selectDataRow?.id}`,
formData,
getAllData,
header,
)
if (res === true) {
resetForm()
}
} catch (error: any) {
console.error(error)
} finally {
setLoadingSubmit(false)
}
}
const getFileTypeIcon = (uri: string) => {
if (!uri) return null
const extension = uri.split('.').pop()?.toLowerCase()
if (['doc'].includes(extension!)) {
return (
<a
href={`${import.meta.env.VITE_BACKEND_URL}${uri}`}
download="Document Download"
target="_self"
>
<IconFileTypeDoc style={{ color: '#1e62c1' }} />
</a>
)
}
if (['xls', 'xlsx'].includes(extension!)) {
return (
<a
href={`${import.meta.env.VITE_BACKEND_URL}${uri}`}
download="Document Download"
target="_self"
>
<IconFileTypeXls style={{ color: '#0e864b' }} />
</a>
)
}
if (['docx'].includes(extension!)) {
return (
<IconFileTypeDoc
onClick={() => {
openModalFile()
setSelectDataFileRow(selectDataRow)
}}
style={{ color: '#1e62c1' }}
/>
)
}
return (
<IconFileTypePdf
onClick={() => {
openModalFile()
setSelectDataFileRow(selectDataRow)
}}
style={{ color: '#ff1b0e' }}
/>
)
}
const resetForm = () => {
close()
form.reset()
setSelectDataRow({})
}
return (
<Modal
opened={opened}
onClose={resetForm}
size="lg"
title={
<Text fw={700} fz={'lg'}>
Update Document
</Text>
}
>
<form
onSubmit={form.onSubmit((values) => {
handleUpdate(values)
})}
>
{form.values.type ? (
<Badge tt="initial" color="orange" mb="md">
Files
</Badge>
) : (
<Badge tt="initial" mb="md">
Links
</Badge>
)}
{form.values.type ? (
<Box>
<TextInput
label="Title"
key={'title'}
{...form.getInputProps('title')}
maxLength={255}
disabled={loadingSubmit}
required
mb={'md'}
/>
<FileInput
accept=".doc,.docx,.xls,.xlsx,.pdf"
label="Upload Doc, Excel, PDF file"
value={form.values.file}
onChange={(file) => form.setFieldValue('file', file || null)}
disabled={loadingSubmit}
mb={'md'}
/>
{form.values.file ? (
''
) : (
<Group
w="fit-content"
style={{ cursor: 'pointer' }}
mb={'md'}
title={`File ${form.values.uri
.split('.')
.pop()
?.toLowerCase()} detail`}
>
{getFileTypeIcon(form.values.uri)}
<Text>.{form.values.uri.split('.').pop()?.toLowerCase()}</Text>
</Group>
)}
</Box>
) : (
<Box>
<TextInput
label="Title"
key={'title'}
{...form.getInputProps('title')}
maxLength={255}
disabled={loadingSubmit}
required
mb={'md'}
/>
<TextInput
label="Enter URI"
key={'uri'}
{...form.getInputProps('uri')}
disabled={loadingSubmit}
required
mb={'md'}
/>
</Box>
)}
<Checkbox
label="Is Active"
mb={'md'}
checked={form.values.is_active}
onChange={(event) =>
form.setFieldValue('is_active', event.currentTarget.checked)
}
disabled={loadingSubmit}
/>
<Box ta="center" mt="lg">
<Button color="green" type="submit" loading={loadingSubmit}>
Update
</Button>
</Box>
</form>
{openedModalFile && (
<ModalFileDocument
opened={openedModalFile}
close={closeModalFile}
selectDataRow={selectDataFileRow}
setSelectDataRow={setSelectDataFileRow}
/>
)}
</Modal>
)
}
export default ModalEditDocument

View File

@ -1,115 +0,0 @@
import { useEffect, useState } from 'react'
import { getDownloadFile } from '@/rtk/helpers/apiService'
import { Modal, Text, Box, Loader, Paper, Button, Group } from '@mantine/core'
import FileViewer from 'react-file-viewer'
import { IconDownload } from '@tabler/icons-react'
import { downloadFile } from '@/api/Admin'
type MProps = {
opened: boolean
close: () => void
selectDataRow: any
setSelectDataRow: any
}
interface TDocumentFile {
uri: string
fileType: string
}
const ModalFileDocument = ({
opened,
close,
selectDataRow,
setSelectDataRow,
}: MProps) => {
const [fileDoc, setFileDoc] = useState<TDocumentFile>()
const [loader, setLoader] = useState<boolean>(false)
useEffect(() => {
getFile()
}, [])
const getFile = async () => {
try {
setLoader(true)
const params = {}
const fileUri = selectDataRow?.uri.replace('storage/uploads/', '')
const res = await getDownloadFile(`${downloadFile}/${fileUri}`, params)
setFileDoc({
uri: URL.createObjectURL(res.data),
fileType: getFileType(selectDataRow?.uri) || 'default',
})
} catch (error: any) {
console.log(error)
} finally {
setLoader(false)
}
}
const supportedFileTypes = ['pdf', 'xlsx', 'xls', 'docx', 'doc']
const getFileType = (fileName: string) => {
const extension = fileName.split('.').pop()?.toLowerCase()
return supportedFileTypes.includes(extension!) ? extension : 'default'
}
return (
<Modal
opened={opened}
onClose={() => {
close()
setSelectDataRow({})
}}
size="65%"
title={
<Text fw={700} fz={'lg'}>
File Detail: {selectDataRow?.title}
</Text>
}
>
<Group justify="flex-end" mb={'md'}>
<a
href={`${import.meta.env.VITE_BACKEND_URL}${
import.meta.env.VITE_BACKEND_URL?.includes('localhost')
? ''
: 'image/'
}${selectDataRow.uri}`}
download="Document Download"
target={
getFileType(selectDataRow?.uri) === 'pdf' ? '_blank' : '_self'
}
>
<Button
leftSection={<IconDownload size={18} />}
color={
getFileType(selectDataRow?.uri) === 'pdf'
? 'red'
: getFileType(selectDataRow?.uri) === 'xlsx' ||
getFileType(selectDataRow?.uri) === 'xls'
? 'green'
: ''
}
>
Download .{getFileType(selectDataRow?.uri)}
</Button>
</a>
</Group>
<Paper withBorder>
{loader ? (
<Box ta={'center'} my={20}>
<Loader size={40} />
</Box>
) : (
<Box w="100%">
<FileViewer fileType={fileDoc?.fileType} filePath={fileDoc?.uri} />
</Box>
)}
</Paper>
</Modal>
)
}
export default ModalFileDocument

View File

@ -5,7 +5,6 @@ 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'
@ -82,20 +81,6 @@ const mainRoutes = [
</ProtectedRoute>
),
},
{
path: '/documents',
element: (
<ProtectedRoute mode="home" permission="staff,accountant">
<BasePage
main={
<>
<Document />
</>
}
></BasePage>
</ProtectedRoute>
),
},
{
path: '/timekeeping',
element: (