create page manage profiles, page add profile user
This commit is contained in:
parent
7c8267d6e3
commit
e226ad41d7
|
|
@ -115,4 +115,126 @@ class ProfileController extends Controller
|
|||
$user->save();
|
||||
return AbstractController::ResultSuccess($path);
|
||||
}
|
||||
|
||||
public function listFiles(Request $request)
|
||||
{
|
||||
// Get the root folder from the input URL
|
||||
$rootFolder = $request->input('root_folder');
|
||||
|
||||
// Ensure the root folder is correctly formatted
|
||||
$rootFolder = rtrim($rootFolder, '/') . '/';
|
||||
|
||||
// Get all files and directories in the specified root folder
|
||||
$fileList = $this->getDirectoryTree(public_path($rootFolder), $rootFolder);
|
||||
|
||||
return response()->json(['data' => $fileList, 'status' => true]);
|
||||
}
|
||||
|
||||
private function getDirectoryTree($dir, $urlRoot)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
// Scan the directory for files and folders
|
||||
$files = scandir($dir);
|
||||
|
||||
foreach ($files as $file) {
|
||||
if ($file !== '.' && $file !== '..') {
|
||||
$filePath = $dir . DIRECTORY_SEPARATOR . $file;
|
||||
$fileUrl = url($urlRoot . $file);
|
||||
|
||||
if (is_dir($filePath)) {
|
||||
// If it's a directory, recurse into it
|
||||
$results[] = [
|
||||
'label' => $file,
|
||||
'type' => 'directory',
|
||||
'value' => $fileUrl,
|
||||
'children' => $this->getDirectoryTree($filePath, $urlRoot . $file . '/')
|
||||
];
|
||||
} else {
|
||||
// If it's a file, add it to the list
|
||||
$results[] = [
|
||||
'label' => $file,
|
||||
'type' => 'file',
|
||||
'value' => $fileUrl
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function updateProfile(Request $request)
|
||||
{
|
||||
$name = $request->input('name') ?? auth('admins')->user()->name;
|
||||
// Validate the incoming files
|
||||
$request->validate([
|
||||
'files.*' => 'required|file|mimes:jpg,png,jpeg,pdf,doc,docx|max:5120', // Adjust file types and size limit as needed
|
||||
]);
|
||||
|
||||
$uploadedFiles = [];
|
||||
$baseDirectory = 'profiles/' . $name;
|
||||
$othersDirectory = $baseDirectory . '/others';
|
||||
|
||||
// Check if the base directory exists, if not create it
|
||||
if (!Storage::disk('public')->exists($baseDirectory)) {
|
||||
Storage::disk('public')->makeDirectory($baseDirectory);
|
||||
}
|
||||
|
||||
// Check if the "others" directory exists, if not create it
|
||||
if (!Storage::disk('public')->exists($othersDirectory)) {
|
||||
Storage::disk('public')->makeDirectory($othersDirectory);
|
||||
}
|
||||
if ($request->hasFile('files')) {
|
||||
foreach ($request->file('files') as $file) {
|
||||
// Store the file and get its path
|
||||
$originalFilename = $file->getClientOriginalName();
|
||||
if (strpos($originalFilename, '__') === 0) {
|
||||
// Store the file in the "others" directory
|
||||
$path = $file->storeAs($othersDirectory, $originalFilename, 'public');
|
||||
} else {
|
||||
// Store the file in the base directory
|
||||
$path = $file->storeAs($baseDirectory, $originalFilename, 'public');
|
||||
}
|
||||
$uploadedFiles[] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'Files uploaded successfully',
|
||||
'files' => $uploadedFiles,
|
||||
]);
|
||||
}
|
||||
|
||||
public function removeFile(Request $request)
|
||||
{
|
||||
// Validate that the file URL is provided in the request
|
||||
$request->validate([
|
||||
'file_url' => 'required|string',
|
||||
]);
|
||||
|
||||
// Get the full file URL from the request
|
||||
$fileUrl = $request->input('file_url');
|
||||
|
||||
// Parse the file path from the URL (remove the base URL part)
|
||||
$storagePath = parse_url($fileUrl, PHP_URL_PATH); // Extract the path part of the URL
|
||||
$filePath = str_replace('/storage/', '', $storagePath); // Remove "/storage/" to get the actual file path
|
||||
|
||||
// Check if the file exists before attempting to delete it
|
||||
if (Storage::disk('public')->exists($filePath)) {
|
||||
// Delete the file
|
||||
Storage::disk('public')->delete($filePath);
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'File deleted successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'File not found',
|
||||
], 404);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ Route::middleware('api')
|
|||
Route::group([
|
||||
'prefix' => 'timekeeping',
|
||||
], function () {
|
||||
Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
|
||||
Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/addMutilple', [TimekeepingController::class, 'addWorkingTimeForMultipleUser'])->middleware('check.permission:admin.hr');
|
||||
Route::post('/addNote', [TimekeepingController::class, 'addNoteForUser'])->middleware('check.permission:admin.hr');
|
||||
Route::get('/delete', [TimekeepingController::class, 'deleteNote'])->middleware('check.permission:admin.hr');
|
||||
|
|
@ -145,7 +145,7 @@ Route::middleware('api')
|
|||
Route::group([
|
||||
'prefix' => 'leave-management',
|
||||
], function () {
|
||||
Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff');
|
||||
Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
|
||||
Route::post('/saveNoteLeave', [LeaveManagementController::class, 'saveNoteLeave'])->middleware('check.permission:admin.hr');
|
||||
});
|
||||
|
||||
|
|
@ -159,6 +159,14 @@ Route::middleware('api')
|
|||
Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin');
|
||||
});
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'profile',
|
||||
], function () {
|
||||
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::group([
|
||||
'prefix' => 'criterias',
|
||||
], function () {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie', 'storage/*'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -11,19 +11,17 @@
|
|||
"test": "npm run jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@mantine/charts": "^7.4.1",
|
||||
"@mantine/core": "7.2.1",
|
||||
"@mantine/dates": "^7.3.2",
|
||||
"@mantine/ds": "^7.2.2",
|
||||
"@mantine/form": "^7.2.2",
|
||||
"@mantine/hooks": "7.2.1",
|
||||
"@mantine/notifications": "^7.2.2",
|
||||
"@mantine/vanilla-extract": "7.2.1",
|
||||
"@reduxjs/toolkit": "^1.9.7",
|
||||
"@tabler/icons-react": "^2.40.0",
|
||||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@mantine/core": "^7.13.2",
|
||||
"@mantine/dates": "^7.13.2",
|
||||
"@mantine/form": "^7.13.2",
|
||||
"@mantine/hooks": "^7.13.2",
|
||||
"@mantine/notifications": "^7.13.2",
|
||||
"@mantine/vanilla-extract": "^7.13.2",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"@tabler/icons-react": "^3.19.0",
|
||||
"@uiw/react-codemirror": "^4.23.5",
|
||||
"@vanilla-extract/css": "^1.16.0",
|
||||
"axios": "^1.6.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.10",
|
||||
|
|
@ -33,6 +31,7 @@
|
|||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"react": "^18.2.0",
|
||||
"react-doc-viewer": "^0.1.14",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router-dom": "^6.19.0",
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ export const getAllTechByUserId =
|
|||
export const evaluation = API_URL + 'v1/admin/evaluation/report'
|
||||
export const sprintReview = API_URL + 'v1/admin/evaluation/sprint-review'
|
||||
|
||||
export const getAllFilesInProfiles = API_URL + 'v1/admin/profile/all-files'
|
||||
export const updateProfileFolder = API_URL + 'v1/admin/profile/update-profile'
|
||||
export const deleteFile = API_URL + 'v1/admin/profile/delete-profile-file'
|
||||
|
||||
//Technical
|
||||
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
|
||||
export const createTechnical = API_URL + 'v1/admin/technical/create'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import DocViewer, { DocViewerRenderers } from "react-doc-viewer";
|
||||
|
||||
export default function FilePreview() {
|
||||
const docs = [
|
||||
// {
|
||||
// uri:
|
||||
// "http://localhost:9000/uploads/ULRYB3ATJ56B/Screenshot%202021-04-28%20at%2014.04.23.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20210507%2F%2Fs3%2Faws4_request&X-Amz-Date=20210507T142426Z&X-Amz-Expires=432000&X-Amz-SignedHeaders=host&X-Amz-Signature=761187860be22801088ab8c212733f7f52af8f62d638f1341ee2ae4c18944251"
|
||||
// // "http://localhost:9000/uploads/6QK5HJ84MAEM/RAS-118_CompanyCodes__SalesOffices.xlsx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20210507%2F%2Fs3%2Faws4_request&X-Amz-Date=20210507T110429Z&X-Amz-Expires=432000&X-Amz-SignedHeaders=host&X-Amz-Signature=c20f9b77ffdc1a15910cea5acd3420b6583a1d4d38ce5716da30f1d0ea4315d5"
|
||||
// // "https://res.cloudinary.com/cloudinaryforme/image/upload/v1618339571/workplace-1245776_1920_i9ayae.jpg"
|
||||
// },
|
||||
|
||||
// {
|
||||
// uri:
|
||||
// "https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf"
|
||||
// },
|
||||
{ uri: "https://s28.q4cdn.com/392171258/files/doc_downloads/test.pdf" },
|
||||
];
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>Hello CodeSandbox</h1>
|
||||
|
||||
<DocViewer pluginRenderers={DocViewerRenderers} documents={docs} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
// import { render } from 'tests'
|
||||
// import Footer from './Footer'
|
||||
|
||||
// describe('Footer component', () => {
|
||||
// it('renders "Footer"', () => {
|
||||
// render(<Footer />)
|
||||
// // expect(getByRole('divider', { name: 'Login' })).toBeInTheDocument()
|
||||
// })
|
||||
// })
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
import { Text, Container, ActionIcon, Group, rem } from '@mantine/core'
|
||||
import {
|
||||
IconBrandTwitter,
|
||||
IconBrandYoutube,
|
||||
IconBrandInstagram,
|
||||
} from '@tabler/icons-react'
|
||||
import { MantineLogo } from '@mantine/ds'
|
||||
import * as classes from './FooterLinks.module.css.ts'
|
||||
|
||||
const data = [
|
||||
{
|
||||
title: 'About',
|
||||
links: [
|
||||
{ label: 'Features', link: '#' },
|
||||
{ label: 'Pricing', link: '#' },
|
||||
{ label: 'Support', link: '#' },
|
||||
{ label: 'Forums', link: '#' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Project',
|
||||
links: [
|
||||
{ label: 'Contribute', link: '#' },
|
||||
{ label: 'Media assets', link: '#' },
|
||||
{ label: 'Changelog', link: '#' },
|
||||
{ label: 'Releases', link: '#' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
links: [
|
||||
{ label: 'Join Discord', link: '#' },
|
||||
{ label: 'Follow on Twitter', link: '#' },
|
||||
{ label: 'Email newsletter', link: '#' },
|
||||
{ label: 'GitHub discussions', link: '#' },
|
||||
],
|
||||
},
|
||||
]
|
||||
const Footer = () => {
|
||||
const groups = data.map((group) => {
|
||||
const links = group.links.map((link, index) => (
|
||||
<Text<'a'>
|
||||
key={index}
|
||||
className={classes.link}
|
||||
component="a"
|
||||
href={link.link}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
>
|
||||
{link.label}
|
||||
</Text>
|
||||
))
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper} key={group.title}>
|
||||
<Text className={classes.title}>{group.title}</Text>
|
||||
{links}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<footer className={classes.footer}>
|
||||
<Container className={classes.inner}>
|
||||
<div className={classes.logo}>
|
||||
<MantineLogo size={30} />
|
||||
<Text size="xs" c="dimmed" className={classes.description}>
|
||||
Build fully functional accessible web applications faster than ever
|
||||
</Text>
|
||||
</div>
|
||||
<div className={classes.groups}>{groups}</div>
|
||||
</Container>
|
||||
<Container className={classes.afterFooter}>
|
||||
<Text c="dimmed" size="sm">
|
||||
© 2023 APACTECH.dev. All rights reserved.
|
||||
</Text>
|
||||
|
||||
<Group
|
||||
gap={0}
|
||||
className={classes.social}
|
||||
justify="flex-end"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<ActionIcon size="lg" color="gray" variant="subtle">
|
||||
<IconBrandTwitter
|
||||
style={{ width: rem(18), height: rem(18) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</ActionIcon>
|
||||
<ActionIcon size="lg" color="gray" variant="subtle">
|
||||
<IconBrandYoutube
|
||||
style={{ width: rem(18), height: rem(18) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</ActionIcon>
|
||||
<ActionIcon size="lg" color="gray" variant="subtle">
|
||||
<IconBrandInstagram
|
||||
style={{ width: rem(18), height: rem(18) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Container>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
import { rem } from '@mantine/core'
|
||||
import { vars } from '@/theme'
|
||||
import { style } from '@vanilla-extract/css'
|
||||
|
||||
export const footer = style({
|
||||
paddingTop: rem(vars.spacing.xl),
|
||||
paddingBottom: rem(vars.spacing.xl),
|
||||
backgroundColor: `light-dark(${vars.colors.gray[0]}, ${vars.colors.dark[6]})`,
|
||||
borderTop: `${rem(1)} solid light-dark(${vars.colors.gray[2]}, transparent)`,
|
||||
})
|
||||
|
||||
export const logo = style({
|
||||
maxWidth: rem(200),
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const description = style({
|
||||
marginTop: rem(5),
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
marginTop: rem(vars.spacing.xs),
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const inner = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const groups = style({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const wrapper = style({
|
||||
width: rem(160),
|
||||
})
|
||||
|
||||
export const link = style({
|
||||
display: 'block',
|
||||
color: `light-dark(${vars.colors.gray[6]}, ${vars.colors.dark[1]})`,
|
||||
fontSize: vars.fontSizes.sm,
|
||||
paddingTop: rem(3),
|
||||
paddingBottom: rem(3),
|
||||
|
||||
':hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
})
|
||||
|
||||
export const title = style({
|
||||
fontSize: vars.fontSizes.lg,
|
||||
fontWeight: 700,
|
||||
fontFamily: `Greycliff CF, ${vars.fontFamily}`,
|
||||
marginBottom: Number(vars.spacing.xs) / 2,
|
||||
color: `light-dark(${vars.colors.black}, ${vars.colors.white})`,
|
||||
})
|
||||
|
||||
export const afterFooter = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: vars.spacing.xl,
|
||||
paddingTop: vars.spacing.xl,
|
||||
paddingBottom: vars.spacing.xl,
|
||||
borderTop: `rem(1) solid light-dark(${vars.colors.gray[2]}, ${vars.colors.dark[4]})`,
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const social = style({
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.sm})`]: {
|
||||
marginTop: vars.spacing.xs,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { style, styleVariants } from '@vanilla-extract/css'
|
||||
import { vars } from '../../theme'
|
||||
import { rem } from '@mantine/core'
|
||||
|
||||
export const header = style({
|
||||
paddingTop: rem(vars.spacing.sm),
|
||||
backgroundColor: `light-dark(${vars.colors.gray[0]}, ${vars.colors.dark[6]})`,
|
||||
borderBottom: `${rem(1)} solid light-dark(${
|
||||
vars.colors.gray[2]
|
||||
}, transparent)`,
|
||||
marginBottom: rem(120),
|
||||
})
|
||||
|
||||
export const mainSection = style({
|
||||
paddingBottom: rem(vars.spacing.sm),
|
||||
})
|
||||
|
||||
export const user = style({
|
||||
color: `light-dark(${vars.colors.black}, ${vars.colors.dark[0]})`,
|
||||
padding: `${rem(vars.spacing.xs)} ${rem(vars.spacing.sm)}`,
|
||||
borderRadius: rem(vars.radius.sm),
|
||||
transition: 'background-color 100ms ease',
|
||||
|
||||
':hover': {
|
||||
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[8]})`,
|
||||
},
|
||||
|
||||
'@media': {
|
||||
[`(max-width: ${vars.breakpoints.xs})`]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const userActive = style({
|
||||
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[8]})`,
|
||||
})
|
||||
|
||||
export const tabsList = style({
|
||||
'::before': {
|
||||
display: 'none',
|
||||
},
|
||||
})
|
||||
|
||||
export const tab = styleVariants({
|
||||
default: {
|
||||
fontWeight: 500,
|
||||
height: rem(38),
|
||||
backgroundColor: 'transparent',
|
||||
position: 'relative',
|
||||
bottom: '-1px',
|
||||
|
||||
':hover': {
|
||||
backgroundColor: `light-dark(${vars.colors.gray[1]}, ${vars.colors.dark[5]})`,
|
||||
},
|
||||
},
|
||||
active: {
|
||||
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[7]})`,
|
||||
borderColor: `light-dark(${vars.colors.gray[2]}, ${vars.colors.dark[7]})`,
|
||||
borderBottomColor: 'transparent',
|
||||
},
|
||||
})
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
// import { render } from '@tests'
|
||||
// import Header from './Header'
|
||||
|
||||
// describe('Header component', () => {
|
||||
// it('renders "Header"', () => {
|
||||
// render(<Header header={''} setHeader={function(): void {
|
||||
// throw new Error('Function not implemented.')
|
||||
// } } />)
|
||||
// // expect(getByRole('divider', { name: 'Login' })).toBeInTheDocument()
|
||||
// })
|
||||
// })
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
import { logout } from '@/rtk/dispatches/auth'
|
||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
||||
import {
|
||||
Avatar,
|
||||
Burger,
|
||||
Container,
|
||||
Group,
|
||||
Menu,
|
||||
Tabs,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
rem,
|
||||
} from '@mantine/core'
|
||||
import { MantineLogo } from '@mantine/ds'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconLogout,
|
||||
IconSettings,
|
||||
IconSwitchHorizontal,
|
||||
} from '@tabler/icons-react'
|
||||
import cx from 'clsx'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { ColorSchemeToggle } from '../ColorSchemeToggle/ColorSchemeToggle'
|
||||
import LanguagePicker from '../LanguagePicker/LanguagePicker'
|
||||
import * as classes from './DoubleHeader.module.css.ts'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// const user = {
|
||||
// name: 'Jane Spoonfighter',
|
||||
// email: 'janspoon@fighter.dev',
|
||||
// image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png',
|
||||
// };
|
||||
|
||||
const tabs = ['Home', 'Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5']
|
||||
|
||||
const Header = ({
|
||||
header,
|
||||
setHeader,
|
||||
}: {
|
||||
header: string
|
||||
setHeader: (newHeader: string) => void
|
||||
}) => {
|
||||
const [opened, { toggle }] = useDisclosure(false)
|
||||
const [userMenuOpened, setUserMenuOpened] = useState(false)
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate()
|
||||
const user = useAppSelector((state) => state.authentication.user.user)
|
||||
|
||||
const items = tabs.map((tab) => (
|
||||
<Tabs.Tab value={tab} key={tab} onClick={() => setHeader(tab)}>
|
||||
{tab}
|
||||
</Tabs.Tab>
|
||||
))
|
||||
const handleLogout = useCallback(() => {
|
||||
dispatch(logout(navigate))
|
||||
}, [dispatch, navigate])
|
||||
return (
|
||||
<div className={classes.header}>
|
||||
<Container className={classes.mainSection} size="xl">
|
||||
<Group justify="space-between">
|
||||
<MantineLogo size={28} />
|
||||
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="xs" size="sm" />
|
||||
|
||||
<Group>
|
||||
<LanguagePicker />
|
||||
|
||||
<ColorSchemeToggle />
|
||||
<Menu
|
||||
width={260}
|
||||
position="bottom-end"
|
||||
transitionProps={{ transition: 'pop-top-right' }}
|
||||
onClose={() => setUserMenuOpened(false)}
|
||||
onOpen={() => setUserMenuOpened(true)}
|
||||
withinPortal
|
||||
>
|
||||
<Menu.Target>
|
||||
<UnstyledButton
|
||||
className={cx(classes.user, {
|
||||
[classes.userActive]: userMenuOpened,
|
||||
})}
|
||||
>
|
||||
<Group gap={7}>
|
||||
<Avatar
|
||||
src={
|
||||
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png'
|
||||
}
|
||||
alt={user.name}
|
||||
radius="xl"
|
||||
size={25}
|
||||
/>
|
||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
||||
{user.name}
|
||||
</Text>
|
||||
<IconChevronDown
|
||||
style={{ width: rem(12), height: rem(12) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{/* <Menu.Item
|
||||
leftSection={
|
||||
<IconHeart
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
color={theme.colors.red[6]}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Liked posts
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconStar
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
color={theme.colors.yellow[6]}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Saved posts
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconMessage
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
color={theme.colors.blue[6]}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Your comments
|
||||
</Menu.Item> */}
|
||||
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconSettings
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Account settings
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconSwitchHorizontal
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Change account
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconLogout
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
|
||||
{/* <Menu.Divider />
|
||||
|
||||
<Menu.Label>Danger zone</Menu.Label>
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
<IconPlayerPause style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
|
||||
}
|
||||
>
|
||||
Pause subscription
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
color="red"
|
||||
leftSection={<IconTrash style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
|
||||
>
|
||||
Delete account
|
||||
</Menu.Item> */}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Group>
|
||||
</Container>
|
||||
<Container size="md">
|
||||
<Tabs
|
||||
defaultValue={header}
|
||||
variant="outline"
|
||||
visibleFrom="sm"
|
||||
classNames={{
|
||||
// root: classes.tab.active,
|
||||
list: classes.tabsList,
|
||||
tab: classes.tab.default,
|
||||
}}
|
||||
>
|
||||
<Tabs.List justify="center">{items}</Tabs.List>
|
||||
</Tabs>
|
||||
</Container>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
|
@ -4,6 +4,7 @@ import { logout } from '@/rtk/dispatches/auth'
|
|||
import { get, post } from '@/rtk/helpers/apiService'
|
||||
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
||||
import { checkPermissions } from '@/utils/checkRoles'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
|
|
@ -26,6 +27,7 @@ import {
|
|||
IconCalendarClock,
|
||||
IconChartDots2,
|
||||
IconDevices,
|
||||
IconFolders,
|
||||
IconLayoutSidebarLeftExpand,
|
||||
IconLayoutSidebarRightExpand,
|
||||
IconListCheck,
|
||||
|
|
@ -39,13 +41,12 @@ import {
|
|||
IconSun,
|
||||
IconTicket,
|
||||
IconUsersGroup,
|
||||
IconZoomExclamation,
|
||||
IconZoomExclamation
|
||||
} from '@tabler/icons-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
|
||||
import classes from './NavbarSimpleColored.module.css'
|
||||
import { checkPermissions } from '@/utils/checkRoles'
|
||||
|
||||
const data = [
|
||||
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
|
||||
|
|
@ -53,7 +54,7 @@ const data = [
|
|||
link: '/timekeeping',
|
||||
label: 'Timekeeping',
|
||||
icon: IconCalendar,
|
||||
permissions: 'admin,hr,staff,tester',
|
||||
permissions: 'admin,hr,staff,tester,accountant',
|
||||
group: 'staff',
|
||||
},
|
||||
{
|
||||
|
|
@ -74,7 +75,7 @@ const data = [
|
|||
link: '/leave-management',
|
||||
label: 'Leave Management',
|
||||
icon: IconCalendarClock,
|
||||
permissions: 'admin,hr,staff,tester',
|
||||
permissions: 'admin,hr,staff,tester,accountant',
|
||||
group: 'staff',
|
||||
},
|
||||
{
|
||||
|
|
@ -140,6 +141,13 @@ const data = [
|
|||
group: 'admin',
|
||||
permissions: 'admin',
|
||||
},
|
||||
{
|
||||
link: '/profiles',
|
||||
label: 'Profiles',
|
||||
icon: IconFolders,
|
||||
group: 'admin',
|
||||
permissions: 'admin,accountant',
|
||||
},
|
||||
// { link: '/jira', label: 'Jira', icon: IconSubtask },
|
||||
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
|
||||
// { link: '/general-setting', label: 'General Setting', icon: IconSettings },
|
||||
|
|
@ -231,7 +239,7 @@ const Navbar = ({
|
|||
// })
|
||||
|
||||
const group = [
|
||||
{ name: 'staff', label: 'General', permissions: 'admin,hr,staff,tester' },
|
||||
{ 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,28 @@
|
|||
.root {
|
||||
/* font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; */
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.label[data-hovered] {
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
}
|
||||
/* Add the light and dark logic using classes, for example */
|
||||
.label[data-hovered].light {
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
}
|
||||
.label[data-hovered].dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
color: var(--mantine-color-white);
|
||||
}
|
||||
|
||||
.label[data-selected] {
|
||||
font-weight: 700;
|
||||
}
|
||||
.label[data-selected].light {
|
||||
background-color: var(--mantine-color-blue-0);
|
||||
color: var(--mantine-color-black);
|
||||
}
|
||||
.label[data-selected].dark {
|
||||
background-color: rgba(var(--mantine-color-blue-8), 0.35);
|
||||
color: var(--mantine-color-blue-0);
|
||||
}
|
||||
|
|
@ -0,0 +1,504 @@
|
|||
import {
|
||||
deleteFile,
|
||||
getAllFilesInProfiles,
|
||||
updateProfileFolder,
|
||||
} from '@/api/Admin'
|
||||
import { Xdelete } from '@/rtk/helpers/CRUD'
|
||||
import { get } from '@/rtk/helpers/apiService'
|
||||
import { getAccessToken } from '@/rtk/localStorage'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FileInput,
|
||||
Group,
|
||||
Modal,
|
||||
RenderTreeNodePayload,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
Tree,
|
||||
} from '@mantine/core'
|
||||
import {
|
||||
IconFileTypeDocx,
|
||||
IconFileTypePdf,
|
||||
IconFolder,
|
||||
IconFolderOpen,
|
||||
IconFolderX,
|
||||
IconListCheck,
|
||||
IconPhoto,
|
||||
} from '@tabler/icons-react'
|
||||
import axios from 'axios'
|
||||
import { useEffect, useState } from 'react'
|
||||
import classes from './AllProfiles.module.css'
|
||||
|
||||
interface FileIconProps {
|
||||
name: string
|
||||
isFolder: boolean
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
type TFileProfile = {
|
||||
label: string
|
||||
type: string
|
||||
value: string
|
||||
children?: TFileProfile[]
|
||||
}
|
||||
|
||||
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 [openedProfile, setOpenedProfile] = useState(false)
|
||||
|
||||
function FileIcon({ name, isFolder, expanded }: FileIconProps) {
|
||||
if (name.endsWith('.pdf')) {
|
||||
return <IconFileTypePdf size={14} />
|
||||
}
|
||||
|
||||
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} />
|
||||
)
|
||||
}
|
||||
|
||||
function Leaf({
|
||||
node,
|
||||
expanded,
|
||||
hasChildren,
|
||||
elementProps,
|
||||
}: RenderTreeNodePayload) {
|
||||
return (
|
||||
<Group gap={5} {...elementProps}>
|
||||
{!node.children ? (
|
||||
<a href={node.value} target="_blank">
|
||||
<FileIcon
|
||||
name={node.value}
|
||||
isFolder={hasChildren}
|
||||
expanded={expanded}
|
||||
/>
|
||||
<span>{node.label}</span>
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
<FileIcon
|
||||
name={node.value}
|
||||
isFolder={hasChildren}
|
||||
expanded={expanded}
|
||||
/>
|
||||
<span>{node.label}</span>
|
||||
<Tooltip label="Upload">
|
||||
<IconListCheck
|
||||
color="green"
|
||||
width={15}
|
||||
height={15}
|
||||
style={{
|
||||
display: node.label !== 'others' ? 'block' : 'none',
|
||||
cursor: 'pointer',
|
||||
zIndex: 100000,
|
||||
}}
|
||||
onClick={() => {
|
||||
setCurrentName(node.label!.toString())
|
||||
setOpenedProfile(true)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
|
||||
const handleOtherFileChange = (
|
||||
index: number,
|
||||
field: string,
|
||||
value: File | string,
|
||||
) => {
|
||||
const updatedFiles: any = [...otherFiles]
|
||||
updatedFiles[index][field] = value
|
||||
setOtherFiles(updatedFiles)
|
||||
}
|
||||
|
||||
const addOtherFileInput = () => {
|
||||
setOtherFiles([...otherFiles, { file: null, type: '' }])
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault()
|
||||
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 (cv) {
|
||||
formData.append('files[]', cv)
|
||||
}
|
||||
|
||||
if (idCard) {
|
||||
formData.append('files[]', idCard)
|
||||
}
|
||||
|
||||
if (transcript) {
|
||||
formData.append('files[]', transcript)
|
||||
}
|
||||
|
||||
if (universityDiploma) {
|
||||
formData.append('files[]', universityDiploma)
|
||||
}
|
||||
|
||||
formData.append('name', currentName)
|
||||
|
||||
const token = await getAccessToken()
|
||||
try {
|
||||
const response = await axios.post(updateProfileFolder, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 200) {
|
||||
getAllFile()
|
||||
getTree()
|
||||
setOtherFiles([])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading files', error)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
getAllFile()
|
||||
}, [currentName])
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.title}>
|
||||
<h3>
|
||||
<Text>Admin/</Text>
|
||||
Profiles
|
||||
</h3>
|
||||
</div>
|
||||
<Box ml={'lg'}>
|
||||
<Tree
|
||||
classNames={classes}
|
||||
selectOnClick
|
||||
clearSelectionOnOutsideClick
|
||||
data={treeData}
|
||||
renderNode={(payload) => <Leaf {...payload} />}
|
||||
/>
|
||||
<Modal
|
||||
size={'lg'}
|
||||
opened={openedProfile}
|
||||
onClose={() => {
|
||||
setOpenedProfile(false)
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AllProfiles
|
||||
|
|
@ -1,26 +1,33 @@
|
|||
import {
|
||||
deleteFile,
|
||||
getAllFilesInProfiles,
|
||||
getProfilesData,
|
||||
listUserTechnical,
|
||||
updateProfileFolder,
|
||||
updateProfilesData,
|
||||
updateUserTechnical,
|
||||
} 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 { get, post, postImage } from '@/rtk/helpers/apiService'
|
||||
import { requirementsPassword } from '@/rtk/helpers/variables'
|
||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
|
||||
import { getUser } from '@/rtk/localStorage'
|
||||
import { getAccessToken, getUser } from '@/rtk/localStorage'
|
||||
import { success } from '@/rtk/slices/auth'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
FileInput,
|
||||
Flex,
|
||||
Group,
|
||||
Loader,
|
||||
Modal,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
|
|
@ -28,18 +35,26 @@ import {
|
|||
import { notifications } from '@mantine/notifications'
|
||||
import {
|
||||
IconExchange,
|
||||
IconFolder,
|
||||
IconPasswordUser,
|
||||
IconUserCode,
|
||||
IconUserCog,
|
||||
} from '@tabler/icons-react'
|
||||
import axios from 'axios'
|
||||
import moment from 'moment'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import classes from './Profile.module.css'
|
||||
import DataTableAll from '@/components/DataTable/DataTable'
|
||||
import moment from 'moment'
|
||||
import { Xdelete } from '@/rtk/helpers/CRUD'
|
||||
|
||||
const isCompactMenu = false
|
||||
|
||||
type TFileProfile = {
|
||||
label: string
|
||||
type: string
|
||||
value: string
|
||||
children?: TFileProfile[]
|
||||
}
|
||||
const Profile = () => {
|
||||
const user = useAppSelector((state) => state.authentication.user)
|
||||
const userData = getUser()
|
||||
|
|
@ -122,12 +137,102 @@ 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 updatedFiles: any = [...otherFiles]
|
||||
updatedFiles[index][field] = value
|
||||
setOtherFiles(updatedFiles)
|
||||
}
|
||||
|
||||
const addOtherFileInput = () => {
|
||||
setOtherFiles([...otherFiles, { file: null, type: '' }])
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault()
|
||||
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 (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,
|
||||
})
|
||||
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)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const result = await getListProfilesData()
|
||||
setDataProfile(result ?? [])
|
||||
}
|
||||
fetchData()
|
||||
getAllFile()
|
||||
}, [])
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
|
|
@ -201,6 +306,24 @@ 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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.title}>
|
||||
|
|
@ -329,6 +452,15 @@ const Profile = () => {
|
|||
Your Technical
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
style={{ width: '50%' }}
|
||||
color="orange"
|
||||
onClick={() => setOpenedProfile(true)}
|
||||
>
|
||||
<IconFolder stroke={1.5} />
|
||||
Update profile
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
|
@ -437,6 +569,173 @@ const Profile = () => {
|
|||
</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
size={'lg'}
|
||||
opened={openedProfile}
|
||||
onClose={() => {
|
||||
setOpenedProfile(false)
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Box>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import ResetPassword from '@/components/Authentication/ResetPassword'
|
||||
import BasePage from '@/components/BasePage/BasePage'
|
||||
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 LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
|
||||
|
|
@ -83,7 +84,7 @@ const mainRoutes = [
|
|||
{
|
||||
path: '/timekeeping',
|
||||
element: (
|
||||
<ProtectedRoute mode="home" permission="staff">
|
||||
<ProtectedRoute mode="home" permission="staff,accountant">
|
||||
<BasePage
|
||||
main={
|
||||
<>
|
||||
|
|
@ -97,7 +98,7 @@ const mainRoutes = [
|
|||
{
|
||||
path: '/leave-management',
|
||||
element: (
|
||||
<ProtectedRoute mode="home" permission="staff">
|
||||
<ProtectedRoute mode="home" permission="staff,accountant">
|
||||
<BasePage
|
||||
main={
|
||||
<>
|
||||
|
|
@ -234,6 +235,20 @@ const mainRoutes = [
|
|||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/profiles',
|
||||
element: (
|
||||
<ProtectedRoute mode="route" permission="admin,accountant">
|
||||
<BasePage
|
||||
main={
|
||||
<>
|
||||
<AllProfiles />
|
||||
</>
|
||||
}
|
||||
></BasePage>
|
||||
</ProtectedRoute>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// path: '/packages',
|
||||
// element: (
|
||||
|
|
|
|||
Loading…
Reference in New Issue