create function user manament

This commit is contained in:
JOSEPH LE 2024-09-06 16:28:59 +07:00
parent 8f6092be62
commit bb3c9b2106
20 changed files with 498 additions and 1666 deletions

View File

@ -3,16 +3,108 @@
namespace Modules\Auth\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Traits\IsAPI;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
use Modules\Auth\app\Models\User;
use Illuminate\Support\Str;
class UserController extends Controller
{
use IsAPI;
public function __construct()
{
$this->middleware('jwt.auth');
}
public function users()
{
return response()->json(User::all());
return response()->json(['data' => User::all(), 'status' => true]);
}
public function createOrUpdate(Request $request)
{
$this->apiValidation($request, [
'name' => 'required|string',
'email' => 'required|email',
'permission' => 'required|string',
]);
if ($request->has('id')) {
$payload = $request->only(['name', 'email', 'permission']);
$user = User::find($request->id);
$user->update($payload);
return response()->json(['data' => $user, 'status' => true, 'message' => 'Update successful']);
} else {
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt('Work@1234'),
'permission' => $request->permission
]);
$user_res = [
'name' => $user->name,
'email' => $user->email,
'password' => 'Work@1234',
'url' => 'https://ms.prology.net'
];
$gitea = Http::withHeaders([
'Authorization' => 'token ' . env('GITEA_ADMIN_TOKEN'),
'Accept' => 'application/json',
])->post('https://gitea.nswteam.net/api/v1/admin/users', [
'email' => $request->email,
'full_name' => $request->name,
'login_name' => Str::of($request->name)->lower()->replace(' ', ''),
'password' => 'Work@1234',
'must_change_password' => false,
'send_notify' => false,
'username' => Str::of($request->name)->lower()->replace(' ', '')
]);
$gitea_data = $gitea->json();
$gitea_res = [
"login" => $gitea_data['login'],
"full_name" => $gitea_data['full_name'],
"email" => $gitea_data['email'],
"password" => 'Work@1234',
"url" => 'https://gitea.nswteam.net',
];
$adminEmail = env('ZULIP_ADMIN_EMAIL');
$apiKey = env('ZULIP_API_KEY');
$apiUrl = env('ZULIP_API_URL') . '/invites';
$zulip = Http::asForm()->withBasicAuth($adminEmail, $apiKey)->post($apiUrl, [
'invitee_emails' => $request->email,
'invite_expires_in_minutes' => 1440,
'invite_as' => 400,
'stream_ids' => '[22]'
]);
$zulip_data = $zulip->json();
$zulip_data['msg'] = 'Check inbox email ' . $request->email;
$zulip_data['url'] = 'https://zulip.ipsupply.com.au';
return response()->json(['data' => ['user' => $user_res, 'gitea' => $gitea_res, 'zulip' => $zulip_data], 'status' => true, 'message' => 'Create successful']);
}
return response()->json(['status' => false, 'message' => 'Process fail']);
}
public function delete(Request $request)
{
$user = User::find($request->id);
if ($user) {
$user->delete();
return response()->json(['status' => true, 'message' => 'Delete successful']);
}
return response()->json(['status' => false, 'message' => 'User not found']);
}
}

View File

