ManagementSystem/FRONTEND/src/pages/SprintReview/SprintReview.tsx

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ẽ 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