create function user manament
This commit is contained in:
parent
8f6092be62
commit
bb3c9b2106
|
|
@ -3,16 +3,108 @@
|
||||||
namespace Modules\Auth\app\Http\Controllers;
|
namespace Modules\Auth\app\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
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 Modules\Auth\app\Models\User;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
|
use IsAPI;
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->middleware('jwt.auth');
|
$this->middleware('jwt.auth');
|
||||||
}
|
}
|
||||||
public function users()
|
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']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ use Tymon\JWTAuth\Facades\JWTAuth;
|
||||||
class User extends Authenticatable implements JWTSubject
|
class User extends Authenticatable implements JWTSubject
|
||||||
{
|
{
|
||||||
use HasApiTokens, HasFactory, Notifiable;
|
use HasApiTokens, HasFactory, Notifiable;
|
||||||
use HasRoles;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|
@ -26,6 +25,7 @@ class User extends Authenticatable implements JWTSubject
|
||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
|
'permission'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ Route::middleware(['api'])
|
||||||
->prefix('v1')
|
->prefix('v1')
|
||||||
->name('api.v1.')
|
->name('api.v1.')
|
||||||
->group(function () {
|
->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::post('login', [LoginController::class, 'login']);
|
||||||
Route::middleware(['jwt.auth'])->post('logout', [LoginController::class, 'logout']);
|
Route::middleware(['jwt.auth'])->post('logout', [LoginController::class, 'logout']);
|
||||||
Route::middleware(['jwt.auth'])->post('change-password', [AuthController::class, 'updatePassword']);
|
Route::middleware(['jwt.auth'])->post('change-password', [AuthController::class, 'updatePassword']);
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,6 @@
|
||||||
const API_URL = import.meta.env.VITE_BACKEND_URL + 'api/'
|
const API_URL = import.meta.env.VITE_BACKEND_URL + 'api/'
|
||||||
|
|
||||||
export const getDetailUser = API_URL + 'v1/admin/detail'
|
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
|
// Tracking
|
||||||
export const getListTracking = API_URL + 'v1/admin/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 getTicketsOfUser = API_URL + 'v1/admin/ticket/getByUserId'
|
||||||
export const deleteTicket = API_URL + 'v1/admin/ticket/delete'
|
export const deleteTicket = API_URL + 'v1/admin/ticket/delete'
|
||||||
export const addTicket = API_URL + 'v1/admin/ticket/create'
|
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'
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
IconSun,
|
IconSun,
|
||||||
IconTicket,
|
IconTicket,
|
||||||
IconDevices,
|
IconDevices,
|
||||||
|
IconUsersGroup,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
@ -70,6 +71,12 @@ const data = [
|
||||||
icon: IconDevices,
|
icon: IconDevices,
|
||||||
group: 'admin',
|
group: 'admin',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
link: '/users',
|
||||||
|
label: 'Users Management',
|
||||||
|
icon: IconUsersGroup,
|
||||||
|
group: 'admin',
|
||||||
|
}
|
||||||
// { link: '/jira', label: 'Jira', icon: IconSubtask },
|
// { link: '/jira', label: 'Jira', icon: IconSubtask },
|
||||||
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
|
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
|
||||||
// { link: '/general-setting', label: 'General Setting', icon: IconSettings },
|
// { link: '/general-setting', label: 'General Setting', icon: IconSettings },
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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%;
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
8
FRONTEND/src/pages/Banner/Banner.module.css → FRONTEND/src/pages/UsersManagement/UsersManagement.module.css
Executable file → Normal file
8
FRONTEND/src/pages/Banner/Banner.module.css → FRONTEND/src/pages/UsersManagement/UsersManagement.module.css
Executable file → Normal file
|
|
@ -14,15 +14,15 @@
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editIcon {
|
.deleteIcon {
|
||||||
color: rgb(9, 132, 132);
|
color: red;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: 25%;
|
border-radius: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleteIcon {
|
.editIcon {
|
||||||
color: red;
|
color: rgb(9, 132, 132);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: 25%;
|
border-radius: 25%;
|
||||||
|
|
@ -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
|
||||||
|
|
@ -3,15 +3,13 @@ import ResetPassword from '@/components/Authentication/ResetPassword'
|
||||||
import BasePage from '@/components/BasePage/BasePage'
|
import BasePage from '@/components/BasePage/BasePage'
|
||||||
import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
|
import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
|
||||||
import PageLogin from '@/pages/Auth/Login/Login'
|
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 LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
|
||||||
import PageNotFound from '@/pages/NotFound/NotFound'
|
import PageNotFound from '@/pages/NotFound/NotFound'
|
||||||
import Tickets from '@/pages/Tickets/Tickets'
|
import Tickets from '@/pages/Tickets/Tickets'
|
||||||
import TicketsManagement from '@/pages/TicketsManagement/TicketsManagement'
|
import TicketsManagement from '@/pages/TicketsManagement/TicketsManagement'
|
||||||
import Timekeeping from '@/pages/Timekeeping/Timekeeping'
|
import Timekeeping from '@/pages/Timekeeping/Timekeeping'
|
||||||
import Tracking from '@/pages/Tracking/Tracking'
|
import Tracking from '@/pages/Tracking/Tracking'
|
||||||
|
import UsersManagement from '@/pages/UsersManagement/UsersManagement'
|
||||||
import PageWelcome from '@/pages/Welcome/Welcome'
|
import PageWelcome from '@/pages/Welcome/Welcome'
|
||||||
import Worklogs from '@/pages/Worklogs/Worklogs'
|
import Worklogs from '@/pages/Worklogs/Worklogs'
|
||||||
import { Navigate } from 'react-router-dom'
|
import { Navigate } from 'react-router-dom'
|
||||||
|
|
@ -34,48 +32,6 @@ const mainRoutes = [
|
||||||
</ProtectedRoute>
|
</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',
|
path: '/tracking',
|
||||||
element: (
|
element: (
|
||||||
|
|
@ -174,6 +130,20 @@ const mainRoutes = [
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
element: (
|
||||||
|
<ProtectedRoute mode="route" permission="admin,hr">
|
||||||
|
<BasePage
|
||||||
|
main={
|
||||||
|
<>
|
||||||
|
<UsersManagement/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
></BasePage>
|
||||||
|
</ProtectedRoute>
|
||||||
|
),
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// path: '/packages',
|
// path: '/packages',
|
||||||
// element: (
|
// element: (
|
||||||
|
|
|
||||||
|
|
@ -69,4 +69,11 @@ export type TOrderValues = {
|
||||||
package_name?: string
|
package_name?: string
|
||||||
package_price?: string
|
package_price?: string
|
||||||
discounted_package_price?: string
|
discounted_package_price?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TUser = {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
permission: string
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue