Thực hiện tạo page Sprint Review

This commit is contained in:
Truong Vo 2024-09-13 01:39:45 +07:00
parent 16af707711
commit e4745f1636
8 changed files with 650 additions and 74 deletions

View File

@ -20,7 +20,7 @@ return new class extends Migration
$table->string('note')->nullable(); $table->string('note')->nullable();
$table->string('created_by'); $table->string('created_by');
$table->foreign('sprint_id')->references('id')->on('sprints')->onDelete('cascade'); $table->foreign('sprint_id')->references('id')->on('sprint')->onDelete('cascade');
$table->foreign('criteria_id')->references('id')->on('criterias')->onDelete('cascade'); $table->foreign('criteria_id')->references('id')->on('criterias')->onDelete('cascade');
}); });
} }

View File

@ -24,7 +24,7 @@ return new class extends Migration
$table->unsignedBigInteger('sprint_id'); $table->unsignedBigInteger('sprint_id');
$table->string('created_by'); $table->string('created_by');
$table->foreign('sprint_id')->references('id')->on('sprints')->onDelete('cascade'); $table->foreign('sprint_id')->references('id')->on('sprint')->onDelete('cascade');
}); });
} }

View File

@ -21,7 +21,7 @@ return new class extends Migration
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('criteria_id')->references('id')->on('criterias')->onDelete('cascade'); $table->foreign('criteria_id')->references('id')->on('criterias')->onDelete('cascade');
$table->foreign('sprint_id')->references('id')->on('sprints')->onDelete('cascade'); $table->foreign('sprint_id')->references('id')->on('sprint')->onDelete('cascade');
}); });
} }

View File

@ -40,6 +40,7 @@ import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput' import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
import classes from './NavbarSimpleColored.module.css' import classes from './NavbarSimpleColored.module.css'
import { IconListCheck } from '@tabler/icons-react'
const data = [ const data = [
// { link: '/dashboard', label: 'Dashboard', icon: IconHome }, // { link: '/dashboard', label: 'Dashboard', icon: IconHome },
@ -79,7 +80,13 @@ const data = [
label: 'Users Management', label: 'Users Management',
icon: IconUsersGroup, icon: IconUsersGroup,
group: 'admin', group: 'admin',
} },
{
link: '/sprint-review',
label: 'Sprint Review',
icon: IconListCheck,
group: 'admin',
},
// { link: '/jira', label: 'Jira', icon: IconSubtask }, // { link: '/jira', label: 'Jira', icon: IconSubtask },
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush }, // { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
// { link: '/general-setting', label: 'General Setting', icon: IconSettings }, // { link: '/general-setting', label: 'General Setting', icon: IconSettings },

View File

