Thực hiện page staff evaluation

This commit is contained in:
Truong Vo 2024-09-20 11:50:53 +07:00
parent 4daa37ec9f
commit 6d4190d8a2
8 changed files with 719 additions and 122 deletions

View File

@ -83,6 +83,7 @@ export const DataTableAll = ({
searchInput,
checkBox,
size,
infoTotal,
}: {
data: any[]
columns: Column[]
@ -90,6 +91,7 @@ export const DataTableAll = ({
searchInput?: boolean
checkBox?: boolean
size: string
infoTotal?: React.ReactNode // Set the type to ReactNode to allow JSX elements
}) => {
const [Tdata, setTData] = useState<any[]>(data)
// const [tempData, setTempData] = useState<any[]>([])
@ -323,7 +325,14 @@ export const DataTableAll = ({
</Text>
)}
</Box>
<Box
className={classes.paginationBox}
display={infoTotal ? 'flex' : 'none'}
>
<Text fz={'sm'} ta={'right'}>
{infoTotal}
</Text>
</Box>
<Box
className={classes.paginationBox}
display={pagination ? 'flex' : 'none'}
@ -573,7 +582,7 @@ export const DataTablePagination = ({
)
})
const removeParam = (name:string) => {
const removeParam = (name: string) => {
// Create a URL object
let url = new URL(window.location.href)
@ -682,7 +691,8 @@ export const DataTablePagination = ({
search: urlParams.toString(),
})
}
statusSort.status === 'clear' && removeParam(`order_by_${statusSort.name}`)
statusSort.status === 'clear' &&
removeParam(`order_by_${statusSort.name}`)
} catch (error) {
console.warn(error)
}

View File

@ -24,6 +24,7 @@ import {
IconBinaryTree2,
IconCalendar,
IconCalendarClock,
IconChartDots2,
IconDevices,
IconLayoutSidebarLeftExpand,
IconLayoutSidebarRightExpand,
@ -37,7 +38,7 @@ import {
IconSun,
IconTicket,
IconUsersGroup,
IconZoomExclamation,
IconZoomExclamation
} from '@tabler/icons-react'
import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
@ -107,6 +108,12 @@ const data = [
icon: IconZoomExclamation,
group: 'hidden',
},
{
link: '/staff-avaluation',
label: 'Staff evaluation',
icon: IconChartDots2,
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,58 @@
.projectHeader {
display: flex;
align-items: center;
cursor: pointer;
padding: 5px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 4px;
margin-top: 10px;
background-color: #457bad;
color: white;
transition: background-color 0.3s;
}
.projectHeader:hover {
background-color: var(--hover-background);
color: #2d353c;
}
.sprintList {
padding-left: 20px; /* Indentation for sprints under the project */
}
.sprintHeader2 {
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.1);
width: 100%;
padding-left: 5px;
padding-top: 5px;
padding-bottom: 5px;
margin-left: 20px;
align-items: center;
cursor: pointer;
background-color: rgba(162, 162, 162, 0.1);
}
.sprintHeader {
margin-top: 5px;
display: flex;
align-items: center;
cursor: pointer;
padding-left: 5px;
padding-top: 5px;
padding-bottom: 5px;
/* margin-left: 20px; */
transition: background-color 0.3s;
}
.sprintHeader2:hover {
background-color: var(--hover-background);
}
.criteriaTable {
padding-left: 35px; /* Further indentation for the criteria table */
}
.project {
margin-top: 1rem !important;
}

View File

@ -0,0 +1,145 @@
import { Box, Table, Text } from '@mantine/core'
import { IconCornerDownRight } from '@tabler/icons-react'
import { useState } from 'react'
import classes from './ProjectInvolvement.module.css'
interface Project {
name: string
sprints: Sprint[]
}
interface Sprint {
name: string
criterias: TableRow[]
}
interface TableRow {
criteria: string
note: string
createdBy: string
point: number
}
interface ExpandedProjects {
[projectName: string]: boolean
}
interface ExpandedSprints {
[projectName: string]: {
[sprintName: string]: boolean
}
}
interface ProjectInvolvementProps {
dataProfile: Project[]
page: string
}
type CriteriaTableProps = {
data: TableRow[]
page: string
}
const CriteriaTable: React.FC<CriteriaTableProps> = ({ data, page }) => (
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr bg="#228be66b">
<Table.Th style={{ textAlign: 'center', width: '25%' }}>
Criteria
</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '45%' }}>Note</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '20%' }}>
Created by
</Table.Th>
{page == 'profile' ? (
''
) : (
<Table.Th style={{ textAlign: 'center', width: '10%' }}>
Point
</Table.Th>
)}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data.map((row, index) => (
<Table.Tr key={index}>
<Table.Td style={{ textAlign: 'start' }}>{row.criteria}</Table.Td>
<Table.Td style={{ textAlign: 'start' }}>{row.note}</Table.Td>
<Table.Td style={{ textAlign: 'start' }}>{row.createdBy}</Table.Td>
{page == 'profile' ? (
''
) : (
<Table.Td>{row.point == 0 ? '' : row.point}</Table.Td>
)}
</Table.Tr>
))}
</Table.Tbody>
</Table>
)
const ProjectInvolvement = ({ dataProfile, page }: ProjectInvolvementProps) => {
const [expandedProjects, setExpandedProjects] = useState<ExpandedProjects>({})
const [expandedSprints, setExpandedSprints] = useState<ExpandedSprints>({})
const handleProjectToggle = (projectName: string) => {
setExpandedProjects((prev) => ({
...prev,
[projectName]: !prev[projectName], // Toggle state for this project
}))
}
const handleSprintToggle = (projectName: string, sprintName: string) => {
setExpandedSprints((prev) => ({
...prev,
[projectName]: {
...prev[projectName],
[sprintName]: !prev[projectName]?.[sprintName], // Toggle state for this sprint
},
}))
}
return (
<Box className={classes.project} mt="lg">
{dataProfile.map((project) => (
<div key={project.name}>
<Box
className={classes.projectHeader}
onClick={() => handleProjectToggle(project.name)}
>
<Text ml="sm" fw={600}>
{project.name}
</Text>
</Box>
{expandedProjects[project.name] && (
<Box className={classes.sprintList}>
{project.sprints.map((sprint) => (
<div key={sprint.name}>
<Box
className={classes.sprintHeader}
onClick={() =>
handleSprintToggle(project.name, sprint.name)
}
>
<IconCornerDownRight size={20} />
<Text ml="xs" className={classes.sprintHeader2} fw={600}>
{sprint.name}
</Text>
</Box>
{expandedSprints[project.name]?.[sprint.name] && (
<Box className={classes.criteriaTable}>
<CriteriaTable data={sprint.criterias} page={page} />
</Box>
)}
</div>
))}
</Box>
)}
</div>
))}
</Box>
)
}
export default ProjectInvolvement

