Technical: add Profile page, Organization Settings page

This commit is contained in:
dbdbd9 2024-09-21 10:19:04 +07:00
parent 5bc84a10b8
commit bb40261a0d
6 changed files with 643 additions and 16 deletions

View File

@ -76,9 +76,17 @@ export const getProfilesData = API_URL + 'v1/admin/criterias/profiles-data'
export const updateProfilesData =
API_URL + 'v1/admin/criterias/profiles-data/update'
export const listUserTechnical = API_URL + 'v1/admin/technical/get-tech-of-user'
export const updateUserTechnical = API_URL + 'v1/admin/technical/technicals-user/update'
export const getAllUser = API_URL + 'v1/admin/technical/get-all-user'
export const getAllTechByUserId =
API_URL + 'v1/admin/technical/get-tech-by-user-id'
export const evaluation = API_URL + 'v1/admin/evaluation/report'
export const sprintReview = API_URL + 'v1/admin/evaluation/sprint-review'
//Technical
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
export const createTechnical = API_URL + 'v1/admin/technical/create'
export const deleteTechnical = API_URL + 'v1/admin/technical/delete'

View File

@ -35,10 +35,11 @@ import {
IconQrcode,
IconReport,
IconScan,
IconSettings,
IconSun,
IconTicket,
IconUsersGroup,
IconZoomExclamation
IconZoomExclamation,
} from '@tabler/icons-react'
import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
@ -114,6 +115,12 @@ const data = [
icon: IconChartDots2,
group: 'admin',
},
{
link: '/organization-settings',
label: 'Organization Settings',
icon: IconSettings,
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

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

View File

@ -0,0 +1,322 @@
import { useEffect, useState } from 'react'
import {
Box,
Button,
Dialog,
Flex,
Group,
Loader,
Modal,
Tabs,
Text,
TextInput,
} from '@mantine/core'
import classes from './OrganizationSettings.module.css'
import DataTableAll from '@/components/DataTable/DataTable'
import { get, post } from '@/rtk/helpers/apiService'
import { notifications } from '@mantine/notifications'
import { createTechnical, deleteTechnical, listTechnical } from '@/api/Admin'
import { useForm } from '@mantine/form'
import { Xdelete } from '@/rtk/helpers/CRUD'
import moment from 'moment'
interface DataTechnical {
id: number
name: string
level: number
point: number
updated_at: string
}
function OrganizationSettings() {
const [activeTab, setActiveTab] = useState<string | null>('technical')
const [isLoading, setIsLoading] = useState(false)
const [addTechnicalOpen, setAddTechnicalOpen] = useState(false)
const [deleteTechnicalOpen, setDeleteTechnicalOpen] = useState(false)
const [selectedId, setSelectedId] = useState(0)
const [dataTechnical, setDataTechnical] = useState<DataTechnical[]>([])
const form = useForm({
initialValues: {
name: '',
level: '',
},
})
useEffect(() => {
const fetchData = async () => {
await getListTechnical()
}
fetchData()
}, [])
const columns = [
{
name: 'level',
size: '5%',
header: 'Level',
render: (row: any) => {
return (
<Box
style={
row?.level
? row?.level === 1
? { backgroundColor: '#d9d2e9' }
: row?.level === 2
? { backgroundColor: '#ffd966' }
: { backgroundColor: '#cfe2f3' }
: { backgroundColor: '' }
}
fw={500}
ta="center"
p={4}
>
{row?.level ? row.level : ''}
</Box>
)
},
},
{
name: 'name',
size: '40%',
header: 'Name',
},
{
name: 'created_at',
size: '40%',
header: 'Created at',
render: (row: any) => {
return (
<Box>{moment(row?.created_at).format('HH:mm:ss DD/MM/YYYY')}</Box>
)
},
},
{
name: 'action',
size: '15%',
header: 'Action',
render: (row: any) => {
return (
<Flex justify="center">
<Button
color="red"
onClick={() => {
setDeleteTechnicalOpen(true)
setSelectedId(row?.id)
}}
>
Delete
</Button>
</Flex>
)
},
},
]
const getListTechnical = async () => {
try {
setIsLoading(true)
const params = {}
const res = await get(listTechnical, params)
if (res.status) {
setDataTechnical(res.data)
}
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
} finally {
setIsLoading(false)
}
return []
}
const handleCreate = async (values: any) => {
try {
const { id, ...data } = values
const res = await post(createTechnical, data)
if (res.status === true) {
setAddTechnicalOpen(false)
form.reset()
await getListTechnical()
notifications.show({
title: 'Success',
message: res.message,
color: 'green',
})
}
} catch (error) {
console.log(error)
}
}
const handleDelete = async (id: number) => {
try {
await Xdelete(deleteTechnical, { id: id }, getListTechnical)
setSelectedId(0)
setDeleteTechnicalOpen(false)
} catch (error) {
console.log(error)
}
}
return (
<div>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Staff Evaluation
</h3>
</div>
<Box w="100%" display={'flex'} mt={15} ml={10}>
<Tabs w="100%" value={activeTab} onChange={setActiveTab}>
<Tabs.List>
<Tabs.Tab value="technical">Technical setting</Tabs.Tab>
<Tabs.Tab value="second">Setting 2</Tabs.Tab>
<Tabs.Tab value="third">Setting 3</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="technical" pt="xs">
<Box mt={'md'} p={16}>
<Text fw={500}>Note: </Text>
<Flex gap={8}>
<Box p={8} bg="#d9d2e9">
<span style={{ fontWeight: 500 }}>Level 1:</span> 3-12 Month
</Box>
<Box p={8} bg="#ffd966">
<span style={{ fontWeight: 500 }}>Level 2:</span> 3-5 Year
</Box>
<Box p={8} bg="#cfe2f3">
<span style={{ fontWeight: 500 }}>Level 3:</span> 5 -8 Year
</Box>
</Flex>
</Box>
<Box mt={'md'}>
{isLoading ? (
<Box
style={{
marginTop: '10%',
textAlign: 'center',
display: 'block',
}}
>
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
<Text fw={600} c={'gray'} mt={8}>
Loading Technical...
</Text>
</Box>
) : (
<DataTableAll
data={dataTechnical}
columns={columns}
size=""
searchInput
infoTotal={
<Button onClick={() => setAddTechnicalOpen(true)}>
+ Add
</Button>
}
/>
)}
</Box>
</Tabs.Panel>
<Tabs.Panel value="second" pt="xs">
Setting 2
</Tabs.Panel>
<Tabs.Panel value="third" pt="xs">
Setting 3
</Tabs.Panel>
</Tabs>
</Box>
<Modal
opened={addTechnicalOpen}
onClose={() => {
setAddTechnicalOpen(false)
form.reset()
}}
title={
<Text pl={'sm'} fw={700} fz={'lg'}>
Add Technical
</Text>
}
>
<form
onSubmit={form.onSubmit(async (values) => {
await handleCreate(values)
})}
>
<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="Level"
mb={'md'}
value={form.values.level}
error={form.errors.level}
onChange={(e) => form.setFieldValue('level', e.target.value)}
/>
<Box ta={'center'}>
<Button mt={'lg'} bg={'green'} type="submit">
Create
</Button>
</Box>
</Box>
</form>
</Modal>
<Dialog
opened={deleteTechnicalOpen}
className={classes.dialog}
withCloseButton
onClose={() => setDeleteTechnicalOpen(false)}
size="lg"
radius="md"
position={{ top: 30, right: 10 }}
>
<Box className={classes.dialogText} size="sm" mb="xs" fw={500}>
Do you want to delete this technical?
<Group justify="center" m={10}>
<Button
fw={700}
size="xs"
variant="filled"
color="red"
onClick={async () => handleDelete(selectedId)}
>
Yes
</Button>
<Button
fw={700}
size="xs"
variant="filled"
color="gray"
onClick={() => setDeleteTechnicalOpen(false)}
>
Cancel
</Button>
</Group>
</Box>
</Dialog>
</div>
)
}
export default OrganizationSettings

View File

@ -1,4 +1,9 @@
import { getProfilesData, updateProfilesData } from '@/api/Admin'
import {
getProfilesData,
listUserTechnical,
updateProfilesData,
updateUserTechnical,
} from '@/api/Admin'
import { changePassword } from '@/api/Auth'
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
@ -12,6 +17,8 @@ import {
Avatar,
Box,
Button,
Flex,
Loader,
Modal,
PasswordInput,
Text,
@ -19,12 +26,32 @@ import {
Title,
} from '@mantine/core'
import { notifications } from '@mantine/notifications'
import { IconPasswordUser } from '@tabler/icons-react'
import {
IconPasswordUser,
IconUserCode,
IconUserCog,
} from '@tabler/icons-react'
import { useCallback, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import classes from './Profile.module.css'
import DataTableAll from '@/components/DataTable/DataTable'
import moment from 'moment'
const isCompactMenu = false
interface DataTechnical {
id: number
name: string
level: number
point: number
updated_at: string
}
interface UpdateDataTechnical {
technical_id: number | string
point: number | string
}
const Profile = () => {
const user = useAppSelector((state) => state.authentication.user)
const userData = getUser()
@ -35,11 +62,22 @@ const Profile = () => {
new_password: '',
confirm_password: '',
})
const [swapTap, setSwapTab] = useState(false)
const [avatar, setAvatar] = useState(user.user.avatar)
const [loading, setLoading] = useState(false)
const [dataProfile, setDataProfile] = useState<any>([])
const [countSpam, setCountSpam] = useState(0)
const [dataTechnical, setDataTechnical] = useState<DataTechnical[]>([])
const [loadingTechnical, setLoadingTechnical] = useState(false)
const [isUpdateTechnical, setIsUpdateTechnical] = useState(false)
const [updatedDataTechnical, setUpdatedDataTechnical] = useState<
UpdateDataTechnical[]
>([])
const [selectedAvatar, setSelectedAvatar] = useState<string | null>(null)
const navigate = useNavigate()
const dispatch = useAppDispatch()
@ -183,6 +221,142 @@ const Profile = () => {
dispatch(logout(navigate))
}, [dispatch, navigate])
useEffect(() => {
const fetchData = async () => {
await getListUserTechnical()
}
fetchData()
}, [])
const columns = [
{
name: 'level',
size: '5%',
header: 'Level',
render: (row: any) => {
return (
<Box
style={
row?.level
? row?.level === 1
? { backgroundColor: '#d9d2e9' }
: row?.level === 2
? { backgroundColor: '#ffd966' }
: { backgroundColor: '#cfe2f3' }
: { backgroundColor: '' }
}
fw={500}
ta="center"
p={4}
>
{row?.level ? row.level : ''}
</Box>
)
},
},
{
name: 'name',
size: '50%',
header: 'Name',
render: (row: any) => {
return <Text ta="start">{row?.name}</Text>
},
},
{
name: 'point',
size: '5%',
header: 'Point',
render: (row: any) => {
return (
<TextInput
mb={'md'}
value={
updatedDataTechnical.find(
(technicalItem: UpdateDataTechnical) =>
technicalItem.technical_id === row?.id,
)?.point
}
onChange={(e) => {
setUpdatedDataTechnical((prev: any) =>
prev.map((technicalItem: UpdateDataTechnical) => {
if (technicalItem.technical_id === row.id) {
return {
...technicalItem,
point: e.target.value,
}
}
return technicalItem
}),
)
setIsUpdateTechnical(true)
}}
/>
)
},
},
{
name: 'updated_at',
size: '40%',
header: 'Last update',
render: (row: any) => {
return (
<Text ta="start">
{moment(row?.updated_at).format('HH:mm:ss DD/MM/YYYY')}
</Text>
)
},
},
]
const getListUserTechnical = async () => {
try {
setLoadingTechnical(true)
const params = {}
const res = await get(listUserTechnical, params)
if (res.status) {
setDataTechnical(res.data)
setUpdatedDataTechnical(
res.data?.map((technicalItem: DataTechnical) => {
return {
technical_id: technicalItem.id,
point: technicalItem.point,
}
}),
)
}
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
} finally {
setLoadingTechnical(false)
}
return []
}
const handleUpdateTechnical = async () => {
try {
const res = await post(updateUserTechnical, {
technicals: updatedDataTechnical,
})
if (res.status === true) {
await getListUserTechnical()
notifications.show({
title: 'Success',
message: res.message,
color: 'green',
})
}
} catch (error) {
console.log(error)
}
}
return (
<div>
<div className={classes.title}>
@ -281,28 +455,119 @@ const Profile = () => {
display: 'flex',
flexFlow: 'column',
alignItems: 'center',
gap: 8,
}}
mt={10}
>
<a
href="#"
className={classes.link}
<Button
style={{ width: '50%' }}
onClick={() => setOpened(true)}
color="green"
>
<IconPasswordUser className={classes.linkIcon} stroke={1.5} />
<span
className={`${classes.itemLabel} ${classes.labelCompactMenu}`}
<IconPasswordUser stroke={1.5} />
Change password
</Button>
{swapTap ? (
<Button
style={{ width: '50%' }}
onClick={() => setSwapTab(!swapTap)}
>
Change password
</span>
</a>
<IconUserCode stroke={1.5} />
Project Involved
</Button>
) : (
<Button
style={{ width: '50%' }}
onClick={() => setSwapTab(!swapTap)}
>
<IconUserCog stroke={1.5} />
Your Technical
</Button>
)}
</Box>
</Box>
<Box className={classes.projectInvolvement}>
<Title order={3}>Project Involved</Title>
<ProjectInvolvement dataProfile={dataProfile} page="profile" />
</Box>
{swapTap ? (
<Box className={classes.projectInvolvement}>
<Flex justify="space-between" wrap="wrap">
<Box px={16} ta="start">
<Text fw={500}>Level: </Text>
<Flex gap={8}>
<Box p={8} bg="#d9d2e9">
<span style={{ fontWeight: 500 }}>1:</span> 3-12 Month
</Box>
<Box p={8} bg="#ffd966">
<span style={{ fontWeight: 500 }}>2:</span> 3-5 Year
</Box>
<Box p={8} bg="#cfe2f3">
<span style={{ fontWeight: 500 }}>3:</span> 5 -8 Year
</Box>
</Flex>
</Box>
<Box px={16} ta="start">
<Text fw={500}>Point: </Text>
<Flex gap={8}>
<Box p={8} bg="#FFEA00">
<span style={{ fontWeight: 500 }}>0:</span> Unknown
</Box>
<Box p={8} bg="#FFEA00">
<span style={{ fontWeight: 500 }}>1:</span> Basic
</Box>
<Box p={8} bg="#FFEA00">
<span style={{ fontWeight: 500 }}>2:</span> Advanced
</Box>
<Box p={8} bg="#FFEA00">
<span style={{ fontWeight: 500 }}>3:</span> Master
</Box>
</Flex>
</Box>
</Flex>
<Flex justify="space-between" mt={'lg'}>
<div></div>
<Title order={3}>Technicals</Title>
<Button
disabled={loadingTechnical || !isUpdateTechnical}
onClick={handleUpdateTechnical}
>
Update
</Button>
</Flex>
{loadingTechnical ? (
<Box
style={{
width: '100%',
marginTop: '10%',
textAlign: 'center',
display: 'block',
}}
>
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
<Text fw={600} c={'gray'} mt={8}>
Loading Technical...
</Text>
</Box>
) : (
<DataTableAll
data={dataTechnical}
columns={columns}
size=""
searchInput
/>
)}
</Box>
) : (
<Box className={classes.projectInvolvement}>
<Title order={3}>Project Involved</Title>
<ProjectInvolvement dataProfile={dataProfile} page="profile" />
</Box>
)}
</Box>
<Modal

View File

@ -6,6 +6,7 @@ import Allocation from '@/pages/Allocation/Allocation'
import PageLogin from '@/pages/Auth/Login/Login'
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
import PageNotFound from '@/pages/NotFound/NotFound'
import OrganizationSettings from '@/pages/OrganizationSettings/OrganizationSettings'
import Profile from '@/pages/Profile/Profile'
import SprintReview from '@/pages/SprintReview/SprintReview'
import StaffEvaluation from '@/pages/StaffEvaluation/StaffEvaluation'
@ -219,6 +220,20 @@ const mainRoutes = [
</ProtectedRoute>
),
},
{
path: '/organization-settings',
element: (
<ProtectedRoute mode="route" permission="admin">
<BasePage
main={
<>
<OrganizationSettings />
</>
}
></BasePage>
</ProtectedRoute>
),
},
// {
// path: '/packages',
// element: (