774 lines
22 KiB
TypeScript
774 lines
22 KiB
TypeScript
import {
|
|
getAllBoardByIdProject,
|
|
getAllCriteriasBySprint,
|
|
getAllProject,
|
|
getAllSprintByIdBoard,
|
|
updateSprintReview,
|
|
} from '@/api/Admin'
|
|
import { get } from '@/rtk/helpers/apiService'
|
|
import { update } from '@/rtk/helpers/CRUD'
|
|
import {
|
|
Box,
|
|
Button,
|
|
Dialog,
|
|
Group,
|
|
Loader,
|
|
Select,
|
|
Table,
|
|
Text,
|
|
Textarea,
|
|
TextInput,
|
|
Title,
|
|
Tooltip,
|
|
} from '@mantine/core'
|
|
import { notifications } from '@mantine/notifications'
|
|
import { useEffect, useState } from 'react'
|
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
import classes from './SprintReview.module.css'
|
|
|
|
type DataProject = {
|
|
id: number
|
|
name: string
|
|
}
|
|
type DataSprint = {
|
|
id: number
|
|
name: string
|
|
|
|
startDate?: string
|
|
endDate?: string
|
|
completeDate?: string
|
|
}
|
|
|
|
type DataCriteriaSprint = {
|
|
[key: string]: string
|
|
// id?: number
|
|
// name: string
|
|
// expect: string
|
|
// actual: string
|
|
// note: string
|
|
// point?: string
|
|
}
|
|
|
|
interface DataCriteriaUsers {
|
|
[key: string]: string
|
|
}
|
|
// type DataCriteriaUsers = {
|
|
// id?: number
|
|
// name: string
|
|
// user: string
|
|
// created_by: string
|
|
// note: string
|
|
// point?: string
|
|
// }
|
|
|
|
const SprintReview = () => {
|
|
// const [users, setUsers] = useState<TUser[]>([])
|
|
const [action, setAction] = useState('')
|
|
// const [info, setInfo] = useState('')
|
|
const [activeBtn, setActiveBtn] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
const [loadingProject, setLoadingProject] = useState(true)
|
|
const [isDisabledSprint, setIsDisabledSprint] = useState(true)
|
|
const [criteriaSprint, setCriteriaSprint] = useState<DataCriteriaSprint[]>([])
|
|
const [criteriaUsers, setCriteriaUsers] = useState<DataCriteriaUsers[]>([])
|
|
const [infoBoard, setInfoBoard] = useState('')
|
|
const [filter, setFilter] = useState({
|
|
projectID: '',
|
|
sprintID: '',
|
|
})
|
|
const [dataProject, setDataProject] = useState<DataProject[]>([])
|
|
const [dataSprint, setDataSprint] = useState<DataSprint[]>([])
|
|
const [finalPoint, setFinalPoint] = useState('')
|
|
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
|
|
useEffect(() => {
|
|
const searchParams = new URLSearchParams(location.search)
|
|
const allParams: { [key: string]: string } = {}
|
|
|
|
searchParams.forEach((value, key) => {
|
|
allParams[key] = value
|
|
})
|
|
|
|
const newFilter = {
|
|
projectID: allParams.projectID || '',
|
|
sprintID: allParams.sprintID || '',
|
|
}
|
|
|
|
if (!allParams.projectID) {
|
|
searchParams.delete('sprintID')
|
|
}
|
|
|
|
setFilter(newFilter)
|
|
navigate(`?${searchParams.toString()}`)
|
|
}, [location.search]) // Mỗi khi URL thay đổi, cập nhật giá trị
|
|
|
|
const handleSelectChangeSprint = (sprintID: any) => {
|
|
const searchParams = new URLSearchParams(location.search)
|
|
if (sprintID) {
|
|
searchParams.set('sprintID', sprintID)
|
|
} else {
|
|
searchParams.delete('sprintID')
|
|
}
|
|
if (filter.projectID) {
|
|
searchParams.set('projectID', filter.projectID)
|
|
}
|
|
navigate(`?${searchParams.toString()}`)
|
|
}
|
|
|
|
const handleSelectChangeProject = (projectID: any) => {
|
|
if (projectID === null) {
|
|
setFilter({ projectID: '', sprintID: '' })
|
|
setInfoBoard('')
|
|
} else {
|
|
setFilter({ projectID: projectID, sprintID: '' })
|
|
}
|
|
setDataSprint([])
|
|
setIsDisabledSprint(true)
|
|
const searchParams = new URLSearchParams(location.search)
|
|
|
|
if (projectID) {
|
|
searchParams.set('projectID', projectID)
|
|
} else {
|
|
searchParams.delete('projectID')
|
|
searchParams.delete('sprintID')
|
|
}
|
|
navigate(`?${searchParams.toString()}`)
|
|
}
|
|
|
|
const getListDataProject = async () => {
|
|
try {
|
|
const params = {}
|
|
const res = await get(getAllProject, params)
|
|
if (res.status) {
|
|
return res.data
|
|
}
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
const getBoardByIdProject = async (id: string) => {
|
|
try {
|
|
const params = {}
|
|
const res = await get(`${getAllBoardByIdProject}?id=${id}`, params)
|
|
if (res.status) {
|
|
return res.data
|
|
}
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
const getListDataSprint = async (id: string) => {
|
|
try {
|
|
const params = {}
|
|
const res = await get(`${getAllSprintByIdBoard}?id=${id}`, params)
|
|
if (res.status) {
|
|
return res.data
|
|
}
|
|
} catch (error: any) {
|
|
notifications.show({
|
|
title: 'Error',
|
|
message: error.message ?? error,
|
|
color: 'red',
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
const getListCriteriasBySprint = async (id: string) => {
|
|
try {
|
|
const params = {}
|
|
const res = await get(`${getAllCriteriasBySprint}/${id}`, 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 getListDataProject()
|
|
setDataProject(result ?? [])
|
|
setLoadingProject(false)
|
|
}
|
|
fetchData()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (filter.projectID) {
|
|
setLoading(true)
|
|
const fetchData = async () => {
|
|
const result = await getBoardByIdProject(filter.projectID)
|
|
if (result.values[0]) {
|
|
setInfoBoard(result.values[0].id ?? '')
|
|
setLoading(false)
|
|
}
|
|
}
|
|
fetchData()
|
|
} else {
|
|
setIsDisabledSprint(true)
|
|
setDataSprint([])
|
|
}
|
|
}, [filter.projectID])
|
|
|
|
useEffect(() => {
|
|
if (infoBoard != '') {
|
|
const fetchData = async () => {
|
|
const result = await getListDataSprint(infoBoard)
|
|
if (result.values) {
|
|
setDataSprint(
|
|
result.values.filter(
|
|
(item: any) =>
|
|
typeof item.completeDate !== 'undefined' &&
|
|
item.completeDate !== '',
|
|
) ?? [],
|
|
)
|
|
setIsDisabledSprint(false)
|
|
}
|
|
}
|
|
fetchData()
|
|
}
|
|
}, [infoBoard])
|
|
|
|
useEffect(() => {
|
|
setLoading(true)
|
|
setCriteriaSprint([])
|
|
setCriteriaUsers([])
|
|
if (filter.sprintID) {
|
|
const fetchData = async () => {
|
|
const result = await getListCriteriasBySprint(filter.sprintID)
|
|
if (result) {
|
|
setCriteriaSprint(result.criterias ?? [])
|
|
setCriteriaUsers(result.users ?? [])
|
|
setFinalPoint(result.point ?? '')
|
|
setLoading(false)
|
|
}
|
|
}
|
|
fetchData()
|
|
} else {
|
|
setLoading(false)
|
|
}
|
|
}, [filter])
|
|
|
|
const getAll = () => {
|
|
const fetchData = async () => {
|
|
const result = await getListCriteriasBySprint(filter.sprintID)
|
|
if (result) {
|
|
setCriteriaSprint(result.criterias ?? [])
|
|
setCriteriaUsers(result.users ?? [])
|
|
setFinalPoint(result.point ?? '')
|
|
setLoading(false)
|
|
}
|
|
}
|
|
fetchData()
|
|
}
|
|
|
|
const handleUpdate = async () => {
|
|
const dataSprintSearch = dataSprint.find(
|
|
(item: DataSprint) => item.id == Number(filter.sprintID),
|
|
)
|
|
|
|
const values = {
|
|
name: dataSprint.find((item: any) => item.id == filter.sprintID)?.name,
|
|
|
|
start_date: dataSprintSearch?.startDate,
|
|
end_date: dataSprintSearch?.endDate,
|
|
complete_date: dataSprintSearch?.completeDate,
|
|
|
|
project_id: filter.projectID,
|
|
criterias: criteriaSprint,
|
|
users: criteriaUsers,
|
|
finalPoint: finalPoint,
|
|
}
|
|
try {
|
|
const res = await update(
|
|
`${updateSprintReview}/${filter.sprintID}`,
|
|
values,
|
|
getAll,
|
|
)
|
|
if (res === true) {
|
|
setAction('')
|
|
}
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
|
|
const pointsOptions = [
|
|
{ value: '1', label: '1' },
|
|
{ value: '2', label: '2' },
|
|
{ value: '3', label: '3' },
|
|
{ value: '4', label: '4' },
|
|
{ value: '5', label: '5' },
|
|
]
|
|
|
|
const rowStyle = {
|
|
height: '30px', // Điều chỉnh chiều cao hàng
|
|
}
|
|
|
|
const handleInputChange = (
|
|
index: number,
|
|
field: keyof DataCriteriaSprint,
|
|
value: string,
|
|
) => {
|
|
const updatedCriteria = [...criteriaSprint]
|
|
updatedCriteria[index][field] = value
|
|
setCriteriaSprint(updatedCriteria)
|
|
}
|
|
|
|
const handleInputChangeUsers = (
|
|
index: number,
|
|
field: keyof DataCriteriaUsers,
|
|
value: any,
|
|
) => {
|
|
const updatedCriteria = [...criteriaUsers]
|
|
updatedCriteria[index][field] = value
|
|
setCriteriaUsers(updatedCriteria)
|
|
}
|
|
const calculateRowSpan = (users: DataCriteriaUsers[], user_id: any) => {
|
|
return users.filter((item) => item.user_id === user_id).length
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className={classes.title}>
|
|
<h3>
|
|
<Text>Admin/</Text>
|
|
Sprint Review
|
|
</h3>
|
|
<Button
|
|
onClick={() => {
|
|
setAction('update')
|
|
}}
|
|
style={{
|
|
display: filter.sprintID ? 'block' : 'none',
|
|
}}
|
|
>
|
|
Update
|
|
</Button>
|
|
</div>
|
|
<Box
|
|
style={{
|
|
marginTop: '20%',
|
|
textAlign: 'center',
|
|
display: loadingProject ? 'block' : 'none',
|
|
// display: 'none',
|
|
}}
|
|
>
|
|
<Loader size={'sm'} color="green" type="bars" m={'0 auto'} />
|
|
<Text fw={600} c={'gray'}>
|
|
Loading . . .
|
|
</Text>
|
|
</Box>
|
|
<Box display={'flex'} p={15}>
|
|
<Box
|
|
style={[
|
|
{
|
|
display: 'flex',
|
|
flexFlow: 'column',
|
|
},
|
|
loadingProject ? { display: 'none' } : {},
|
|
]}
|
|
w={'50%'}
|
|
>
|
|
<Box w="100%" display={'flex'}>
|
|
<Text
|
|
mr={'xs'}
|
|
style={{ alignContent: 'center' }}
|
|
fw={600}
|
|
size={'md'}
|
|
>
|
|
Project:
|
|
</Text>
|
|
<Select
|
|
searchable
|
|
clearable
|
|
w="60%"
|
|
value={filter.projectID || null}
|
|
size="xs"
|
|
label=""
|
|
data={dataProject.map((project) => ({
|
|
value: project.id.toString(),
|
|
label: project.name,
|
|
}))}
|
|
onChange={(e) => {
|
|
handleSelectChangeProject(e)
|
|
}}
|
|
></Select>
|
|
<Text
|
|
ml={'lg'}
|
|
style={{ alignContent: 'center' }}
|
|
fw={600}
|
|
size={'md'}
|
|
>
|
|
Sprint:
|
|
</Text>
|
|
<Select
|
|
disabled={isDisabledSprint}
|
|
searchable
|
|
clearable
|
|
w="40%"
|
|
value={filter.sprintID || null} // Đảm bảo rằng nếu filter.sprint trống, thì giá trị sẽ là null
|
|
size="xs"
|
|
ml={'sm'}
|
|
label=""
|
|
data={dataSprint.map((sprint) => ({
|
|
value: sprint.id.toString(),
|
|
label: sprint.name,
|
|
}))}
|
|
onChange={(e) => {
|
|
setFilter({ ...filter, sprintID: e! })
|
|
handleSelectChangeSprint(e) // Gọi hàm để cập nhật URL với sprintID
|
|
}}
|
|
></Select>
|
|
</Box>
|
|
</Box>
|
|
<Box
|
|
style={{
|
|
display: 'flex',
|
|
flexFlow: 'column',
|
|
// backgroundColor: 'yellow',
|
|
alignSelf: 'center',
|
|
}}
|
|
w={'50%'}
|
|
>
|
|
<Text style={{ textAlign: 'center' }} fw={600}>
|
|
{
|
|
dataProject.find((item: any) => item.id === filter.projectID)
|
|
?.name
|
|
}{' '}
|
|
{dataSprint.find((item: any) => item.id === Number(filter.sprintID))
|
|
?.name
|
|
? '/'
|
|
: ''}{' '}
|
|
{
|
|
dataSprint.find((item) => item.id === Number(filter.sprintID))
|
|
?.name
|
|
}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Box>
|
|
{/* Tiêu đề Criteria for Sprint */}
|
|
<Title
|
|
order={4}
|
|
ml="xs"
|
|
style={
|
|
criteriaSprint.length == 0 || loading ? { display: 'none' } : {}
|
|
}
|
|
>
|
|
Criteria for Sprint: <Box fs={'italic'} fz={'sm'}>
|
|
<a href={`/test-report?projectID=${filter.projectID}&sprintID=${filter.sprintID}`} target='_blank'>Sprint test report</a>
|
|
</Box>
|
|
</Title>
|
|
<Box
|
|
style={{
|
|
marginTop: '20%',
|
|
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>
|
|
{/* Bảng Criteria for Sprint */}
|
|
<Table
|
|
striped
|
|
highlightOnHover
|
|
withTableBorder
|
|
withColumnBorders
|
|
// me={'xs'}
|
|
style={
|
|
criteriaSprint.length == 0 || loading ? { display: 'none' } : {}
|
|
}
|
|
>
|
|
<Table.Thead>
|
|
<Table.Tr bg="#228be66b" style={rowStyle}>
|
|
<Table.Th style={{ textAlign: 'center', width: '15%' }}>
|
|
Criteria
|
|
</Table.Th>
|
|
<Table.Th style={{ textAlign: 'center', width: '25%' }}>
|
|
Expect Result
|
|
</Table.Th>
|
|
<Table.Th style={{ textAlign: 'center', width: '25%' }}>
|
|
Actual Result
|
|
</Table.Th>
|
|
<Table.Th style={{ textAlign: 'center', width: '30%' }}>Note</Table.Th>
|
|
<Table.Th style={{ textAlign: 'center', width: '5%' }}>
|
|
Point
|
|
</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{criteriaSprint.map((item, index) => (
|
|
<Table.Tr key={index} style={rowStyle}>
|
|
{/* TextInput cho trường name */}
|
|
<Table.Td>
|
|
{/* <TextInput
|
|
value={item.name}
|
|
onChange={(e) =>
|
|
handleInputChange(index, 'name', e.target.value)
|
|
}
|
|
/> */}
|
|
<Tooltip multiline label={item.description}>
|
|
<span>{item.name}</span>
|
|
</Tooltip>
|
|
</Table.Td>
|
|
{/* TextInput cho trường expect */}
|
|
<Table.Td>
|
|
<TextInput
|
|
value={item.expect}
|
|
onChange={(e) =>
|
|
handleInputChange(index, 'expect', e.target.value)
|
|
}
|
|
/>
|
|
</Table.Td>
|
|
{/* TextInput cho trường actual */}
|
|
<Table.Td>
|
|
<TextInput
|
|
value={item.actual}
|
|
onChange={(e) =>
|
|
handleInputChange(index, 'actual', e.target.value)
|
|
}
|
|
/>
|
|
</Table.Td>
|
|
{/* TextInput cho trường note */}
|
|
<Table.Td>
|
|
<Textarea
|
|
autosize
|
|
placeholder="Good performance"
|
|
value={item.note}
|
|
onChange={(e) =>
|
|
handleInputChange(index, 'note', e.target.value)
|
|
}
|
|
minRows={2}
|
|
maxRows={4}
|
|
/>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Select
|
|
data={pointsOptions}
|
|
defaultValue=""
|
|
size="sm"
|
|
value={item.point == '' ? null : String(item.point)}
|
|
onChange={(value) =>
|
|
handleInputChange(index, 'point', value ?? '')
|
|
}
|
|
/>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
{criteriaSprint.length > 0 && (
|
|
<Table.Tr style={rowStyle}>
|
|
<Table.Td
|
|
colSpan={1}
|
|
style={{ textAlign: 'center' }}
|
|
bg="#d4eac7"
|
|
>
|
|
<Text fw={600} color="red">
|
|
{' '}
|
|
Final result{' '}
|
|
</Text>
|
|
</Table.Td>
|
|
<Table.Td bg="#d4eac7">
|
|
<Select
|
|
data={pointsOptions}
|
|
defaultValue={'0'}
|
|
value={finalPoint}
|
|
onChange={(value) => setFinalPoint(value ?? '')}
|
|
size="xs"
|
|
w={'40%'}
|
|
styles={() => ({
|
|
input: {
|
|
color: 'red',
|
|
fontWeight: '600',
|
|
fontSize: 'initial',
|
|
},
|
|
})}
|
|
/>
|
|
</Table.Td>
|
|
<Table.Td colSpan={3}></Table.Td>
|
|
</Table.Tr>
|
|
)}
|
|
</Table.Tbody>
|
|
</Table>
|
|
|
|
{/* Tiêu đề Criteria for Users */}
|
|
<Title
|
|
order={4}
|
|
ml="xs"
|
|
mt={'xs'}
|
|
style={
|
|
criteriaUsers.length == 0 || loading ? { display: 'none' } : {}
|
|
}
|
|
>
|
|
Criteria for Users:
|
|
</Title>
|
|
|
|
{/* Criteria for Users Table */}
|
|
<Table
|
|
striped
|
|
highlightOnHover
|
|
withTableBorder
|
|
withColumnBorders
|
|
// mt="md"
|
|
style={
|
|
criteriaUsers.length == 0 || loading ? { display: 'none' } : {}
|
|
}
|
|
>
|
|
<Table.Thead>
|
|
<Table.Tr bg="#228be66b" style={rowStyle}>
|
|
<Table.Th style={{ textAlign: 'center', width: '15%' }}>
|
|
User
|
|
</Table.Th>
|
|
<Table.Th style={{ textAlign: 'center', width: '25%' }}>
|
|
Criteria
|
|
</Table.Th>
|
|
<Table.Th style={{ textAlign: 'center', width: '40%' }}>Note</Table.Th>
|
|
<Table.Th style={{ textAlign: 'center', width: '15%' }}>
|
|
Created by
|
|
</Table.Th>
|
|
<Table.Th style={{ textAlign: 'center', width: '5%' }}>
|
|
Point
|
|
</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{(() => {
|
|
let alreadyDisplayed: number[] = []
|
|
return criteriaUsers.map((item: any, index) => {
|
|
// Tính rowSpan cho người dùng hiện tại
|
|
const rowSpan = calculateRowSpan(criteriaUsers, item.user_id)
|
|
|
|
const checkUser = alreadyDisplayed.find(
|
|
(i: any) => i == item.user_id,
|
|
)
|
|
? true
|
|
: false
|
|
|
|
alreadyDisplayed.push(item.user_id)
|
|
|
|
return (
|
|
<Table.Tr key={index} style={rowStyle}>
|
|
{!checkUser && (
|
|
<Table.Td rowSpan={rowSpan}>{item.user}</Table.Td>
|
|
)}
|
|
|
|
<Table.Td>
|
|
<Tooltip multiline label={item.description}>
|
|
<span>{item.name}</span>
|
|
</Tooltip>
|
|
</Table.Td>
|
|
|
|
<Table.Td>
|
|
<Textarea
|
|
autosize
|
|
placeholder="Good performance"
|
|
value={item.note}
|
|
onChange={(e) =>
|
|
handleInputChangeUsers(index, 'note', e.target.value)
|
|
}
|
|
minRows={2}
|
|
maxRows={4}
|
|
/>
|
|
</Table.Td>
|
|
|
|
<Table.Td>
|
|
<TextInput
|
|
value={item.created_by}
|
|
onChange={(e) =>
|
|
handleInputChangeUsers(
|
|
index,
|
|
'created_by',
|
|
e.target.value,
|
|
)
|
|
}
|
|
/>
|
|
</Table.Td>
|
|
|
|
<Table.Td>
|
|
<Select
|
|
data={pointsOptions}
|
|
defaultValue=""
|
|
size="sm"
|
|
value={item.point == '' ? null : String(item.point)}
|
|
onChange={(value) =>
|
|
handleInputChangeUsers(index, 'point', value)
|
|
}
|
|
/>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
)
|
|
})
|
|
})()}
|
|
</Table.Tbody>
|
|
</Table>
|
|
</Box>
|
|
<Dialog
|
|
className={classes.dialog}
|
|
opened={action === 'update'}
|
|
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 update?
|
|
<Group justify="center" m={10}>
|
|
<Button
|
|
disabled={activeBtn}
|
|
fw={700}
|
|
size="xs"
|
|
variant="light"
|
|
onClick={async () => {
|
|
setLoading(true)
|
|
setActiveBtn(true)
|
|
await handleUpdate()
|
|
setActiveBtn(false)
|
|
setAction('')
|
|
}}
|
|
>
|
|
Yes
|
|
</Button>
|
|
<Button
|
|
fw={700}
|
|
size="xs"
|
|
variant="light"
|
|
onClick={() => setAction('')}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</Group>
|
|
</Text>
|
|
</Dialog>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default SprintReview
|