1342 lines
37 KiB
TypeScript
1342 lines
37 KiB
TypeScript
import {
|
|
evaluation,
|
|
getAllTechByUserId,
|
|
getAllUser,
|
|
projectReview,
|
|
sprintReview,
|
|
projectReviewAdd,
|
|
projectReviewUpdate,
|
|
projectReviewDelete,
|
|
evaluationReportAllUsers,
|
|
getListTrackingSummary,
|
|
getPJParticipating,
|
|
} from '@/api/Admin'
|
|
import DataTableAll from '@/components/DataTable/DataTable'
|
|
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
|
|
import { get, getDownloadFile, post } from '@/rtk/helpers/apiService'
|
|
import {
|
|
Box,
|
|
Button,
|
|
Dialog,
|
|
Group,
|
|
Loader,
|
|
Modal,
|
|
Select,
|
|
Tabs,
|
|
Text,
|
|
Textarea,
|
|
TextInput,
|
|
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'
|
|
import {
|
|
IconClearAll,
|
|
IconEdit,
|
|
IconPresentationAnalytics,
|
|
IconReportAnalytics,
|
|
IconX,
|
|
} from '@tabler/icons-react'
|
|
import { useForm } from '@mantine/form'
|
|
import { update, Xdelete } from '@/rtk/helpers/CRUD'
|
|
import { PieChart } from '@mantine/charts'
|
|
|
|
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
|
|
}
|
|
|
|
interface DataTechnical {
|
|
id: number
|
|
name: string
|
|
level: number
|
|
point: number
|
|
updated_at: string
|
|
}
|
|
|
|
interface DataProjectReview {
|
|
id: number
|
|
name: string
|
|
role: string
|
|
note: string
|
|
user_id: number
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
interface DataPJParticipating {
|
|
name: string
|
|
total_task: number
|
|
total_time_spent: number
|
|
}
|
|
|
|
type TLog = {
|
|
id: number
|
|
name: string
|
|
status: string
|
|
time_string: Date
|
|
}
|
|
|
|
interface DataSummaryTracking {
|
|
on_time_morning: number
|
|
late_morning: number
|
|
on_time_afternoon: number
|
|
late_afternoon: number
|
|
value: TLog[]
|
|
}
|
|
|
|
const StaffEvaluation = () => {
|
|
const [loading, setLoading] = useState(false)
|
|
const [loadingReview, setLoadingReview] = useState(false)
|
|
const [loadingWorkingStyle, setLoadingWorkingStyle] = useState(false)
|
|
const [loadingPJParticipating, setLoadingPJParticipating] = useState(false)
|
|
const [loadingTechnical, setLoadingTechnical] = useState(false)
|
|
const [dataProfile, setDataProfile] = useState<any>([])
|
|
const [dataTechnical, setDataTechnical] = useState<DataTechnical[]>([])
|
|
const [dataProjectReview, setDataProjectReview] = useState<
|
|
DataProjectReview[]
|
|
>([])
|
|
const [listUsers, setListUsers] = useState<User[]>([])
|
|
const [filter, setFilter] = useState<Filter>({
|
|
userID: '',
|
|
fromDate: null,
|
|
toDate: null,
|
|
})
|
|
const [action, setAction] = useState('')
|
|
const [item, setItem] = useState({ id: 0 })
|
|
const [disableBtn, setDisableBtn] = useState(false)
|
|
const [activeBtn, setActiveBtn] = useState(false)
|
|
const [loadingExport, setLoadingExport] = useState(false)
|
|
const [loadingExportAll, setLoadingExportAll] = useState(false)
|
|
const [dataPJParticipating, setDataPJParticipating] = useState<
|
|
DataPJParticipating[]
|
|
>([])
|
|
const [dataSummaryTracking, setDataSummaryTracking] =
|
|
useState<DataSummaryTracking>({
|
|
on_time_morning: 0,
|
|
late_morning: 0,
|
|
on_time_afternoon: 0,
|
|
late_afternoon: 0,
|
|
value: [],
|
|
})
|
|
|
|
const form = useForm({
|
|
initialValues: {
|
|
id: 0,
|
|
name: '',
|
|
role: '',
|
|
note: '',
|
|
user_id: 0,
|
|
created_at: '',
|
|
updated_at: '',
|
|
},
|
|
validate: {
|
|
name: (value) =>
|
|
value.length === 0 ? 'Please enter project name' : null,
|
|
role: (value) => (value.length === 0 ? 'Please enter role' : null),
|
|
note: (value) => (value.length === 0 ? 'Please enter note' : null),
|
|
},
|
|
})
|
|
|
|
const getListUser = async () => {
|
|
try {
|
|
const params = {}
|
|
const res = await get(getAllUser, params)
|
|
if (res.status) {
|
|
return res.data
|
|
}
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
function getFormattedDateTime(): string {
|
|
const now = new Date()
|
|
const year = now.getFullYear()
|
|
const month = String(now.getMonth() + 1).padStart(2, '0') // Tháng bắt đầu từ 0
|
|
const day = String(now.getDate()).padStart(2, '0')
|
|
const hours = String(now.getHours()).padStart(2, '0')
|
|
const minutes = String(now.getMinutes()).padStart(2, '0')
|
|
const seconds = String(now.getSeconds()).padStart(2, '0')
|
|
|
|
return `${year}${month}${day}${hours}${minutes}${seconds}`
|
|
}
|
|
|
|
const downloadFile = async (filterSearch: Filter) => {
|
|
try {
|
|
const params = {
|
|
userID: filterSearch.userID ?? '',
|
|
fromDate: filterSearch.fromDate
|
|
? moment(filterSearch.fromDate).format('YYYY-MM-DD')
|
|
: null,
|
|
toDate: filterSearch.toDate
|
|
? moment(filterSearch.toDate).format('YYYY-MM-DD')
|
|
: null,
|
|
}
|
|
const user = listUsers.find(
|
|
(el) => el.id.toString() === filterSearch.userID,
|
|
)
|
|
setLoadingExport(true)
|
|
const res = await getDownloadFile(evaluation, params)
|
|
|
|
if (res.status) {
|
|
const fileURL = window.URL.createObjectURL(new Blob([res.data]))
|
|
const fileLink = document.createElement('a')
|
|
|
|
const fileName = `STAFF_EVALUATION_${user?.name
|
|
?.split(' ')
|
|
.join('_')}_${getFormattedDateTime()}.docx`
|
|
|
|
fileLink.href = fileURL
|
|
fileLink.setAttribute('download', fileName)
|
|
document.body.appendChild(fileLink)
|
|
|
|
fileLink.click()
|
|
fileLink.remove()
|
|
}
|
|
setLoadingExport(false)
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
const downloadFileAll = async (filterSearch: Filter) => {
|
|
try {
|
|
const params = {
|
|
fromDate: filterSearch.fromDate
|
|
? moment(filterSearch.fromDate).format('YYYY-MM-DD')
|
|
: null,
|
|
toDate: filterSearch.toDate
|
|
? moment(filterSearch.toDate).format('YYYY-MM-DD')
|
|
: null,
|
|
}
|
|
setLoadingExportAll(true)
|
|
const res = await getDownloadFile(evaluationReportAllUsers, params)
|
|
|
|
if (res.status) {
|
|
const fileURL = window.URL.createObjectURL(new Blob([res.data]))
|
|
const fileLink = document.createElement('a')
|
|
|
|
const fileName = `STAFF_EVALUATION_All_USERS_${getFormattedDateTime()}.docx`
|
|
|
|
fileLink.href = fileURL
|
|
fileLink.setAttribute('download', fileName)
|
|
document.body.appendChild(fileLink)
|
|
|
|
fileLink.click()
|
|
fileLink.remove()
|
|
}
|
|
setLoadingExportAll(false)
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
const result = await getListUser()
|
|
setListUsers(result ?? [])
|
|
}
|
|
fetchData()
|
|
}, [])
|
|
|
|
const getListProfilesData = async (filterSearch: Filter) => {
|
|
try {
|
|
const params = {
|
|
userID: filterSearch.userID ?? '',
|
|
fromDate: filterSearch.fromDate
|
|
? moment(filterSearch.fromDate).format('YYYY-MM-DD')
|
|
: null,
|
|
toDate: filterSearch.toDate
|
|
? moment(filterSearch.toDate).format('YYYY-MM-DD')
|
|
: null,
|
|
}
|
|
const res = await get(sprintReview, params)
|
|
if (res.status) {
|
|
return res.data
|
|
}
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
const getListTechnicalByUserId = async (id: string) => {
|
|
try {
|
|
const params = {}
|
|
const res = await get(`${getAllTechByUserId}/${id}`, params)
|
|
if (res.status) {
|
|
return res.data
|
|
}
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
const getListProjectReview = async (filterSearch: Filter) => {
|
|
try {
|
|
const params = {
|
|
userID: filterSearch.userID ?? '',
|
|
fromDate: filterSearch.fromDate
|
|
? moment(filterSearch.fromDate).format('YYYY-MM-DD')
|
|
: null,
|
|
toDate: filterSearch.toDate
|
|
? moment(filterSearch.toDate).format('YYYY-MM-DD')
|
|
: null,
|
|
}
|
|
const res = await get(projectReview, params)
|
|
if (res.status) {
|
|
return res.data
|
|
}
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
const getListSummaryTracking = async (filterSearch: Filter) => {
|
|
try {
|
|
const params = {
|
|
userID: filterSearch.userID ?? '',
|
|
fromDate: filterSearch.fromDate
|
|
? moment(filterSearch.fromDate).format('YYYY-MM-DD')
|
|
: null,
|
|
toDate: filterSearch.toDate
|
|
? moment(filterSearch.toDate).format('YYYY-MM-DD')
|
|
: null,
|
|
}
|
|
const res = await get(getListTrackingSummary, params)
|
|
if (res.status) {
|
|
return res.data
|
|
}
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
const getListProjectParticipating = async (filterSearch: Filter) => {
|
|
try {
|
|
const fromDate = filterSearch.fromDate
|
|
? moment(filterSearch.fromDate).format('YYYY-MM-DD')
|
|
: moment(new Date()).format('YYYY-MM-DD')
|
|
const toDate = filterSearch.toDate
|
|
? moment(filterSearch.toDate).format('YYYY-MM-DD')
|
|
: moment(new Date()).format('YYYY-MM-DD')
|
|
const params = {
|
|
userID: filterSearch.userID ?? '',
|
|
fromDate: fromDate,
|
|
toDate: toDate,
|
|
}
|
|
const res = await get(getPJParticipating, params)
|
|
if (res.status) {
|
|
const value = processJiraData(res.data, fromDate, toDate, res.accountId)
|
|
return value
|
|
}
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
function processJiraData(
|
|
data: any,
|
|
startDate: any,
|
|
endDate: any,
|
|
accountId: string,
|
|
) {
|
|
const projectSummary: any = {}
|
|
const start = new Date(startDate)
|
|
const end = new Date(endDate)
|
|
|
|
data.issues.forEach((issue: any) => {
|
|
const projectName = issue.fields.project.name
|
|
const worklogs = issue.fields.worklog.worklogs
|
|
|
|
// Filter worklogs based on 'started' date range
|
|
const filteredWorklogs = worklogs.filter((log: any) => {
|
|
const logDate = new Date(log.started)
|
|
return (
|
|
logDate >= start &&
|
|
logDate <= end &&
|
|
accountId === log?.updateAuthor?.accountId
|
|
)
|
|
})
|
|
|
|
if (filteredWorklogs.length === 0) return // Skip if no worklogs in range
|
|
|
|
if (!projectSummary[projectName]) {
|
|
projectSummary[projectName] = {
|
|
project_name: projectName,
|
|
total_task: 0,
|
|
total_time_spent: 0,
|
|
}
|
|
}
|
|
|
|
// Get unique issueIds within the filtered worklogs
|
|
const uniqueIssues = new Set(
|
|
filteredWorklogs.map((log: any) => log.issueId),
|
|
)
|
|
|
|
// Sum up total time spent from filtered worklogs
|
|
const totalTimeSpent = filteredWorklogs.reduce(
|
|
(sum: number, log: any) => sum + log.timeSpentSeconds,
|
|
0,
|
|
)
|
|
|
|
projectSummary[projectName].total_task += uniqueIssues.size
|
|
projectSummary[projectName].total_time_spent += totalTimeSpent
|
|
})
|
|
|
|
const returnValue: DataPJParticipating[] = Object.values(projectSummary)
|
|
|
|
return returnValue
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (filter?.userID) {
|
|
setLoading(true)
|
|
setLoadingReview(true)
|
|
setLoadingWorkingStyle(true)
|
|
const fetchData = async () => {
|
|
const result = await getListProfilesData(filter)
|
|
setDataProfile(result ?? [])
|
|
setLoading(false)
|
|
}
|
|
const fetchDataProject = async () => {
|
|
const result = await getListProfilesData(filter)
|
|
const resultProject = await getListProjectReview(filter)
|
|
setDataProfile(result ?? [])
|
|
setDataProjectReview(resultProject ?? [])
|
|
setLoadingReview(false)
|
|
}
|
|
const fetchDataTracking = async () => {
|
|
const resultTracking = await getListSummaryTracking(filter)
|
|
setDataSummaryTracking(resultTracking ?? [])
|
|
setLoadingWorkingStyle(false)
|
|
}
|
|
const fetchDataPJParticipating = async () => {
|
|
const resultPJParticipating = await getListProjectParticipating(filter)
|
|
setDataPJParticipating(resultPJParticipating ?? [])
|
|
setLoadingPJParticipating(false)
|
|
}
|
|
fetchData()
|
|
fetchDataProject()
|
|
fetchDataTracking()
|
|
if (filter?.fromDate && filter?.toDate) {
|
|
setLoadingPJParticipating(true)
|
|
fetchDataPJParticipating()
|
|
}
|
|
}
|
|
}, [filter])
|
|
|
|
useEffect(() => {
|
|
if (filter?.userID) {
|
|
setLoadingTechnical(true)
|
|
const fetchData = async () => {
|
|
const result = await getListTechnicalByUserId(filter?.userID)
|
|
setDataTechnical(result ?? [])
|
|
setLoadingTechnical(false)
|
|
}
|
|
fetchData()
|
|
}
|
|
}, [filter?.userID])
|
|
|
|
const columns = [
|
|
{
|
|
name: 'level',
|
|
size: '10%',
|
|
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: '',
|
|
header: 'Name',
|
|
},
|
|
{
|
|
name: 'point',
|
|
size: '10%',
|
|
header: 'Point',
|
|
render: (row: any) => {
|
|
if (row?.point > 0)
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
{row?.point}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: 'updated_at',
|
|
size: '25%',
|
|
header: 'Last update',
|
|
render: (row: any) => {
|
|
if (row?.updated_at)
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
{moment(row?.updated_at).format('DD/MM/YYYY HH:mm:ss')}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
]
|
|
|
|
const infoTotal = () => {
|
|
// Tính tổng point và số lượng point > 0
|
|
let totalPoint = 0
|
|
let count = 0
|
|
|
|
dataTechnical.forEach((item) => {
|
|
if (item.point > 0) {
|
|
totalPoint += item.point
|
|
count++
|
|
}
|
|
})
|
|
|
|
const averagePoint = count > 0 ? (totalPoint / count).toFixed(2) : '0.00'
|
|
|
|
return (
|
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<Text mr={20} fs={'italic'}>
|
|
Avg: {averagePoint}
|
|
</Text>
|
|
<Text fs={'italic'}>Total: {totalPoint}</Text>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const columnsProjectReview = [
|
|
// {
|
|
// name: 'id',
|
|
// size: '5%',
|
|
// header: 'Num',
|
|
// render: (row: any) => {
|
|
// return (
|
|
// <Box fw={500} ta="center" p={4}>
|
|
// {row?.id ? row.id : ''}
|
|
// </Box>
|
|
// )
|
|
// },
|
|
// },
|
|
{
|
|
name: 'name',
|
|
size: '15%',
|
|
header: 'Name',
|
|
},
|
|
{
|
|
name: 'role',
|
|
size: '15%',
|
|
header: 'Role',
|
|
},
|
|
{
|
|
name: 'note',
|
|
size: '',
|
|
header: 'Note',
|
|
},
|
|
{
|
|
name: 'created_at',
|
|
size: '10%',
|
|
header: 'Created at',
|
|
render: (row: any) => {
|
|
if (row?.created_at)
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
{moment(row?.created_at).format('DD/MM/YYYY HH:mm:ss')}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: 'updated_at',
|
|
size: '10%',
|
|
header: 'Last update',
|
|
render: (row: any) => {
|
|
if (row?.updated_at)
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
{moment(row?.updated_at).format('DD/MM/YYYY HH:mm:ss')}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: '#',
|
|
size: '5%',
|
|
header: 'Action',
|
|
render: (row: DataProjectReview) => {
|
|
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 columnsPJParticipating = [
|
|
{
|
|
name: 'project_name',
|
|
size: '50%',
|
|
header: 'Name',
|
|
},
|
|
{
|
|
name: 'total_task',
|
|
size: '25%',
|
|
header: 'Total task',
|
|
},
|
|
{
|
|
name: 'total_time_spent',
|
|
size: '25%',
|
|
header: 'Total time spent',
|
|
render: (row: any) => {
|
|
return <div>{row?.total_time_spent / 60 / 60}h</div>
|
|
},
|
|
},
|
|
]
|
|
|
|
const columnsDetailWorking = [
|
|
{
|
|
name: 'name',
|
|
size: '40%',
|
|
header: 'Name',
|
|
},
|
|
{
|
|
name: 'time_string',
|
|
size: '40%',
|
|
header: 'Time',
|
|
render: (row: any) => {
|
|
return moment(row.time_string).format('YYYY/MM/DD - HH:mm:ss')
|
|
},
|
|
},
|
|
{
|
|
name: 'status',
|
|
size: '20%',
|
|
header: 'Status',
|
|
},
|
|
]
|
|
|
|
const handleCreate = async (values: DataProjectReview) => {
|
|
try {
|
|
const res = await post(projectReviewAdd, {
|
|
name: values.name,
|
|
role: values.role,
|
|
note: values.note,
|
|
user_id: filter.userID,
|
|
})
|
|
if (res.id) {
|
|
setAction('')
|
|
form.reset()
|
|
const resultProject = await getListProjectReview(filter)
|
|
setDataProjectReview(resultProject ?? [])
|
|
}
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
|
|
const handleUpdate = async (values: DataProjectReview) => {
|
|
try {
|
|
const res = await update(projectReviewUpdate, {
|
|
id: values.id,
|
|
name: values.name,
|
|
role: values.role,
|
|
note: values.note,
|
|
user_id: filter.userID,
|
|
})
|
|
if (res) {
|
|
setAction('')
|
|
form.reset()
|
|
const resultProject = await getListProjectReview(filter)
|
|
setDataProjectReview(resultProject ?? [])
|
|
}
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
|
|
const handleDelete = async (id: number) => {
|
|
try {
|
|
await Xdelete(projectReviewDelete, { id: id }, async () => {
|
|
const resultProject = await getListProjectReview(filter)
|
|
setDataProjectReview(resultProject ?? [])
|
|
})
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className={classes.title}>
|
|
<h3>
|
|
<Text>Admin/</Text>
|
|
Staff Evaluation
|
|
</h3>
|
|
<Box
|
|
w="20%"
|
|
display={'flex'}
|
|
style={{ justifyContent: 'flex-end' }}
|
|
mr={10}
|
|
>
|
|
{loadingExportAll ? (
|
|
<Button
|
|
disabled
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
width: '135px',
|
|
}}
|
|
onClick={() => {}}
|
|
>
|
|
<Loader size={'sm'} color="green" type="oval" m={'0 auto'} />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
// m={5}
|
|
style={{ display: 'flex', width: '135px' }}
|
|
onClick={() => downloadFileAll(filter)}
|
|
>
|
|
Export all user
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
</div>
|
|
|
|
<Box w="100%" display={'flex'} mt={15} ml={10}>
|
|
<Box w="80%" display={'flex'} style={{ alignItems: 'center' }}>
|
|
<Text
|
|
mr={'xs'}
|
|
style={{ alignContent: 'center' }}
|
|
fw={600}
|
|
size={'md'}
|
|
>
|
|
User:
|
|
</Text>
|
|
<Select
|
|
style={{ width: '20%' }}
|
|
label={''}
|
|
placeholder="Select user"
|
|
maxLength={255}
|
|
size={'xs'}
|
|
required
|
|
data={listUsers.map((i: User) => ({
|
|
value: i.id.toString(),
|
|
label: i.name,
|
|
}))}
|
|
value={filter.userID}
|
|
onChange={(e) => setFilter({ ...filter, userID: e! })}
|
|
/>
|
|
<Box
|
|
display={'flex'}
|
|
mr={10}
|
|
ms={10}
|
|
style={{ alignItems: 'center' }}
|
|
>
|
|
<Text
|
|
mr={'xs'}
|
|
style={{ alignContent: 'center' }}
|
|
fw={600}
|
|
size={'md'}
|
|
>
|
|
From Date:
|
|
</Text>
|
|
|
|
<DateInput
|
|
placeholder="Select date"
|
|
clearable
|
|
size="xs"
|
|
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} style={{ alignItems: 'center' }}>
|
|
<Text
|
|
mr={'xs'}
|
|
style={{ alignContent: 'center' }}
|
|
fw={600}
|
|
size={'md'}
|
|
>
|
|
To Date:
|
|
</Text>
|
|
<DateInput
|
|
placeholder="Select date"
|
|
clearable
|
|
size="xs"
|
|
required
|
|
label={''}
|
|
value={filter.toDate ? new Date(filter.toDate) : null}
|
|
valueFormat="DD/MM/YYYY"
|
|
onChange={(e) => setFilter({ ...filter, toDate: e! })}
|
|
></DateInput>
|
|
</Box>
|
|
</Box>
|
|
<Box
|
|
w="20%"
|
|
display={'flex'}
|
|
style={{ justifyContent: 'flex-end' }}
|
|
mr={10}
|
|
>
|
|
{loadingExport ? (
|
|
<Button
|
|
disabled
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
width: '80px',
|
|
}}
|
|
onClick={() => {}}
|
|
>
|
|
<Loader size={'sm'} color="green" type="oval" m={'0 auto'} />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
// m={5}
|
|
style={{
|
|
display: filter.userID != '' ? 'flex' : 'none',
|
|
width: '80px',
|
|
}}
|
|
onClick={() => downloadFile(filter)}
|
|
>
|
|
Export
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
<Tabs mt={8} variant="outline" defaultValue="general">
|
|
<Tabs.List>
|
|
<Tabs.Tab
|
|
value="general"
|
|
leftSection={<IconClearAll size={16} color="teal" />}
|
|
>
|
|
<span style={{ fontSize: '16px' }}>General</span>
|
|
</Tabs.Tab>
|
|
<Tabs.Tab
|
|
value="project_review"
|
|
leftSection={<IconPresentationAnalytics size={16} color="blue" />}
|
|
>
|
|
<span style={{ fontSize: '16px' }}>Project review</span>
|
|
</Tabs.Tab>
|
|
<Tabs.Tab
|
|
value="working_style"
|
|
leftSection={<IconReportAnalytics size={16} color="#fab005" />}
|
|
>
|
|
<span style={{ fontSize: '16px' }}>Working review</span>
|
|
</Tabs.Tab>
|
|
</Tabs.List>
|
|
|
|
<Tabs.Panel value="general">
|
|
<Box className={classes.userInfoSection} display="flex">
|
|
<Box className={classes.projectInvolvement}>
|
|
<Box
|
|
style={{
|
|
marginTop: '10%',
|
|
textAlign: 'center',
|
|
display: loading ? 'block' : 'none',
|
|
// display: 'none',
|
|
}}
|
|
>
|
|
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
|
|
<Text fw={600} c={'gray'}>
|
|
Loading . . .
|
|
</Text>
|
|
</Box>
|
|
{!loading && dataProfile.length == 0 && (
|
|
<Box
|
|
style={{
|
|
marginTop: '10%',
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
}}
|
|
>
|
|
<Text fw={600} c={'gray'}>
|
|
No Data Sprint
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
{!loading && (
|
|
<ProjectInvolvement dataProfile={dataProfile} page="admin" />
|
|
)}
|
|
</Box>
|
|
|
|
<Box className={classes.sidebar}>
|
|
<Title order={3} className={classes.titleSidebar}>
|
|
Technicals
|
|
</Title>
|
|
{loadingTechnical ? (
|
|
<Box
|
|
style={{
|
|
marginTop: '10%',
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
}}
|
|
>
|
|
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
|
|
<Text fw={600} c={'gray'}>
|
|
Loading . . .
|
|
</Text>
|
|
</Box>
|
|
) : (
|
|
<DataTableAll
|
|
data={dataTechnical}
|
|
columns={columns}
|
|
size=""
|
|
searchInput
|
|
infoTotal={infoTotal()}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Tabs.Panel>
|
|
|
|
<Tabs.Panel value="project_review">
|
|
<Box className={classes.userInfoSection} display="flex">
|
|
{loadingReview ? (
|
|
<Box
|
|
style={{
|
|
width: '100%',
|
|
display: loadingReview ? 'block' : 'none',
|
|
}}
|
|
>
|
|
<Box
|
|
style={{
|
|
marginTop: '10%',
|
|
textAlign: 'center',
|
|
// display: 'none',
|
|
}}
|
|
>
|
|
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
|
|
<Text fw={600} c={'gray'}>
|
|
Loading . . .
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
) : (
|
|
<DataTableAll
|
|
data={dataProjectReview}
|
|
columns={columnsProjectReview}
|
|
size=""
|
|
searchInput
|
|
// infoTotal={infoTotal()}
|
|
componentRight={
|
|
<Box
|
|
w="100%"
|
|
display={'flex'}
|
|
style={{ justifyContent: 'flex-end' }}
|
|
mr={10}
|
|
>
|
|
<Button
|
|
color="teal"
|
|
style={{ display: filter.userID != '' ? 'flex' : 'none' }}
|
|
onClick={() => {
|
|
setAction('add')
|
|
form.reset()
|
|
}}
|
|
>
|
|
Add
|
|
</Button>
|
|
</Box>
|
|
}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</Tabs.Panel>
|
|
<Tabs.Panel value="working_style">
|
|
{loadingWorkingStyle ? (
|
|
<Box
|
|
style={{
|
|
width: '100%',
|
|
display: loadingWorkingStyle ? 'block' : 'none',
|
|
}}
|
|
>
|
|
<Box
|
|
style={{
|
|
marginTop: '10%',
|
|
textAlign: 'center',
|
|
// display: 'none',
|
|
}}
|
|
>
|
|
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
|
|
<Text fw={600} c={'gray'}>
|
|
Loading . . .
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
) : (
|
|
<Box>
|
|
<Tabs defaultValue="overview" orientation="vertical">
|
|
<Tabs.List justify="center">
|
|
<Tabs.Tab value="overview">Overview</Tabs.Tab>
|
|
<Tabs.Tab value="detail">Detail</Tabs.Tab>
|
|
</Tabs.List>
|
|
|
|
<Tabs.Panel value="overview">
|
|
<Box style={{ height: 350, marginTop: 8 }}>
|
|
<Box className={classes.chartContainer} display="flex">
|
|
<PieChart
|
|
withLabelsLine
|
|
labelsPosition="outside"
|
|
labelsType="value"
|
|
withLabels={dataSummaryTracking.value.length > 0}
|
|
withTooltip
|
|
data={
|
|
dataSummaryTracking.value.length > 0
|
|
? [
|
|
{
|
|
name: 'On time morning',
|
|
value:
|
|
dataSummaryTracking?.on_time_morning ?? 0,
|
|
color: 'lime',
|
|
},
|
|
{
|
|
name: 'Late morning',
|
|
value: dataSummaryTracking?.late_morning ?? 0,
|
|
color: 'red',
|
|
},
|
|
{
|
|
name: 'On time afternoon',
|
|
value:
|
|
dataSummaryTracking?.on_time_afternoon ?? 0,
|
|
color: 'teal',
|
|
},
|
|
{
|
|
name: 'Late afternoon',
|
|
value:
|
|
dataSummaryTracking?.late_afternoon ?? 0,
|
|
color: 'orange',
|
|
},
|
|
]
|
|
: [
|
|
{
|
|
name: 'No data',
|
|
value: 1,
|
|
color: 'gray.6',
|
|
},
|
|
]
|
|
}
|
|
/>
|
|
</Box>
|
|
<Box className={classes.boxContainer} display="flex">
|
|
<Box className={classes.boxContainer} display="flex">
|
|
<div className={classes.boxColorLime}></div>
|
|
<div
|
|
style={{ paddingLeft: '10px', paddingRight: '20px' }}
|
|
>
|
|
On time morning
|
|
</div>
|
|
</Box>
|
|
<Box className={classes.boxContainer} display="flex">
|
|
<div className={classes.boxColorRed}></div>
|
|
<div
|
|
style={{ paddingLeft: '10px', paddingRight: '20px' }}
|
|
>
|
|
Late morning
|
|
</div>
|
|
</Box>
|
|
<Box className={classes.boxContainer} display="flex">
|
|
<div className={classes.boxColorTeal}></div>
|
|
<div
|
|
style={{ paddingLeft: '10px', paddingRight: '20px' }}
|
|
>
|
|
On time afternoon
|
|
</div>
|
|
</Box>
|
|
<Box className={classes.boxContainer} display="flex">
|
|
<div className={classes.boxColorOrange}></div>
|
|
<div style={{ paddingLeft: '10px' }}>
|
|
Late afternoon
|
|
</div>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Tabs.Panel>
|
|
<Tabs.Panel value="detail">
|
|
<Box style={{ marginTop: 8 }}>
|
|
<DataTableAll
|
|
data={dataSummaryTracking.value}
|
|
columns={columnsDetailWorking}
|
|
size=""
|
|
height={300}
|
|
keyHighlight={'isLate'}
|
|
/>
|
|
</Box>
|
|
</Tabs.Panel>
|
|
</Tabs>
|
|
</Box>
|
|
)}
|
|
<Box className={classes.pjParticipatingContainer}>
|
|
<Title order={4}>Project Participating</Title>
|
|
</Box>
|
|
<Box className={classes.boxContainer} display="flex">
|
|
{loadingPJParticipating ? (
|
|
<Box
|
|
style={{
|
|
width: '100%',
|
|
display: loadingPJParticipating ? 'block' : 'none',
|
|
}}
|
|
>
|
|
<Box
|
|
style={{
|
|
marginTop: '10%',
|
|
textAlign: 'center',
|
|
// display: 'none',
|
|
}}
|
|
>
|
|
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
|
|
<Text fw={600} c={'gray'}>
|
|
Analyzing . . .
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
) : (
|
|
<DataTableAll
|
|
data={dataPJParticipating}
|
|
columns={columnsPJParticipating}
|
|
size=""
|
|
height={300}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</Tabs.Panel>
|
|
</Tabs>
|
|
|
|
{/* 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 Review' : 'Update Review'}
|
|
</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
|
|
placeholder="Input name"
|
|
label={
|
|
<span>
|
|
Name project: <span style={{ color: 'red' }}>*</span>
|
|
</span>
|
|
}
|
|
mb={'md'}
|
|
value={form.values.name}
|
|
error={form.errors.name}
|
|
onChange={(e) => form.setFieldValue('name', e.target.value)}
|
|
/>
|
|
|
|
<TextInput
|
|
placeholder="Input role"
|
|
label={
|
|
<span>
|
|
Role: <span style={{ color: 'red' }}>*</span>
|
|
</span>
|
|
}
|
|
mb={'md'}
|
|
value={form.values.role}
|
|
error={form.errors.role}
|
|
onChange={(e) => form.setFieldValue('role', e.target.value)}
|
|
/>
|
|
<Textarea
|
|
placeholder="Input notes"
|
|
rows={4}
|
|
label={
|
|
<span>
|
|
Note: <span style={{ color: 'red' }}>*</span>
|
|
</span>
|
|
}
|
|
mb={'md'}
|
|
value={form.values.note}
|
|
error={form.errors.note}
|
|
onChange={(e) => form.setFieldValue('note', e.target.value)}
|
|
/>
|
|
<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
|
|
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 review?
|
|
<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 StaffEvaluation
|