@ -15,7 +15,6 @@ use Tymon\JWTAuth\Facades\JWTAuth;
class User extends Authenticatable implements JWTSubject
{
use HasApiTokens, HasFactory, Notifiable;
use HasRoles;
/**
* The attributes that are mass assignable.
@ -26,6 +25,7 @@ class User extends Authenticatable implements JWTSubject
'name',
'email',
'password',
'permission'
];
/**

View File

@ -23,7 +23,10 @@ Route::middleware(['api'])
->prefix('v1')
->name('api.v1.')
->group(function () {
Route::get('users', [UserController::class, 'users']);
Route::middleware(['jwt.auth'])->get('users', [UserController::class, 'users']);
Route::middleware(['jwt.auth'])->post('users/createOrUpdate', [UserController::class, 'createOrUpdate']);
Route::middleware(['jwt.auth'])->get('users/delete', [UserController::class, 'delete']);
Route::post('login', [LoginController::class, 'login']);
Route::middleware(['jwt.auth'])->post('logout', [LoginController::class, 'logout']);
Route::middleware(['jwt.auth'])->post('change-password', [AuthController::class, 'updatePassword']);

View File

@ -1,55 +1,6 @@
const API_URL = import.meta.env.VITE_BACKEND_URL + 'api/'
export const getDetailUser = API_URL + 'v1/admin/detail'
// General setting
export const updateGeneralSetting = API_URL + 'v1/admin/setting'
export const getGeneralSetting = API_URL + 'v1/admin/setting'
export const clearCacheAPI = API_URL + 'v1/admin/setting/clear-cache'
// Packages
export const createPackage = API_URL + 'v1/admin/package/create'
export const getAllPackage = API_URL + 'v1/admin/package/all'
export const updatePackage = API_URL + 'v1/admin/package/update'
export const deletePackage = API_URL + 'v1/admin/package/delete'
// Custom theme setting
export const getCustomTheme = API_URL + 'v1/admin/custom-theme'
export const updateCustomTheme = API_URL + 'v1/admin/custom-theme'
// Discounts
export const getDiscounts = API_URL + 'v1/admin/discount/get'
export const getDiscountType = API_URL + 'v1/admin/discount-type/all'
export const createDiscount = API_URL + 'v1/admin/discount/create'
export const updateDiscount = API_URL + 'v1/admin/discount/update'
export const deleteDiscount = API_URL + 'v1/admin/discount/delete'
// Client
export const getClients = API_URL + 'v1/admin/client/get'
export const getCountry = API_URL + 'v1/admin/h-country'
export const addUser = API_URL + 'v1/admin/client/create'
export const updateUser = API_URL + 'v1/admin/client/update'
// Banners
export const getBanners = API_URL + 'v1/admin/banner/all'
export const createBanner = API_URL + 'v1/admin/banner/create'
export const updateBanner = API_URL + 'v1/admin/banner/update'
export const deleteBanner = API_URL + 'v1/admin/banner/delete'
// Orders
export const getOrders = API_URL + 'v1/admin/order/get'
export const updateOrder = API_URL + 'v1/admin/order/update'
// CheckHistory
export const getCheckHistory = API_URL + 'v1/admin/sn-check-history/get'
export const getDetailCheckById = API_URL + 'v1/admin/sn-check-history/show-detail'
// Contacts
export const getContacts = API_URL + 'v1/admin/contact/get'
// Dashboard
export const getDashboard = API_URL + 'v1/admin/dashboard/get'
export const statisticSearchSNByMonth = API_URL + 'v1/admin/dashboard/statistics-search-sn-by-month'
export const statisticRevenuesByMonth = API_URL + 'v1/admin/dashboard/statistics-revenues-by-month'
// Tracking
export const getListTracking = API_URL + 'v1/admin/tracking'
@ -84,4 +35,10 @@ export const getTickets = API_URL + 'v1/admin/ticket/all'
export const getTicketsOfUser = API_URL + 'v1/admin/ticket/getByUserId'
export const deleteTicket = API_URL + 'v1/admin/ticket/delete'
export const addTicket = API_URL + 'v1/admin/ticket/create'
export const handleTicket = API_URL + 'v1/admin/ticket/handle-ticket'
export const handleTicket = API_URL + 'v1/admin/ticket/handle-ticket'
//Users
export const getAllUsers = API_URL + 'v1/users'
export const createOrUpdateUser = API_URL + 'v1/users/createOrUpdate'
export const deleteUser = API_URL + 'v1/users/delete'

View File

@ -0,0 +1,43 @@
import { rem, Tooltip } from '@mantine/core'
import { useClipboard } from '@mantine/hooks'
import { IconCheck, IconCopy } from '@tabler/icons-react'
export function ButtonCopy({
value,
width = 20,
height = 20,
}: {
value: string
width?: number
height?: number
}) {
const clipboard = useClipboard()
return (
<Tooltip
label="Link copied!"
offset={5}
position="bottom"
radius="xl"
transitionProps={{ duration: 100, transition: 'slide-down' }}
opened={clipboard.copied}
>
{clipboard.copied ? (
<IconCheck
style={{ width: rem(width), height: rem(height) }}
stroke={1.5}
/>
) : (
<IconCopy
onClick={() => clipboard.copy(value)}
style={{
width: rem(width),
height: rem(height),
color: 'skyblue',
cursor: 'pointer',
}}
stroke={1.5}
/>
)}
</Tooltip>
)
}

View File

@ -31,6 +31,7 @@ import {
IconSun,
IconTicket,
IconDevices,
IconUsersGroup,
} from '@tabler/icons-react'
import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
@ -70,6 +71,12 @@ const data = [
icon: IconDevices,
group: 'admin',
},
{
link: '/users',
label: 'Users Management',
icon: IconUsersGroup,
group: 'admin',
}
// { link: '/jira', label: 'Jira', icon: IconSubtask },
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
// { link: '/general-setting', label: 'General Setting', icon: IconSettings },

View File

@ -1,470 +0,0 @@
import {
createBanner,
deleteBanner,
getBanners,
updateBanner,
} from '@/api/Admin'
import DataTableAll from '@/components/DataTable/DataTable'
import { Xdelete, create, update } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService'
import { getAccessToken } from '@/rtk/localStorage'
import {
Box,
Button,
Checkbox,
Dialog,
FileInput,
Group,
HoverCard,
Image,
Loader,
Modal,
Text,
TextInput,
Tooltip,
Badge,
} from '@mantine/core'
import { useForm } from '@mantine/form'
import { IconEdit, IconPointFilled, IconTrash } from '@tabler/icons-react'
import moment from 'moment'
import { useEffect, useState } from 'react'
import classes from './Banner.module.css'
const Banner = () => {
// type TFile = {
// name: string
// size: number
// type: string
// }
type TBanner = {
id?: number
title: string
link: string
image?: null | string | File
is_active: boolean | string | number
updated_at?: string
}
const [disableBtn, setDisableBtn] = useState(false)
const [action, setAction] = useState('')
const [loader, setLoader] = useState(true)
const [data, setData] = useState<TBanner[]>([])
const [imageTemp, setImageTemp] = useState<string | null | File>('')
const columns = [
{
name: 'id',
size: '5%',
header: 'ID',
},
{
name: 'title',
size: '15%',
header: 'Title',
},
{
name: 'link',
size: '20%',
header: 'Link',
render: (row: TBanner) =>
(
<Tooltip label={row?.link} position="bottom">
<Box style={{ maxWidth: '300px', overflow: 'hidden' }}>
{row?.link}
</Box>
</Tooltip>
) ?? <Badge color="blue">Null</Badge>,
},
{
name: 'image',
size: '20%',
header: 'Image',
render: (row: TBanner) => {
return (
<HoverCard shadow="md" openDelay={500} position="left">
<HoverCard.Target>
<Image h={'20vh'} src={row.image} />
</HoverCard.Target>
<HoverCard.Dropdown>
<Image
radius="sm"
h={300}
w="auto"
fit="contain"
src={row.image}
/>
</HoverCard.Dropdown>
</HoverCard>
)
},
},
{
name: 'is_active',
size: '5%',
header: 'Status',
render: (row: TBanner) => {
return (
<Box ta={'center'}>
{row.is_active === 1 ? (
<IconPointFilled style={{ color: '#19d105' }} />
) : (
<IconPointFilled style={{ color: 'red' }} />
)}
</Box>
)
},
},
{
name: 'updated_at',
size: '10%',
header: 'Updated at',
render: (row: TBanner) => {
return moment(row.updated_at).format('YYYY/MM/DD - HH:mm:ss')
},
},
{
name: '#',
size: '5%',
header: 'Action',
render: (row: TBanner) => {
return (
<Box className={classes.optionIcon}>
<Tooltip label="Edit">
<IconEdit
className={classes.editIcon}
width={20}
height={20}
onClick={() => {
setAction('edit')
form.setValues(row)
}}
/>
</Tooltip>
<Tooltip label="Delete">
<IconTrash
className={classes.deleteIcon}
width={20}
height={20}
onClick={() => {
setAction('delete')
form.setValues(row)
}}
/>
</Tooltip>
</Box>
)
},
},
]
const form = useForm<TBanner>({
initialValues: {
title: '',
link: '',
image: '',
is_active: true,
},
validate: (values) => ({
title: values.title.length < 5 ? 'Length 5 characters or more' : null,
// link: !/^(https?):\/\/[^\s/$.?#].[^\s]*$/.test(values.link)
// ? "URL incorrect"
// : null,
image:
values.image === null || values.image === ''
? 'Image incorrect'
: typeof values.image !== 'string' &&
values.image &&
values.image.size / 1024 / 1024 > 2
? 'Image must be under 2MB in size.'
: null,
}),
})
const getAllBanner = async () => {
try {
const res = await get(getBanners)
if (res.status) {
setData(res.data)
}
setLoader(false)
} catch (error) {
console.log(error)
}
}
const handleCreate = async (formData: TBanner) => {
try {
const token = getAccessToken()
const config = {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
}
const res = await create(createBanner, formData, getAllBanner, config)
if (res === true) {
setAction('')
form.reset()
}
} catch (error) {
console.log(error)
}
}
const handleUpdate = async (formData: TBanner) => {
try {
const token = getAccessToken()
const config = {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
}
if (formData && formData.is_active == 1) {
formData.is_active = true
} else {
formData.is_active = false
}
if (typeof formData.image !== 'object') {
delete formData.image
}
const res = await update(updateBanner, formData, getAllBanner, config)
if (res === true) {
setAction('')
form.reset()
}
} catch (error) {
console.log(error)
}
}
const handleDelete = async (id: number) => {
try {
const res = await Xdelete(deleteBanner, { id: id }, getAllBanner)
if (res === true) {
setAction('')
form.reset()
}
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getAllBanner()
}, [])
useEffect(() => {
if (form.values.image) {
setImageTemp(form.values.image)
}
}, [form.values.image])
return loader ? (
<Box ta={'center'}>
<Loader size={40} mt={'15%'} />
</Box>
) : (
<Box>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Banners
</h3>
<Button
m={5}
onClick={() => {
setAction('add')
}}
>
+ Add banner
</Button>
</div>
<Box pt={'md'}>
{data.length > 0 ? (
<DataTableAll data={data} columns={columns} size="" />
) : (
<Text
mt={'30vh'}
ta={'center'}
c={'#dbdee3'}
fw={600}
fz={'1.5rem'}
fs={'italic'}
>
Data Empty
</Text>
)}
</Box>
{/* Add/Edit User modal */}
<Modal
opened={action === 'add' || action === 'edit'}
onClose={() => {
setAction('')
form.reset()
}}
title={
<Text pl={'sm'} fw={700} fz={'lg'}>
{action === 'add' ? 'Add banner' : 'Update banner'}
</Text>
}
>
<form
onSubmit={form.onSubmit(async (values: TBanner) => {
setDisableBtn(true)
action === 'edit'
? await handleUpdate(values)
: await handleCreate(values)
setDisableBtn(false)
})}
>
<Box pl={'md'} pr={'md'}>
<TextInput
label={'Title'}
placeholder="Banner x"
maxLength={255}
required
value={form.values.title}
error={form.errors.title}
onChange={(e) =>
form.setFieldValue('title', e.currentTarget.value)
}
/>
<TextInput
label={'Link'}
// required
placeholder="https://www.abc.com"
maxLength={255}
value={form.values.link}
error={form.errors.link}
onChange={(e) =>
form.setFieldValue('link', e.currentTarget.value)
}
/>
<FileInput
label={'Image'}
id="imageBanner"
required
accept="image/*"
// valueComponent={ValueComponent}
error={form.errors.image}
onChange={(e) => form.setFieldValue('image', e)}
/>
{action === 'edit' && typeof imageTemp === 'string' ? (
<Image
style={{ cursor: 'pointer' }}
src={imageTemp}
onClick={() => {
const element = document.getElementById('imageBanner')
element?.click()
}}
/>
) : (
typeof form.values.image === 'object' &&
form.values.image !== null && (
<Image
style={{ cursor: 'pointer' }}
src={URL.createObjectURL(form.values.image!)}
w={250}
m={'auto'}
onClick={() => {
const element = document.getElementById('imageBanner')
element?.click()
}}
></Image>
)
)}
<Checkbox
label={
<Text
fz={'var(--input-label-size,var(--mantine-font-size-sm))'}
fw={600}
>
Status
</Text>
}
mt={'lg'}
display={action === 'edit' ? 'block' : 'none'}
checked={
form.values.is_active || form.values.is_active === '1'
? true
: false
}
onChange={(e) =>
form.setFieldValue('is_active', e.currentTarget.checked)
}
/>
<Box ta={'center'}>
{action === 'add' ? (
<Button
mt={'lg'}
bg={'green'}
type="submit"
disabled={disableBtn}
>
Create
</Button>
) : (
<Button
mt={'lg'}
bg={'green'}
type="submit"
disabled={disableBtn}
>
Save
</Button>
)}
</Box>
</Box>
</form>
</Modal>
{/* Dialog confirm delete */}
<Dialog
className={classes.dialog}
opened={action === 'delete'}
withCloseButton
onClose={() => setAction('')}
size="lg"
radius="md"
position={{ top: 30, right: 10 }}
>
<Text className={classes.dialogText} size="sm" mb="xs" fw={500}>
Do you want to delete this discount?
<Group justify="center" m={10}>
<Button
disabled={disableBtn}
fw={700}
size="xs"
variant="light"
onClick={async () => {
setDisableBtn(true)
await handleDelete(form.values.id!)
setDisableBtn(false)
}}
>
Yes
</Button>
<Button
fw={700}
size="xs"
variant="light"
onClick={() => setAction('')}
>
Cancel
</Button>
</Group>
</Text>
</Dialog>
</Box>
)
}
export default Banner

