Merge pull request 'Sprint-3/MS-28' (#24) from Sprint-3/MS-28 into master

Reviewed-on: #24
This commit is contained in:
joseph 2024-09-16 10:45:23 +10:00
commit a823b6812f
5 changed files with 819 additions and 27 deletions

View File

@ -5,6 +5,7 @@ import { get, post } from '@/rtk/helpers/apiService'
import { requirementsPassword } from '@/rtk/helpers/variables'
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
import {
Avatar,
Box,
Button,
Code,
@ -94,6 +95,12 @@ const data = [
icon: IconZoomExclamation,
group: 'admin',
},
{
link: '/profile',
label: 'Profile',
icon: IconZoomExclamation,
group: 'hidden',
},
// { link: '/jira', label: 'Jira', icon: IconSubtask },
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
// { link: '/general-setting', label: 'General Setting', icon: IconSettings },
@ -338,22 +345,87 @@ const Navbar = ({
>
<div className={classes.navbarMain}>
<Group className={classes.header} justify="space-between">
<Code fw={700} className={classes.version}>
{user.user.name}
</Code>
<Tooltip label="Your QR code" fz={'xs'}>
<IconQrcode
onClick={() => renderQRCode()}
color="#fff164"
width={28}
height={28}
style={{
border: 'solid 2px orange',
borderRadius: '5px',
cursor: 'pointer',
}}
/>
</Tooltip>
<Box display={'flex'} w={'100%'}>
<Box style={{ display: 'flex', flexFlow: 'row' }} w={'100%'}>
<Box
w={'90%'}
style={{
display: 'flex',
flexFlow: 'column',
alignItems: 'center',
}}
>
<Avatar
src={user.user.avatarUrl || '/public/backgroundLogin.png'}
alt="User Avatar"
size={60}
radius="xl"
mb={5}
onClick={() => navigate('/profile')}
style={{
cursor: 'pointer',
}}
/>
<Code
fw={700}
className={classes.version}
onClick={() => navigate('/profile')}
style={{
cursor: 'pointer',
}}
>
{user.user.name}
</Code>
</Box>
<Box
w={'10%'}
style={{
display: 'flex',
justifyContent: 'center',
}}
>
<Tooltip label="Your QR code" fz={'xs'}>
<IconQrcode
onClick={() => renderQRCode()}
color="#fff164"
width={28}
height={28}
style={{
border: 'solid 2px orange',
borderRadius: '5px',
cursor: 'pointer',
}}
/>
</Tooltip>
</Box>
{/* <Box
w={'100%'}
style={{
display: 'flex',
flexFlow: 'row',
justifyContent: 'space-between', // Aligns username to the left and QR code to the right
alignItems: 'center',
}}
>
<Code fw={700} className={classes.version}>
{user.user.name}
</Code>
<Tooltip label="Your QR code" fz={'xs'}>
<IconQrcode
onClick={() => renderQRCode()}
color="#fff164"
width={28}
height={28}
style={{
border: 'solid 2px orange',
borderRadius: '5px',
cursor: 'pointer',
}}
/>
</Tooltip>
</Box> */}
</Box>
</Box>
</Group>
{links}
</div>

View File

@ -0,0 +1,196 @@
: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;
text-align: center;
}
/* Sidebar section for user information */
.sidebar {
width: 30%;
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 {
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);
}
/* 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: var(--hover-background); */
transition: background-color 0.3s;
}
.projectHeader:hover {
background-color: var(--hover-background);
}
.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;
}
.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: 55px; /* 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,484 @@
import { changePassword } from '@/api/Auth'
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
import { logout } from '@/rtk/dispatches/auth'
import { post } from '@/rtk/helpers/apiService'
import { requirementsPassword } from '@/rtk/helpers/variables'
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
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 { useCallback, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import classes from './Profile.module.css'
interface TableRow {
// Add properties for each column in the table
criteria: string
note: string
createdBy: string
point: string
}
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' }}>Criteria</Table.Th>
<Table.Th style={{ textAlign: 'center' }}>Note</Table.Th>
<Table.Th style={{ textAlign: 'center' }}>Created by</Table.Th>
<Table.Th style={{ textAlign: 'center' }}>Point</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data.map((row, index) => (
<Table.Tr key={index}>
<Table.Td>{row.criteria}</Table.Td>
<Table.Td>{row.note}</Table.Td>
<Table.Td>{row.createdBy}</Table.Td>
<Table.Td>{row.point}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)
const projectsData = [
{
name: 'Project Name 1',
sprints: [
{
name: 'Sprint 1',
criterias: [
{
criteria: 'Criteria 1',
note: 'String (default when selecting point)',
createdBy: 'User 1',
point: '1-5',
},
{
criteria: 'Criteria 2',
note: 'String (default when selecting point)',
createdBy: 'User 2',
point: '1-5',
},
],
},
{
name: 'Sprint 2',
criterias: [
{
criteria: 'Criteria 3',
note: 'String (default when selecting point)',
createdBy: 'User 3',
point: '1-5',
},
{
criteria: 'Criteria 4',
note: 'String (default when selecting point)',
createdBy: 'User 4',
point: '1-5',
},
],
},
],
},
{
name: 'Project Name 2',
sprints: [
{
name: 'Sprint 1',
criterias: [
{
criteria: 'Criteria 7',
note: 'String (default when selecting point)',
createdBy: 'User 7',
point: '1-5',
},
],
},
],
},
]
const isCompactMenu = false
const Profile = () => {
const user = useAppSelector((state) => state.authentication.user)
const [opened, setOpened] = useState(false)
const [dataChange, setDataChange] = useState({
password: '',
new_password: '',
confirm_password: '',
})
const [loading, setLoading] = useState(false)
// const [projects, setProjects] = useState(projectsData)
const [expandedProjects, setExpandedProjects] = useState<ExpandedProjects>({})
const [expandedSprints, setExpandedSprints] = useState<ExpandedSprints>({})
const [countSpam, setCountSpam] = useState(0)
const navigate = useNavigate()
const dispatch = useAppDispatch()
const passwordRegex =
/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
// const { setColorScheme } = useMantineColorScheme()
// const computedColorScheme = useComputedColorScheme('light', {
// getInitialValueInEffect: true,
// })
const handleChangePassword = async () => {
try {
if (countSpam > 5) {
setLoading(true)
notifications.show({
title: 'Error',
message: 'Password error more than 5 times. Logout after 3s',
color: 'red',
})
setTimeout(() => {
localStorage.clear()
window.location.reload()
}, 3000)
} else {
setLoading(true)
const res = await post(
changePassword,
{
email: user.user.email,
password: dataChange.password,
new_password: dataChange.new_password,
confirm_password: dataChange.confirm_password,
},
undefined,
)
if (res.status) {
notifications.show({
title: 'Success',
message: 'Reset password success',
color: 'green',
})
setOpened(false)
setDataChange({
password: '',
new_password: '',
confirm_password: '',
})
handleLogout()
} else {
notifications.show({
title: 'Error',
message: res.errors.password,
color: 'red',
})
}
setCountSpam(countSpam + 1)
setLoading(false)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log(error)
if (error.response.status === 422) {
const errorMess = error.response.data.message
notifications.show({
title: 'Error',
message: errorMess,
color: 'red',
})
}
setLoading(false)
}
}
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>
<div className={classes.title}>
<h3>
<Text>Admin/</Text>
Profile
</h3>
</div>
<Box>
<Box className={classes.userInfoSection} display="flex">
<Box className={classes.sidebar}>
<Title order={3}>User Information</Title>
<Box
w={'100%'}
style={{
display: 'flex',
flexFlow: 'column',
alignItems: 'center',
}}
>
<Avatar
src={null || '/public/backgroundLogin.png'}
alt="User Avatar"
size={150}
// radius="xl"
mb={5}
mt={5}
onClick={() => {
console.log('update avatar')
}}
style={{
cursor: 'pointer',
// border: '2px solid black',
borderRadius: '50%',
}}
/>
</Box>
<Box className={classes.userInfo} mt="md">
<TextInput
label="Name"
placeholder="Name"
mb="md"
readOnly
style={{ pointerEvents: 'none' }}
/>
<TextInput
label="Email"
type="email"
placeholder="Email"
mb="md"
readOnly
style={{ pointerEvents: 'none' }}
/>
<TextInput
label="Join Date"
type="date"
mb="md"
readOnly
style={{ pointerEvents: 'none' }}
/>
<TextInput
label="Permission(s)"
placeholder="Permissions"
readOnly
style={{ pointerEvents: 'none' }}
/>
</Box>
<Box
w={'100%'}
style={{
display: 'flex',
flexFlow: 'column',
alignItems: 'center',
}}
mt={10}
>
<a
href="#"
className={classes.link}
onClick={() => setOpened(true)}
>
<IconPasswordUser className={classes.linkIcon} stroke={1.5} />
<span
className={`${classes.itemLabel} ${classes.labelCompactMenu}`}
>
Change password
</span>
</a>
</Box>
</Box>
<Box className={classes.projectInvolvement}>
<Title order={3}>Project Involved</Title>
<Box className={classes.project} mt="lg">
{projectsData.map((project) => (
<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) => (
<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) => ({
...criteria,
}))}
/>
</Box>
)}
</div>
))}
</Box>
)}
</div>
))}
</Box>
</Box>
</Box>
<Modal
opened={opened}
onClose={() => {
setOpened(false)
setDataChange({
password: '',
new_password: '',
confirm_password: '',
})
}}
title={
<Text
fw={700}
fz={'1.2rem'}
className={`${classes.itemLabel} ${
isCompactMenu ? classes.labelCompactMenu : ''
}`}
>
Change password
</Text>
}
>
<Box p="sm">
<TextInput
label="E-mail"
value={user.user.email}
disabled
mb={'md'}
></TextInput>
<PasswordInput
label="Current password"
required
placeholder="Current password"
maxLength={32}
value={dataChange.password}
onChange={(e) => {
setDataChange({ ...dataChange, password: e.target.value })
}}
error={
dataChange.password.length < 8 &&
dataChange.password !== '' &&
'Length 8 characters or more'
}
/>
<PasswordRequirementInput
requirements={requirementsPassword}
value={dataChange}
setValue={setDataChange}
label="New password"
placeholder="New password"
name="new_password"
mb="md"
/>
<PasswordInput
label="Confirm password"
required
placeholder="Confirm password"
maxLength={32}
value={dataChange.confirm_password}
onChange={(e) => {
setDataChange({
...dataChange,
confirm_password: e.target.value,
})
}}
error={
dataChange.new_password !== dataChange.confirm_password &&
dataChange.confirm_password !== '' &&
'Password do not match'
}
></PasswordInput>
<Button
style={{ float: 'right' }}
mb={'lg'}
mt={'lg'}
loading={loading}
disabled={
loading === false &&
dataChange.password !== '' &&
passwordRegex.test(dataChange.new_password) &&
dataChange.new_password === dataChange.confirm_password &&
dataChange.new_password !== ''
? false
: true
}
onClick={() => handleChangePassword()}
>
Update
</Button>
</Box>
</Modal>
</Box>
</div>
)
}
export default Profile

View File

@ -1,11 +1,4 @@
import {
Box,
Button,
Select,
Table,
Text,
Title
} from '@mantine/core'
import { Box, Button, Select, Table, Text, Title } from '@mantine/core'
import { notifications } from '@mantine/notifications'
import { useEffect, useState } from 'react'
import classes from './SprintReview.module.css'
@ -271,16 +264,48 @@ const SprintReview = () => {
</Table.Td>
</Table.Tr>
))}
<Table.Tr bg="#d4eac7" style={rowStyle}>
<Table.Tr style={rowStyle}>
<Table.Td
colSpan={1}
style={{ textAlign: 'center' }}
bg="#d4eac7"
>
Final result
<Text fw={600} color='red'> Final result </Text>
</Table.Td>
<Table.Td bg="#d4eac7">
<Select data={pointsOptions} defaultValue="3" size="xs" />
<Select
data={pointsOptions}
defaultValue="3"
size="xs"
// style={{
// color: 'red',
// fontWeight: '700',
// fontSize: 'initial',
// }}
styles={() => ({
input: {
// borderColor: theme.colors.blue[6], // Border color for the input
// '&:hover': {
// borderColor: theme.colors.blue[4], // Border color on hover
// },
color: 'red',
fontWeight: '600',
fontSize: 'initial',
},
// dropdown: {
// backgroundColor: theme.colors.gray[0], // Background color for the dropdown
// },
// item: {
// '&[data-selected]': {
// backgroundColor: theme.colors.blue[6], // Background color for the selected item
// color: theme.white, // Text color for the selected item
// },
// '&[data-hovered]': {
// backgroundColor: theme.colors.blue[6], // Background color for hovered items
// },
// },
})}
/>
</Table.Td>
<Table.Td colSpan={3}></Table.Td>
</Table.Tr>

View File

@ -5,6 +5,7 @@ import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
import PageLogin from '@/pages/Auth/Login/Login'
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 TestReport from '@/pages/TestReport/TestReport'
import Tickets from '@/pages/Tickets/Tickets'
@ -174,6 +175,20 @@ const mainRoutes = [
</ProtectedRoute>
),
},
{
path: '/profile',
element: (
<ProtectedRoute mode="route" permission="admin,hr">
<BasePage
main={
<>
<Profile />
</>
}
></BasePage>
</ProtectedRoute>
),
},
// {
// path: '/packages',
// element: (