View File

@ -1,73 +1,28 @@
import { getProfilesData, updateProfilesData } from '@/api/Admin'
import { changePassword } from '@/api/Auth'
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
import { logout } from '@/rtk/dispatches/auth'
import { get, post, postImage } from '@/rtk/helpers/apiService'
import { requirementsPassword } from '@/rtk/helpers/variables'
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
import { getUser } from '@/rtk/localStorage'
import { success } from '@/rtk/slices/auth'
import {
Avatar,
Box,
Button,
Modal,
PasswordInput,
Table,
Text,
TextInput,
Title,
} from '@mantine/core'
import { notifications } from '@mantine/notifications'
import { IconCornerDownRight, IconPasswordUser } from '@tabler/icons-react'
import { IconPasswordUser } from '@tabler/icons-react'
import { useCallback, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import classes from './Profile.module.css'
import { success } from '@/rtk/slices/auth'
interface TableRow {
// Add properties for each column in the table
criteria: string
note: string
createdBy: string
point: number
}
interface ExpandedProjects {
[projectName: string]: boolean
}
interface ExpandedSprints {
[projectName: string]: {
[sprintName: string]: boolean
}
}
const CriteriaTable = ({ data }: { data: TableRow[] }) => (
<Table striped highlightOnHover withTableBorder withColumnBorders>
<Table.Thead>
<Table.Tr bg="#228be66b">
<Table.Th style={{ textAlign: 'center', width: '25%' }}>
Criteria
</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '45%' }}>Note</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '20%' }}>
Created by
</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '10%' }}>Point</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data.map((row, index) => (
<Table.Tr key={index}>
<Table.Td style={{ textAlign: 'start' }}>{row.criteria}</Table.Td>
<Table.Td style={{ textAlign: 'start' }}>{row.note}</Table.Td>
<Table.Td style={{ textAlign: 'start' }}>{row.createdBy}</Table.Td>
<Table.Td>{row.point == 0 ? '' : row.point}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)
const isCompactMenu = false
const Profile = () => {
@ -83,8 +38,6 @@ const Profile = () => {
const [avatar, setAvatar] = useState(user.user.avatar)
const [loading, setLoading] = useState(false)
const [dataProfile, setDataProfile] = useState<any>([])
const [expandedProjects, setExpandedProjects] = useState<ExpandedProjects>({})
const [expandedSprints, setExpandedSprints] = useState<ExpandedSprints>({})
const [countSpam, setCountSpam] = useState(0)
const [selectedAvatar, setSelectedAvatar] = useState<string | null>(null)
@ -229,22 +182,6 @@ const Profile = () => {
const handleLogout = useCallback(() => {
dispatch(logout(navigate))
}, [dispatch, navigate])
const handleProjectToggle = (projectName: string) => {
setExpandedProjects((prev) => ({
...prev,
[projectName]: !prev[projectName], // Toggle state for this project
}))
}
const handleSprintToggle = (projectName: string, sprintName: string) => {
setExpandedSprints((prev) => ({
...prev,
[projectName]: {
...prev[projectName],
[sprintName]: !prev[projectName]?.[sprintName], // Toggle state for this sprint
},
}))
}
return (
<div>
@ -364,58 +301,7 @@ const Profile = () => {
<Box className={classes.projectInvolvement}>
<Title order={3}>Project Involved</Title>
<Box className={classes.project} mt="lg">
{dataProfile.map((project: any) => (
<div key={project.name}>
{/* Project Header */}
<Box
className={classes.projectHeader}
onClick={() => handleProjectToggle(project.name)}
>
<Text ml="sm" fw={600}>
{project.name}
</Text>
</Box>
{/* Project's Sprints */}
{expandedProjects[project.name] && (
<Box className={classes.sprintList}>
{project.sprints.map((sprint: any) => (
<div key={sprint.name}>
<Box
className={classes.sprintHeader}
onClick={() =>
handleSprintToggle(project.name, sprint.name)
}
>
<IconCornerDownRight size={20} />
<Text
ml="xs"
className={classes.sprintHeader2}
fw={600}
>
{sprint.name}
</Text>
</Box>
{/* Criteria Table */}
{expandedSprints[project.name]?.[sprint.name] && (
<Box className={classes.criteriaTable}>
{/* <CriteriaTable data={sprint.criteria} /> */}
<CriteriaTable
data={sprint.criterias.map((criteria: any) => ({
...criteria,
}))}
/>
</Box>
)}
</div>
))}
</Box>
)}
</div>
))}
</Box>
<ProjectInvolvement dataProfile={dataProfile} page="profile" />
</Box>
</Box>

View File

@ -0,0 +1,202 @@
:root {
--primary-color: rgb(9, 132, 132);
--secondary-color: #ff9100;
--hover-background: rgba(203, 203, 203, 0.809);
}
.title {
background-color: light-dark(var(white), var(--mantine-color-dark-7));
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-lg)
var(--mantine-spacing-sm);
border-bottom: solid rgba(201, 201, 201, 0.377) 1px;
}
.optionIcon {
display: flex;
justify-content: space-evenly;
}
.deleteIcon,
.editIcon {
cursor: pointer;
padding: 2px;
border-radius: 25%;
}
.deleteIcon {
color: red;
}
.editIcon {
color: var(--primary-color);
}
.editIcon:hover,
.deleteIcon:hover {
background-color: var(--hover-background);
}
.dialog {
background-color: light-dark(white, #2d353c);
text-align: center;
border: solid 1px var(--secondary-color);
}
.dialogText {
color: light-dark(#2d353c, white);
}
/* Wrapper for the entire user info and project section */
.userInfoSection {
display: flex;
margin-top: 20px;
gap: 10px;
}
.titleSidebar {
text-align: center;
}
/* Sidebar section for user information */
.sidebar {
flex-grow: 1;
padding: 15px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Main section for projects and sprints */
.projectInvolvement {
width: 60%;
padding: 0 15px 15px 15px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Responsive Styles */
@media (max-width: 768px) {
.userInfoSection {
flex-direction: column;
}
.sidebar,
.projectInvolvement {
width: 100%;
padding: 10px;
}
}
.sprintContainer {
display: flex;
align-items: center;
}
.iconWrapper {
margin-right: 10px;
flex-shrink: 0;
display: flex;
align-items: center;
}
.projectHeader {
display: flex;
align-items: center;
cursor: pointer;
padding: 5px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 4px;
margin-top: 10px;
background-color: #457bad;
color: white;
transition: background-color 0.3s;
}
.projectHeader:hover {
background-color: var(--hover-background);
color: #2d353c;
}
.sprintList {
padding-left: 20px; /* Indentation for sprints under the project */
}
.sprintHeader2 {
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.1);
width: 100%;
padding-left: 5px;
padding-top: 5px;
padding-bottom: 5px;
margin-left: 20px;
align-items: center;
cursor: pointer;
background-color: rgba(162, 162, 162, 0.1);
}
.sprintHeader {
margin-top: 5px;
display: flex;
align-items: center;
cursor: pointer;
padding-left: 5px;
padding-top: 5px;
padding-bottom: 5px;
/* margin-left: 20px; */
transition: background-color 0.3s;
}
.sprintHeader2:hover {
background-color: var(--hover-background);
}
.criteriaTable {
padding-left: 35px; /* Further indentation for the criteria table */
}
.link {
display: flex;
align-items: center;
text-decoration: none;
font-size: var(--mantine-font-size-sm);
color: rgb(255, 255, 255);
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
margin: var(--mantine-spacing-xs);
border-radius: var(--mantine-radius-sm);
font-weight: 500;
cursor: pointer;
background-color: #00a164;
@mixin hover {
/* background-color: #035737; */
color: var(--mantine-color-white);
.linkIcon {
color: var(--mantine-color-white);
}
}
&[data-active] {
&,
&:hover {
box-shadow: var(--mantine-shadow-sm);
background-color: #6089afb7;
color: var(--mantine-color-white);
.linkIcon {
color: var(--mantine-color-white);
}
}
}
}
.linkIcon {
color: rgb(255, 255, 255);
width: rem(20px);
height: rem(22px);
}

View File

@ -0,0 +1,274 @@
import { getProfilesData } from '@/api/Admin'
import DataTableAll from '@/components/DataTable/DataTable'
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
import { get } from '@/rtk/helpers/apiService'
import { Box, Button, Select, Text, Title } from '@mantine/core'
import { DateInput } from '@mantine/dates'
import { notifications } from '@mantine/notifications'
import moment from 'moment'
import { useEffect, useState } from 'react'
import classes from './StaffEvaluation.module.css'
interface User {
id: number
name: string
email: string
email_verified_at: string | null
permission: string
remember_token: string | null
created_at: string | null
updated_at: string | null
}
interface Filter {
userID: string
fromDate: Date | null
toDate: Date | null
// other properties of the filter object
}
const StaffEvaluation = () => {
const [dataProfile, setDataProfile] = useState<any>([])
const [listUsers, setListUsers] = useState<User[]>([])
const [filter, setFilter] = useState<Filter>({
userID: '',
fromDate: null,
toDate: null,
})
console.log(filter, 'filter')
// const getAllTracking = 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(getListTracking, params)
// if (res.status) {
// setListTracking(res)
// }
// } catch (error:any) {
// notifications.show({
// title: 'Error',
// message: error.message??error,
// color: 'red',
// })
// }
// }
const getListProfilesData = async () => {
try {
const params = {}
const res = await get(getProfilesData, params)
if (res.status) {
return res.data
}
} catch (error: any) {
notifications.show({
title: 'Error',
message: error.message ?? error,
color: 'red',
})
}
return []
}
useEffect(() => {
const fetchData = async () => {
const result = await getListProfilesData()
setDataProfile(result ?? [])
setListUsers(result ?? [])
}
fetchData()
}, [])
const columns = [
{
name: 'level',
size: '20%',
header: 'Level',
},
{
name: 'name',
size: '',
header: 'Name',
},
{
name: 'point',
size: '10%',
header: 'Point',
render: (row: any) => {
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
}}
>
{row?.point}
</div>
)
},
},
{
name: 'updated_at',
size: '25%',
header: 'Last update',
render: (row: any) => {
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
}}
>
{moment(row?.updated_at).format('HH:mm:ss DD/MM/YYYY')}
</div>
)
},
},
]
const users = [
{
id: 1,
level: 'Level 1',
name: 'React JS',
point: 3,
created_at: null,
updated_at: '2024-09-19T09:08:48.000000Z',
},
]
const infoTotal = () => {
return (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Text mr={20} fs={'italic'}>
Avg:{users.length}
</Text>
<Text fs={'italic'}>Total: {users.length}</Text>
</div>
)
}
return (
<div>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Staff Evaluation
</h3>
</div>
<Box w="100%" display={'flex'} mt={15} ml={10}>
<Box w="50%" display={'flex'}>
<Text
mr={'xs'}
style={{ alignContent: 'center' }}
fw={600}
size={'md'}
>
User:
</Text>
<Select
label={''}
placeholder="Select user"
maxLength={255}
required
data={listUsers.map((i: User) => i.name)}
onChange={(e) => setFilter({ ...filter, userID: e! })}
/>
</Box>
<Box
w="50%"
display={'flex'}
style={{ justifyContent: 'flex-end' }}
mr={10}
>
<Button
// m={5}
onClick={() => {
// setAction('add')
// form.reset()
}}
>
Export
</Button>
</Box>
</Box>
<Box className={classes.userInfoSection} display="flex">
<Box className={classes.projectInvolvement}>
<Box
w="100%"
display={'flex'}
mt={15}
style={{ justifyContent: 'space-evenly' }}
>
<Box display={'flex'} mr={10}>
<Text
mr={'xs'}
style={{ alignContent: 'center' }}
fw={600}
size={'md'}
>
From Date:
</Text>
<DateInput
placeholder="Select date"
clearable
required
label={''}
value={filter.fromDate ? new Date(filter.fromDate) : null}
valueFormat="DD/MM/YYYY"
onChange={(e) => setFilter({ ...filter, fromDate: e! })}
></DateInput>
</Box>
<Box display={'flex'} mr={10}>
<Text
mr={'xs'}
style={{ alignContent: 'center' }}
fw={600}
size={'md'}
>
To Date:
</Text>
<DateInput
placeholder="Select date"
clearable
required
label={''}
value={filter.toDate ? new Date(filter.toDate) : null}
valueFormat="DD/MM/YYYY"
onChange={(e) => setFilter({ ...filter, toDate: e! })}
></DateInput>
</Box>
</Box>
<ProjectInvolvement dataProfile={dataProfile} page="admin" />
</Box>
<Box className={classes.sidebar}>
<Title order={3} className={classes.titleSidebar}>
Technicals
</Title>
<DataTableAll
data={users}
columns={columns}
size=""
searchInput
infoTotal={infoTotal()}
/>
</Box>
</Box>
</div>
)
}
export default StaffEvaluation

View File

@ -8,6 +8,7 @@ import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
import PageNotFound from '@/pages/NotFound/NotFound'
import Profile from '@/pages/Profile/Profile'
import SprintReview from '@/pages/SprintReview/SprintReview'
import StaffEvaluation from '@/pages/StaffEvaluation/StaffEvaluation'
import TestReport from '@/pages/TestReport/TestReport'
import Tickets from '@/pages/Tickets/Tickets'
import TicketsManagement from '@/pages/TicketsManagement/TicketsManagement'
@ -204,6 +205,20 @@ const mainRoutes = [
</ProtectedRoute>
),
},
{
path: '/staff-avaluation',
element: (
<ProtectedRoute mode="route" permission="admin">
<BasePage
main={
<>
<StaffEvaluation />
</>
}
></BasePage>
</ProtectedRoute>
),
},
// {
// path: '/packages',
// element: (