View File

@ -1,47 +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;
}
.editIcon {
color: rgb(9, 132, 132);
cursor: pointer;
padding: 2px;
border-radius: 25%;
}
.deleteIcon {
color: red;
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);
}
.dialog {
background-color: light-dark(white, #2d353c);
text-align: center;
border: solid 1px rgb(255, 145, 0);
}
.dialogText {
color: light-dark(#2d353c, white);
}

View File

@ -1,105 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useState } from 'react'
import { getContacts } from '@/api/Admin'
import { Box, Loader, Text } from '@mantine/core'
import classes from './Contacts.module.css'
import { DataTablePagination } from '@/components/DataTable/DataTable'
import { get } from '@/rtk/helpers/apiService'
import moment from 'moment'
const Contacts = () => {
const [loader, setLoader] = useState(true)
const [listContact, setListContact] = useState<any>()
const columns = [
{
name: 'id',
size: '5%',
header: 'ID',
},
{
name: 'name',
size: '15%',
header: 'Name',
},
{
name: 'phone',
size: '15%',
header: 'Phone',
},
{
name: 'company',
size: '15%',
header: 'Company',
},
{
name: 'email',
size: '30%',
header: 'Email',
},
{
name: 'created_at',
size: '20%',
header: 'Created at',
render: (row: any) => {
return moment(row.created_at).format('YYYY/MM/DD - HH:mm:ss')
},
},
]
const getListContact = async () => {
try {
const searchParams = new URLSearchParams(window.location.search)
const params = {}
for (const [key, value] of searchParams.entries()) {
if (key === 'page' && value === '') {
Object.assign(params, { [`${key}`]: 1 })
} else {
Object.assign(params, { [`${key}`]: value })
}
}
const res = await get(getContacts, params)
if (res.status || res.status === 200) {
setListContact(res)
}
setLoader(false)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getListContact()
}, [])
return (
<div>
{loader ? (
<Box ta={'center'}>
<Loader size={40} mt={'15%'} />
</Box>
) : (
<>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Contacts
</h3>
</div>
<Box p={20}>
<DataTablePagination
filterInfo={[]}
data={listContact}
columns={columns}
searchInput
size=""
/>
</Box>
</>
)}
</div>
)
}
export default Contacts

View File

@ -1,17 +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;
}
.txtarea {
height: 80vh;
margin: rem(5);
padding: rem(5);
width: 100%;
}