@ -0,0 +1,47 @@
.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 {
color: red;
cursor: pointer;
padding: 2px;
border-radius: 25%;
}
.editIcon {
color: rgb(9, 132, 132);
cursor: pointer;
padding: 2px;
border-radius: 25%;
}
.editIcon:hover {
background-color: rgba(203, 203, 203, 0.809);
}
.deleteIcon:hover {
background-color: rgba(203, 203, 203, 0.809);
}
.dialog {
background-color: light-dark(white, #2d353c);
text-align: center;
border: solid 1px rgb(255, 145, 0);
}
.dialogText {
color: light-dark(#2d353c, white);
}

View File

@ -0,0 +1,495 @@
import { createOrUpdateUser, deleteUser } from '@/api/Admin'
import { ButtonCopy } from '@/components/CopyClipboard/CopyClipboard'
import { post } from '@/rtk/helpers/apiService'
import { update, Xdelete } from '@/rtk/helpers/CRUD'
import { TUser } from '@/variables/types'
import {
Box,
Button,
Code,
Dialog,
Group,
Modal,
MultiSelect,
Select,
Table,
Text,
TextInput,
Title,
} from '@mantine/core'
import { useForm } from '@mantine/form'
import { useEffect, useState } from 'react'
import classes from './SprintReview.module.css'
const SprintReview = () => {
// const [users, setUsers] = useState<TUser[]>([])
const [action, setAction] = useState('')
const [activeBtn, setActiveBtn] = useState(false)
// const [item, setItem] = useState({ id: 0 })
const [disableBtn, setDisableBtn] = useState(false)
const [info, setInfo] = useState('')
const [filter, setFilter] = useState({
typeReason: '',
statusFilter: '',
})
// const [dataTimeType, setDataTimeType] = useState<DataTimeType[]>([])
// const [dataReason, setDataReason] = useState<DataReason[]>([])
// const getListMasterByType = async (type: string) => {
// try {
// const params = {
// type: type,
// }
// const res = await get(getListMaster, 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 resultTimeType = await getListMasterByType('TIME_TYPE')
// setDataTimeType(
// resultTimeType.filter((item: DataTimeType) => item.c_code !== 'ALL'),
// )
// const resultReason = await getListMasterByType('REASON')
// setDataReason(resultReason)
// }
// fetchData()
// }, [])
const form = useForm({
initialValues: {
id: 0,
name: '',
email: '',
permission: '',
},
})
// const getAll = async () => {
// try {
// const res = await get(getAllUsers)
// if (res.status) {
// setUsers(res.data)
// }
// } catch (error) {
// console.log(error)
// }
// }
const handleCreate = async (values: TUser) => {
try {
const { id, ...data } = values
const res = await post(createOrUpdateUser, data)
if (res.status === true) {
setAction('review')
form.reset()
// getAll()
setInfo(JSON.stringify(res.data, null, 2))
}
} catch (error) {
console.log(error)
}
}
const handleUpdate = async (values: TUser) => {
try {
const res = await update(
createOrUpdateUser,
values,
// , getAll
)
if (res === true) {
setAction('')
form.reset()
}
} catch (error) {
console.log(error)
}
}
const handleDelete = async (id: number) => {
try {
await Xdelete(
deleteUser,
{ id: id },
// , getAll
)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
// getAll()
}, [])
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
}
return (
<div>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Sprint Review
</h3>
<Button
onClick={() => {
setAction('add')
form.reset()
}}
>
Update
</Button>
</div>
<Box display={'flex'} p={15}>
<Box
style={{
display: 'flex',
flexFlow: 'column',
}}
w={'40%'}
>
<Box w="100%" display={'flex'}>
<Text
mr={'xs'}
style={{ alignContent: 'center' }}
fw={600}
size={'md'}
>
Project:
</Text>
<Select
clearable
w="50%"
value={filter.typeReason}
size="xs"
label=""
data={[]}
onChange={(e) => {
setFilter({ ...filter, typeReason: e! })
}}
></Select>
<Text
ml={'lg'}
style={{ alignContent: 'center' }}
fw={600}
size={'md'}
>
Sprint:
</Text>
<Select
clearable
w="50%"
value={filter.statusFilter}
size="xs"
ml={'sm'}
label=""
data={
[
// { value: 'WAITING', label: 'WAITING' },
// { value: 'CONFIRMED', label: 'CONFIRMED' },
// { value: 'REFUSED', label: 'REFUSED' },
]
}
onChange={(e) => {
setFilter({ ...filter, statusFilter: e! })
}}
></Select>
</Box>
</Box>
<Box
style={{
display: 'flex',
flexFlow: 'column',
// backgroundColor: 'yellow',
alignSelf: 'center',
}}
w={'60%'}
>
<Text style={{ textAlign: 'center' }} fw={600}>
Project Name/ Sprint Name
</Text>
</Box>
</Box>
<Box>
{/* Tiêu đề Criteria for Sprint */}
<Title order={5} ml="xs">
Criteria for Sprint:
</Title>
{/* Bảng Criteria for Sprint */}
<Table
striped
highlightOnHover
withTableBorder
withColumnBorders
// me={'xs'}
>
<Table.Thead>
<Table.Tr bg="#228be66b" style={rowStyle}>
<Table.Th style={{ textAlign: 'center', width: '250px' }}>
Criteria
</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '250px' }}>
Expect Result
</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '250px' }}>
Actual Result
</Table.Th>
<Table.Th style={{ textAlign: 'center' }}>Note</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '100px' }}>
Point
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{[...Array(5)].map((_, index) => (
<Table.Tr key={index} style={rowStyle}>
<Table.Td>string</Table.Td>
<Table.Td>string</Table.Td>
<Table.Td>string</Table.Td>
<Table.Td>
string (set default value when select point)
</Table.Td>
<Table.Td>
<Select data={pointsOptions} defaultValue="1" size="xs" />
</Table.Td>
</Table.Tr>
))}
<Table.Tr bg="#d4eac7" style={rowStyle}>
<Table.Td
colSpan={1}
style={{ textAlign: 'center' }}
bg="#d4eac7"
>
Final result
</Table.Td>
<Table.Td bg="#d4eac7">
<Select data={pointsOptions} defaultValue="3" size="xs" />
</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'}>
Criteria for Users:
</Title>
{/* Criteria for Users Table */}
<Table
striped
highlightOnHover
withTableBorder
withColumnBorders
// mt="md"
>
<Table.Thead>
<Table.Tr bg="#228be66b" style={rowStyle}>
<Table.Th style={{ textAlign: 'center', width: '250px' }}>
User
</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '250px' }}>
Criteria
</Table.Th>
<Table.Th style={{ textAlign: 'center' }}>Note</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '250px' }}>
Created by
</Table.Th>
<Table.Th style={{ textAlign: 'center', width: '100px' }}>
Point
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{[...Array(5)].map((_, index) => (
<Table.Tr key={index} style={rowStyle}>
<Table.Td>string</Table.Td>
<Table.Td>string</Table.Td>
<Table.Td>
string (set default value when select point)
</Table.Td>
<Table.Td>string</Table.Td>
<Table.Td>
<Select data={pointsOptions} defaultValue="1" size="xs" />
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Box>
{/* 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 User' : 'Update User'}
</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
label="Name"
mb={'md'}
value={form.values.name}
error={form.errors.name}
onChange={(e) => form.setFieldValue('name', e.target.value)}
/>
<TextInput
label="Email"
mb={'md'}
value={form.values.email}
error={form.errors.email}
onChange={(e) => form.setFieldValue('email', e.target.value)}
/>
<MultiSelect
label={'Permission(s)'}
required
data={['staff', 'admin', 'hr']}
value={
typeof form.values.permission === 'string'
? form.values.permission
.split(',')
.filter((p) => p.trim() !== '')
: form.values.permission
}
error={form.errors.permisstion}
onChange={(e) =>
form.setFieldValue(
'permission',
e!.filter((p) => p.trim() !== '').join(','),
)
}
/>
<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>
<Modal
opened={action === 'review'}
onClose={() => {
setAction('')
}}
size={'lg'}
title={
<Text pl={'sm'} fw={700} fz={'lg'}>
Information to get started
</Text>
}
>
<Text c={'red'} fz={'sm'} fw={700}>
!! Important note: Please remind user to change password after logging
in !!
</Text>
<Code block>
<Box ta={'right'}>
<ButtonCopy value={info} />
</Box>
{info}
</Code>
</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 user?
<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 SprintReview

View File

@ -5,6 +5,7 @@ import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
import PageLogin from '@/pages/Auth/Login/Login' import PageLogin from '@/pages/Auth/Login/Login'
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement' import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
import PageNotFound from '@/pages/NotFound/NotFound' import PageNotFound from '@/pages/NotFound/NotFound'
import SprintReview from '@/pages/SprintReview/SprintReview'
import Tickets from '@/pages/Tickets/Tickets' import Tickets from '@/pages/Tickets/Tickets'
import TicketsManagement from '@/pages/TicketsManagement/TicketsManagement' import TicketsManagement from '@/pages/TicketsManagement/TicketsManagement'
import Timekeeping from '@/pages/Timekeeping/Timekeeping' import Timekeeping from '@/pages/Timekeeping/Timekeeping'
@ -137,7 +138,21 @@ const mainRoutes = [
<BasePage <BasePage
main={ main={
<> <>
<UsersManagement/> <UsersManagement />
</>
}
></BasePage>
</ProtectedRoute>
),
},
{
path: '/sprint-review',
element: (
<ProtectedRoute mode="route" permission="admin,hr">
<BasePage
main={
<>
<SprintReview />
</> </>
} }
></BasePage> ></BasePage>

View File

@ -1,79 +1,91 @@
export type TGeneralSettingValues = { export type TGeneralSettingValues = {
page_title: string page_title: string
email: string email: string
phone: string phone: string
meta_title: string meta_title: string
meta_description: string meta_description: string
meta_keyword: string meta_keyword: string
facebook: string facebook: string
twitter: string twitter: string
linkedin: string linkedin: string
favicon: File | null | string favicon: File | null | string
logo: File | null | string logo: File | null | string
address: string address: string
license: string license: string
} }
export type TPackageValues = { export type TPackageValues = {
id?: number id?: number
title: string title: string
description: string description: string
point: number | string point: number | string
price: number | string price: number | string
status?: boolean status?: boolean
} }
export type TDiscountValues = { export type TDiscountValues = {
id?: number id?: number
code: string code: string
value: string value: string
active_date: any | null active_date: any | null
expiry: any | null expiry: any | null
date_used: any | null date_used: any | null
status?: boolean status?: boolean
discount_type: string discount_type: string
} }
export type TClientValues = { export type TClientValues = {
email: string email: string
name: string name: string
phone: string phone: string
company: string company: string
country_code: string country_code: string
password: string password: string
confirm_password: string confirm_password: string
status: boolean status: boolean
point: string point: string
} }
export type TOrderValues = { export type TOrderValues = {
id?: number id?: number
payment_id: string payment_id: string
point: string point: string
total_price: string total_price: string
user_id: string user_id: string
package_id: string package_id: string
discount: string discount: string
status: string status: string
created_at?: string created_at?: string
updated_at?: string updated_at?: string
user_name?: string user_name?: string
avatar?: string avatar?: string
user_email?: string user_email?: string
user_phone?: string user_phone?: string
user_company?: string user_company?: string
user_country?: string user_country?: string
country_code?: string country_code?: string
discount_value?: string discount_value?: string
discount_type?: string discount_type?: string
package_name?: string package_name?: string
package_price?: string package_price?: string
discounted_package_price?: string discounted_package_price?: string
} }
export type TUser = { export type TUser = {
id: number; id: number
email: string; email: string
name: string; name: string
permission: string permission: string
} }
export type DataReason = {
id: number
c_code: string
c_name: string
}
export type DataTimeType = {
id: number
c_code: string
c_name: string
}