View File

@ -1,122 +0,0 @@
import { getCustomTheme, updateCustomTheme } from '@/api/Admin'
import { update } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService'
import { javascript } from '@codemirror/lang-javascript'
import { Box, Button, Group, InputLabel, Loader, Text, useComputedColorScheme } from '@mantine/core'
import CodeMirror from '@uiw/react-codemirror'
import { useEffect, useState } from 'react'
import classes from './CustomTheme.module.css'
const CustomTheme = () => {
const [data, setData] = useState({
js: '',
css: '',
})
const [textSelect, setTextSelect] = useState('js')
const [loader, setLoader] = useState(true)
const [activeBtn, setActiveBtn] = useState(false)
const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: true,
})
const getCustomData = async () => {
try {
const res = await get(getCustomTheme)
if (res.data !== null) {
setData(res.data)
}
setLoader(false)
} catch (error) {
console.log(error)
}
}
const updateSetting = async () => {
try {
await update(updateCustomTheme, data, getCustomData)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getCustomData()
}, [])
return loader ? (
<Box ta={'center'}>
<Loader size={40} mt={'15%'} />
</Box>
) : (
<>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Custom Theme
</h3>
<Group>
<Button
bg={textSelect !== 'js' ? 'white' : '#5df59f'}
variant={textSelect !== 'js' ? 'outline' : 'filled'}
onClick={() => setTextSelect('js')}
>
JS
</Button>
<Button
bg={textSelect !== 'css' ? 'white' : '#5df59f'}
variant={textSelect !== 'css' ? 'outline' : 'filled'}
onClick={() => setTextSelect('css')}
>
CSS
</Button>
<Button
m={5}
disabled={activeBtn}
onClick={async () => {
setActiveBtn(true)
await updateSetting()
setActiveBtn(false)
}}
>
Save
</Button>
</Group>
</div>
<Box p={20}>
<div className={classes.box}>
<div style={{ display: textSelect === 'js' ? 'block' : 'none' }}>
<InputLabel p={5} size="md">
JavaScript
</InputLabel>
<CodeMirror
value={data.js ? data.js : ''}
height="100%"
className={classes.txtarea}
extensions={[javascript({ jsx: true })]}
theme={computedColorScheme === 'light' ? 'light' : 'dark'}
onChange={(e) => {
setData({ ...data, js: e })
}}
/>
</div>
<div style={{ display: textSelect === 'css' ? 'block' : 'none' }}>
<InputLabel p={5} size="md">
CSS
</InputLabel>
<CodeMirror
value={data.css ? data.css : ''}
height="100%"
className={classes.txtarea}
extensions={[javascript({ jsx: true })]}
theme={computedColorScheme === 'light' ? 'light' : 'dark'}
onChange={(e) => {
setData({ ...data, css: e })
}}
/>
</div>
</div>
</Box>
</>
)
}
export default CustomTheme

View File

@ -1,242 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { statisticRevenuesByMonth, statisticSearchSNByMonth } from '@/api/Admin'
import { get } from '@/rtk/helpers/apiService'
import { Box, Grid, Paper, Text } from '@mantine/core'
import { useEffect, useState } from 'react'
import {
Bar,
BarChart,
CartesianGrid,
ComposedChart,
Legend,
Rectangle,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts'
import classes from './Dashboard.module.css'
type TData = {
month: string | number
year: string | number
timezone_offset: string
}
const Chart = () => {
const [listStatisticSNKeywords, setListStatisticSNKeywords] = useState<any>(
[],
)
const [listStatisticRevenue, setListStatisticRevenue] = useState<any>([])
const [filterDataStatistic] = useState<TData>(() => {
const currentDate = new Date()
const currentMonth = currentDate.getMonth() + 1
const currentYear = currentDate.getFullYear()
const timezoneOffsetMinutes = currentDate.getTimezoneOffset()
const timezoneOffsetHours = Math.floor(Math.abs(timezoneOffsetMinutes) / 60)
const timezoneOffsetSign = timezoneOffsetMinutes > 0 ? '-' : '+'
const timezoneOffset = `${timezoneOffsetSign}${String(
timezoneOffsetHours,
).padStart(2, '0')}:${String(Math.abs(timezoneOffsetMinutes) % 60).padStart(
2,
'0',
)}`
return {
month: currentMonth,
year: currentYear,
timezone_offset: timezoneOffset,
}
})
const handleFetchDataStatisticSNKeywords = async () => {
try {
const res = await get(statisticSearchSNByMonth, filterDataStatistic)
if (res.status) {
const data = Object.keys(res.data)?.map((i) => ({
name: i,
count: res.data[i].count,
count_complete: res.data[i].count_complete,
count_pending: res.data[i].count_pending,
count_fail: res.data[i].count_fail,
}))
setListStatisticSNKeywords(data)
}
} catch (error) {
console.error(error)
}
}
const handleFetchDataStatisticRevenue = async () => {
try {
const res = await get(statisticRevenuesByMonth, filterDataStatistic)
if (res.status) {
const data = Object.keys(res.data)?.map((i) => ({
name: i,
total_price: parseFloat(res.data[i].total_price),
}))
setListStatisticRevenue(data)
}
} catch (error) {
console.error(error)
}
}
const customFormatter = (value: string) => {
const customNames = {
count: 'Total',
count_complete: 'Completed',
count_pending: 'Pending',
count_fail: 'Failed',
total_price: 'Total Price',
} as { [key: string]: string }
return customNames?.[value] || value
}
useEffect(() => {
handleFetchDataStatisticSNKeywords()
}, [])
useEffect(() => {
handleFetchDataStatisticRevenue()
}, [])
return (
<Box className={classes.chartbox}>
{/* Search SN Chart */}
<Grid>
<Grid.Col span={{ base: 12, xl: 6 }}>
<Paper withBorder radius="md" className={classes.paper}>
<Text p={10} className={classes.titleChart}>
Check SN Chart
</Text>
<ResponsiveContainer
width="100%"
height={400}
className={classes.chart}
>
<ComposedChart
height={400}
data={listStatisticSNKeywords}
margin={{
top: 5,
right: 30,
left: 0,
bottom: 5,
}}
>
<CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip
content={(props) => {
const { payload, label } = props
return (
<div className={classes.customTooltip}>
<p>{label}</p>
{payload?.map((entry, index) => (
<p key={index} style={{ color: entry.color }}>
{customFormatter(entry?.name as string)}:{' '}
{entry.value}
</p>
))}
<p
style={{
color: 'green',
}}
>
Complete:{' '}
{payload &&
payload[0] &&
payload[0].payload.count_complete}
</p>
<p
style={{
color: 'blue',
}}
>
Pending:{' '}
{payload &&
payload[0] &&
payload[0].payload.count_pending}
</p>
<p
style={{
color: 'red',
}}
>
Failed:{' '}
{payload &&
payload[0] &&
payload[0].payload.count_fail}
</p>
</div>
)
}}
/>
<Legend formatter={(value) => customFormatter(value)} />
<Bar dataKey="count" barSize={20} fill="#ff7300" />
</ComposedChart>
</ResponsiveContainer>
</Paper>
</Grid.Col>
<Grid.Col span={{ base: 12, xl: 6 }}>
{/* Revenue Chart */}
<Paper withBorder radius="md" className={classes.paper}>
<Text p={10} className={classes.titleChart}>
Revenues Chart
</Text>
<ResponsiveContainer
width="100%"
height={400}
className={classes.chart}
>
<BarChart
height={400}
data={listStatisticRevenue}
margin={{
top: 5,
right: 30,
left: 0,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip
content={(props) => {
const { payload, label } = props
return (
<div className={classes.customTooltip}>
<p>{label}</p>
{payload?.map((entry, index) => (
<p key={index} style={{ color: entry.color }}>
{customFormatter(entry?.name as string)}:{' $'}
{entry.value}
</p>
))}
</div>
)
}}
/>
<Legend formatter={(value) => customFormatter(value)} />
<Bar
dataKey="total_price"
fill="#8884d8"
activeBar={<Rectangle fill="#7dd3fc" stroke="blue" />}
/>
</BarChart>
</ResponsiveContainer>
</Paper>
</Grid.Col>
</Grid>
</Box>
)
}
export default Chart

View File

@ -1,90 +0,0 @@
.label {
font-family:
Greycliff CF,
var(--mantine-font-family);
}
.root {
padding: calc(var(--mantine-spacing-xl) * 1.5);
}
.value {
font-size: rem(48px);
font-weight: 700;
}
.icon {
color: white;
/* color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3)); */
}
.titleItemDashboard {
font-weight: 700;
text-transform: uppercase;
}
.chartBox {
display: flex;
width: 100%;
gap: 12px;
}
.tooltipChart {
background-color: white;
padding: 4px;
border-radius: 6px;
}
.cardDashboard {
border-radius: 12px;
overflow: hidden;
}
.buttonMoreInfo {
background-color: rgba(0, 0, 0, 0.4);
color: white;
padding: 6px 0
}
.buttonMoreInfo:hover {
background-color: rgba(0, 0, 0, 0.3);
cursor: pointer;
}
.paper {
width: 100%;
background-color: light-dark(var(white), var(--mantine-color-dark-7));
overflow: hidden;
}
.titleChart {
font-size: rem(24px);
font-weight: 700;
line-height: 1;
text-align: center;
text-transform: uppercase;
font-family:
Greycliff CF,
var(--mantine-font-family);
background-color: light-dark(var(white), var(--mantine-color-dark-7));
}
.iconMoreInfo {
color: white
}
.customTooltip {
background-color: #fff;
border: 1px solid #ccc;
padding: 8px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.chart {
background-color: light-dark(var(white), var(--mantine-color-dark-7));
}

View File

@ -1,133 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
const Dashboard = () => {
// type Tdata = {
// title: string
// value: number
// url: string
// icon: any
// }
// const navigate = useNavigate()
// const [dashboardData, setDashboardData] = useState<Tdata[]>([])
// const [loader, setLoader] = useState(true)
// const getDashboardInfo = async () => {
// try {
// const res = await get(getDashboard)
// const data = [
// {
// title: 'Discounts',
// value: res.data.discounts,
// url: '/discounts',
// icon: IconDiscount2,
// },
// {
// title: 'Orders',
// value: res.data.orders,
// url: '/order',
// icon: IconReceipt2,
// },
// {
// title: 'Clients',
// value: res.data.clients,
// url: '/client',
// icon: IconUserPlus,
// },
// {
// title: 'Contacts',
// value: res.data.contacts,
// url: '/contacts',
// icon: IconAddressBook,
// },
// ]
// setDashboardData(data)
// setLoader(false)
// } catch (error) {
// console.log(error)
// }
// }
// useEffect(() => {
// // Check if dashboardData is already populated
// if (dashboardData.length === 0) {
// getDashboardInfo()
// }
// }, [dashboardData])
// const items = dashboardData.map((item, index) => {
// const colors = ['#38bdf8', '#34d399', '#f59e0b', '#ef4444']
// const Icon = item.icon
// return (
// <Flex
// key={index}
// bg={colors[index % 100]}
// direction="column"
// justify="space-between"
// className={classes.cardDashboard}
// >
// <Group justify="space-between" p="md">
// <Text
// size="1.8rem"
// c="#FFFFFF"
// className={classes.titleItemDashboard}
// >
// {item.title}
// </Text>
// <Icon className={classes.icon} size="3.5rem" stroke={1.5} />
// </Group>
// <Center pb="md">
// <Text className={classes.value} c="#FFFFFF">
// {item.value}
// </Text>
// </Center>
// <Center
// className={classes.buttonMoreInfo}
// onClick={() => navigate(item.url)}
// >
// More info
// <IconArrowNarrowRight
// color="#FFFFFF"
// className={classes.iconMoreInfo}
// />
// </Center>
// </Flex>
// )
// })
return (
<div>
{/* {loader ? (
<Box ta={'center'}>
<Loader size={40} mt={'15%'} />
</Box>
) : (
<>
<Box>
<div>
<h3>
<Text>Admin/</Text>
Dashboard
</h3>
</div>
</Box>
<Grid>
<Grid.Col span={12}>
<SimpleGrid
className={classes.h100}
cols={{ base: 1, md: 2, lg: 4 }}
>
{items}
</SimpleGrid>
</Grid.Col>
<Grid.Col span={12}>
<Chart />
</Grid.Col>
</Grid>
</>
)} */}
Dashboard
</div>
)
}
export default Dashboard

View File

@ -1,49 +0,0 @@
.box {
border-radius: 5px;
}
.divImage {
display: flex;
justify-content: center;
justify-items: center;
align-items: center;
height: 100%;
background-color: light-dark(#f1f3f5, var(--mantine-color-dark-6));
}
.arrowIcon {
color: #00cf00;
}
.groupBox {
display: flex;
justify-content: space-evenly;
}
.groupBox > div {
width: 49%;
}
.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;
}
.boxFile {
cursor: pointer;
}
.fileInput {
border: none;
}
.labelTooltip {
font-size: 0.8rem;
color: white;
}

View File

@ -1,289 +0,0 @@
import {
clearCacheAPI,
getGeneralSetting,
updateGeneralSetting,
} from '@/api/Admin'
import RenderForm from '@/components/RenderForm/RenderForm'
import { update } from '@/rtk/helpers/CRUD'
import { get } from '@/rtk/helpers/apiService'
import { getAccessToken } from '@/rtk/localStorage'
import {
Box,
Button,
FileInput,
Group,
Image,
Loader,
Text,
Tooltip,
} from '@mantine/core'
import { useForm } from '@mantine/form'
import { notifications } from '@mantine/notifications'
import { IconArrowBigRightFilled, IconPhoto } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
import classes from './GeneralSetting.module.css'
import { generalSettingForm } from '@/variables/formFormat'
import { createInitialValue } from '@/utils/createInitialValue'
import { TGeneralSettingValues } from '@/variables/types'
const GeneralSetting = () => {
const formFormat = generalSettingForm
let initValue = createInitialValue(formFormat.format, {
favicon: '',
logo: '',
}) as TGeneralSettingValues
const form = useForm<TGeneralSettingValues>({
initialValues: initValue,
validate: (values) => ({
email:
values.email === ''
? 'Email is required'
: /^\S+@\S+$/.test(values.email)
? null
: 'Invalid email',
}),
})
const [oldImage, setOldImage] = useState({
favicon: '',
logo: '',
})
const [loader, setLoader] = useState(true)
const [activeBtn, setActiveBtn] = useState(false)
const getSetting = async () => {
try {
const res = await get(getGeneralSetting)
if (res.status && res.data !== null) {
Object.keys(res.data).forEach((key) => {
res.data[key] = res.data[key] === null ? '' : res.data[key]
})
form.setValues(res.data)
setOldImage({
...oldImage,
favicon: res.data.favicon,
logo: res.data.logo,
})
}
setLoader(false)
} catch (error) {
console.log(error)
}
}
const handleSubmit = form.onSubmit(async (values) => {
setActiveBtn(true)
try {
const formData = new FormData()
Object.keys(values).forEach((key) => {
const dynamicKey = key as keyof typeof values
const value = values[dynamicKey]!
if (!['favicon', 'logo'].includes(key)) {
formData.append(key, value)
}
})
if (values.favicon !== null && typeof values.favicon === 'object') {
formData.append('favicon', values.favicon)
}
if (values.logo !== null && typeof values.logo === 'object') {
formData.append('logo', values.logo)
}
const token = getAccessToken()
const headers = {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
}
await update(updateGeneralSetting, formData, getSetting, {
headers: headers,
})
setActiveBtn(false)
} catch (error) {
console.log(error)
}
})
const clearCache = async () => {
try {
const res = await get(clearCacheAPI)
if (res.status) {
notifications.show({
title: 'Success',
message: 'Success',
color: 'green',
})
}
if (res.status === false) {
notifications.show({
title: 'Error',
message: 'Clear cache fail',
color: 'red',
})
}
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getSetting()
}, [])
return loader ? (
<Box ta={'center'}>
<Loader size={40} mt={'15%'} />
</Box>
) : (
<>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>General Setting
</h3>
<Group>
<Button mt={'lg'} disabled={activeBtn} onClick={() => handleSubmit()}>
Save
</Button>
<Button
bg={'cyan'}
mt={'lg'}
disabled={activeBtn}
onClick={async () => {
setActiveBtn(true)
await clearCache()
setActiveBtn(false)
}}
>
Clear Cache
</Button>
</Group>
</div>
<form>
<RenderForm
column={formFormat.column}
form={form}
formFormat={formFormat.format}
/>
<Box className={classes.groupBox}>
<Box
className={classes.boxFile}
onClick={() => {
document.getElementById('faviconInput')?.click()
}}
>
<FileInput
leftSection={<IconPhoto />}
label="Favicon"
mt="md"
accept="image/*"
variant="filled"
onChange={(e) => {
form.setFieldValue('favicon', e)
}}
id="faviconInput"
/>
{/* Favicon display processing */}
<Tooltip
label={
<Text className={classes.labelTooltip}>Change favicon</Text>
}
offset={-20}
color="gray"
>
<div className={classes.divImage}>
{oldImage.favicon !== '' && (
<>
<Box ta={'center'}>
<Image src={oldImage.favicon} w={200} m={10}></Image>
</Box>
</>
)}
{form.values.favicon !== null &&
typeof form.values.favicon === 'object' && (
<>
<IconArrowBigRightFilled
className={classes.arrowIcon}
width={40}
height={50}
/>
<Box ta={'center'}>
<Image
src={URL.createObjectURL(form.values.favicon!)}
w={200}
m={10}
></Image>
</Box>
</>
)}
</div>
</Tooltip>
</Box>
<Box
className={classes.boxFile}
onClick={() => {
document.getElementById('logoInput')?.click()
}}
>
<FileInput
leftSection={<IconPhoto />}
label="Logo"
mt="md"
accept="image/*"
variant="filled"
onChange={(e) => {
form.setFieldValue('logo', e)
}}
className={'fileInput'}
id="logoInput"
/>
{/* Logo display processing */}
<Tooltip
label={<Text className={classes.labelTooltip}>Change logo</Text>}
offset={-20}
color="gray"
>
<div className={classes.divImage}>
{oldImage.logo !== '' && (
<>
<Box ta={'center'}>
<Image src={oldImage.logo} w={200} m={10}></Image>
</Box>
</>
)}
{form.values.logo !== null &&
typeof form.values.logo === 'object' && (
<>
<IconArrowBigRightFilled
className={classes.arrowIcon}
width={40}
height={50}
/>
<Box>
<Image
src={URL.createObjectURL(form.values.logo!)}
w={200}
m={10}
></Image>
</Box>
</>
)}
</div>
</Tooltip>
</Box>
</Box>
</form>
</>
)
}
export default GeneralSetting

View File

@ -14,15 +14,15 @@
justify-content: space-evenly;
}
.editIcon {
color: rgb(9, 132, 132);
.deleteIcon {
color: red;
cursor: pointer;
padding: 2px;
border-radius: 25%;
}
.deleteIcon {
color: red;
.editIcon {
color: rgb(9, 132, 132);
cursor: pointer;
padding: 2px;
border-radius: 25%;

View File

@ -0,0 +1,317 @@
import { createOrUpdateUser, deleteUser, getAllUsers } from '@/api/Admin'
import { ButtonCopy } from '@/components/CopyClipboard/CopyClipboard'
import DataTableAll from '@/components/DataTable/DataTable'
import { get, post } from '@/rtk/helpers/apiService'
import { update, Xdelete } from '@/rtk/helpers/CRUD'
import { TUser } from '@/variables/types'
import {
Badge,
Box,
Button,
Code,
Dialog,
Group,
Modal,
MultiSelect,
Text,
TextInput
} from '@mantine/core'
import { useForm } from '@mantine/form'
import { IconEdit, IconX } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
import classes from './UsersManagement.module.css'
const UsersManagement = () => {
const [users, setUsers] = useState<TUser[]>([])
const [action, setAction] = useState('')
const [activeBtn, setActiveBtn] = useState(false)
const [item, setItem] = useState({ id: 0 })
const [disableBtn, setDisableBtn] = useState(false)
const [info, setInfo] = useState('')
const columns = [
{
name: 'id',
size: '3%',
header: 'ID',
},
{
name: 'name',
size: '17%',
header: 'Name',
},
{
name: 'email',
size: '26%',
header: 'Email',
},
{
name: 'permission',
size: '10%',
header: 'Permission',
render: (row: TUser) => {
if (row.permission.includes(',')) {
return row?.permission?.split(',').map((p) => {
return <Badge mr={'sm'}>{p}</Badge>
})
} else {
return <Badge>{row.permission}</Badge>
}
},
},
{
name: '#',
size: '5%',
header: 'Action',
render: (row: TUser) => {
return (
<Box className={classes.optionIcon}>
<IconEdit
className={classes.editIcon}
onClick={() => {
setAction('edit')
form.setValues(row)
}}
width={20}
height={20}
/>
<IconX
className={classes.deleteIcon}
onClick={() => {
setAction('delete')
setItem(row)
}}
width={20}
height={20}
/>
</Box>
)
},
},
]
const form = useForm({
initialValues: {
id: 0,
name: '',
email: '',
permission: '',
},
})
const getAll = async () => {
try {
const res = await get(getAllUsers)
if (res.status) {
setUsers(res.data)
}
} catch (error) {
console.log(error)
}
}
const handleCreate = async (values: TUser) => {
try {
const { id, ...data } = values
const res = await post(createOrUpdateUser, data)
if (res.status === true) {
setAction('review')
form.reset()
getAll()
setInfo(JSON.stringify(res.data, null, 2))
}
} catch (error) {
console.log(error)
}
}
const handleUpdate = async (values: TUser) => {
try {
const res = await update(createOrUpdateUser, values, getAll)
if (res === true) {
setAction('')
form.reset()
}
} catch (error) {
console.log(error)
}
}
const handleDelete = async (id: number) => {
try {
await Xdelete(deleteUser, { id: id }, getAll)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getAll()
}, [])
return (
<div>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Users Management
</h3>
<Button
onClick={() => {
setAction('add')
form.reset()
}}
>
+ Add
</Button>
</div>
<Box mt={'md'}>
<DataTableAll data={users} columns={columns} size="" searchInput />
</Box>
{/* Add/Edit User modal */}
<Modal
opened={action === 'add' || action === 'edit'}
onClose={() => {
setAction('')
form.reset()
}}
title={
<Text pl={'sm'} fw={700} fz={'lg'}>
{action === 'add' ? 'Add User' : 'Update User'}
</Text>
}
>
<form
onSubmit={form.onSubmit(async (values) => {
setDisableBtn(true)
action === 'edit'
? await handleUpdate(values)
: await handleCreate(values)
setDisableBtn(false)
})}
>
<Box pl={'md'} pr={'md'}>
<TextInput
label="Name"
mb={'md'}
value={form.values.name}
error={form.errors.name}
onChange={(e) => form.setFieldValue('name', e.target.value)}
/>
<TextInput
label="Email"
mb={'md'}
value={form.values.email}
error={form.errors.email}
onChange={(e) => form.setFieldValue('email', e.target.value)}
/>
<MultiSelect
label={'Permission(s)'}
required
data={['staff', 'admin', 'hr']}
value={
typeof form.values.permission === 'string'
? form.values.permission
.split(',')
.filter((p) => p.trim() !== '')
: form.values.permission
}
error={form.errors.permisstion}
onChange={(e) =>
form.setFieldValue(
'permission',
e!.filter((p) => p.trim() !== '').join(','),
)
}
/>
<Box ta={'center'}>
{action === 'add' ? (
<Button
mt={'lg'}
bg={'green'}
type="submit"
disabled={disableBtn}
>
Create
</Button>
) : (
<Button
mt={'lg'}
bg={'green'}
type="submit"
disabled={disableBtn}
>
Save
</Button>
)}
</Box>
</Box>
</form>
</Modal>
<Modal
opened={action === 'review'}
onClose={() => {
setAction('')
}}
size={'lg'}
title={
<Text pl={'sm'} fw={700} fz={'lg'}>
Information to get started
</Text>
}
>
<Text c={'red'} fz={'sm'} fw={700}>
!! Important note: Please remind user to change password after logging
in !!
</Text>
<Code block>
<Box ta={'right'}>
<ButtonCopy value={info} />
</Box>
{info}
</Code>
</Modal>
<Dialog
className={classes.dialog}
opened={action === 'delete'}
withCloseButton
onClose={() => setAction('')}
size="lg"
radius="md"
position={{ top: 30, right: 10 }}
>
<Text className={classes.dialogText} size="sm" mb="xs" fw={500}>
Do you want to delete this user?
<Group justify="center" m={10}>
<Button
disabled={activeBtn}
fw={700}
size="xs"
variant="light"
onClick={async () => {
setActiveBtn(true)
await handleDelete(item.id)
setActiveBtn(false)
setAction('')
}}
>
Yes
</Button>
<Button
fw={700}
size="xs"
variant="light"
onClick={() => setAction('')}
>
Cancel
</Button>
</Group>
</Text>
</Dialog>
</div>
)
}
export default UsersManagement

View File

@ -3,15 +3,13 @@ import ResetPassword from '@/components/Authentication/ResetPassword'
import BasePage from '@/components/BasePage/BasePage'
import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
import PageLogin from '@/pages/Auth/Login/Login'
import CustomTheme from '@/pages/CustomTheme/CustomTheme'
import Dashboard from '@/pages/Dashboard/Dashboard'
import GeneralSetting from '@/pages/GeneralSetting/GeneralSetting'
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
import PageNotFound from '@/pages/NotFound/NotFound'
import Tickets from '@/pages/Tickets/Tickets'
import TicketsManagement from '@/pages/TicketsManagement/TicketsManagement'
import Timekeeping from '@/pages/Timekeeping/Timekeeping'
import Tracking from '@/pages/Tracking/Tracking'
import UsersManagement from '@/pages/UsersManagement/UsersManagement'
import PageWelcome from '@/pages/Welcome/Welcome'
import Worklogs from '@/pages/Worklogs/Worklogs'
import { Navigate } from 'react-router-dom'
@ -34,48 +32,6 @@ const mainRoutes = [
</ProtectedRoute>
),
},
{
path: '/dashboard',
element: (
<ProtectedRoute mode="route" permission="staff">
<BasePage
main={
<>
<Dashboard />
</>
}
></BasePage>
</ProtectedRoute>
),
},
{
path: '/general-setting',
element: (
<ProtectedRoute mode="home" permission="staff">
<BasePage
main={
<>
<GeneralSetting />
</>
}
></BasePage>
</ProtectedRoute>
),
},
{
path: '/custom-theme',
element: (
<ProtectedRoute mode="home" permission="staff">
<BasePage
main={
<>
<CustomTheme />
</>
}
></BasePage>
</ProtectedRoute>
),
},
{
path: '/tracking',
element: (
@ -174,6 +130,20 @@ const mainRoutes = [
</ProtectedRoute>
),
},
{
path: '/users',
element: (
<ProtectedRoute mode="route" permission="admin,hr">
<BasePage
main={
<>
<UsersManagement/>
</>
}
></BasePage>
</ProtectedRoute>
),
},
// {
// path: '/packages',
// element: (

View File

@ -69,4 +69,11 @@ export type TOrderValues = {
package_name?: string
package_price?: string
discounted_package_price?: string
}
export type TUser = {
id: number;
email: string;
name: string;
permission: string
}