reg: group actions, sort, pagination, search, validation

This commit is contained in:
иосиф брыков 2024-02-20 23:57:12 +05:00
parent 33183e250d
commit c655868fe0
19 changed files with 4311 additions and 275 deletions

3578
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^5.1.0" "vite": "^5.1.0",
"vite-plugin-pwa": "^0.19.0"
} }
} }

View File

@ -45,12 +45,12 @@ type Props = {
} }
export default function MainInput({value, onChange, placeholder, type}: Props) { export default function MainInput({value, onChange, placeholder, type}: Props) {
const inputRef = useRef<HTMLInputElement>(null) // Создаем ссылку для элемента ввода const inputRef = useRef<HTMLInputElement>(null)
const activateInput = () => { const activateInput = () => {
if (inputRef.current) { if (inputRef.current) {
inputRef.current.focus() // Устанавливаем фокус на элемент ввода inputRef.current.focus()
} }
} }
return ( return (

View File

@ -1,8 +1,8 @@
type Props = {} type Props = {}
import styled, { keyframes } from 'styled-components'; import styled, { keyframes } from 'styled-components'
const loadingSpinnerSize = '33px'; const loadingSpinnerSize = '33px'
const spinAnimation = keyframes` const spinAnimation = keyframes`
from { from {
@ -11,7 +11,7 @@ const spinAnimation = keyframes`
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
`; `
export const LoadingSpinner = styled.div` export const LoadingSpinner = styled.div`
position: relative; position: relative;
@ -21,68 +21,19 @@ export const LoadingSpinner = styled.div`
width: ${loadingSpinnerSize}; width: ${loadingSpinnerSize};
height: ${loadingSpinnerSize}; height: ${loadingSpinnerSize};
svg { img {
&:first-child { &:first-child {
animation: ${spinAnimation} 1s linear infinite; animation: ${spinAnimation} 1s linear infinite;
} }
position: absolute; position: absolute;
} }
`; `
export const LoaderModal = styled.div`
position: fixed;
top: 0;
right: 0;
height: 100vh;
width: 100vw;
background-color: rgba(255, 255, 255, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
& .loader-modal-container {
width: 100px;
height: 100px;
display: flex;
background: ${props => props.theme.bg};
justify-content: center;
align-items: center;
border-radius: 15px;
${props => props.theme.mainShadow}
}
`;
export default function Loader({}: Props) { export default function Loader({}: Props) {
return <LoaderModal> return (
<div className='loader-modal-container'> <LoadingSpinner>
<LoadingSpinner> <img src="/src/images/loader/Spiner_1.svg" alt="" />
<svg <img src="/src/images/loader/Spiner_2.svg" alt="" />
xmlns='http://www.w3.org/2000/svg' </LoadingSpinner>
width='34' )
height='34'
viewBox='0 0 34 34'
fill='none'
>
<path
d='M30.8032 16.7037C32.2414 16.7037 33.4281 15.5308 33.2048 14.11C33.0003 12.809 32.6422 11.5338 32.1359 10.3115C31.2965 8.28488 30.0661 6.44348 28.515 4.8924C26.9639 3.34132 25.1225 2.11093 23.0959 1.27149C21.8736 0.765196 20.5984 0.407096 19.2974 0.202604C17.8766 -0.0207325 16.7037 1.16596 16.7037 2.60425C16.7037 4.04254 17.8842 5.17917 19.2857 5.50224C19.905 5.64498 20.5128 5.83917 21.1027 6.08352C22.4974 6.66121 23.7646 7.50794 24.832 8.57536C25.8995 9.64279 26.7462 10.91 27.3239 12.3047C27.5682 12.8946 27.7624 13.5024 27.9052 14.1217C28.2282 15.5232 29.3649 16.7037 30.8032 16.7037Z'
fill='#006666'
/>
</svg>
<svg
xmlns='http://www.w3.org/2000/svg'
width='34'
height='34'
viewBox='0 0 34 34'
fill='none'
>
<path
opacity='0.3'
d='M33.4814 17.2591C33.4814 26.4843 26.0029 33.9628 16.7777 33.9628C7.55248 33.9628 0.0739746 26.4843 0.0739746 17.2591C0.0739746 8.03389 7.55248 0.555389 16.7777 0.555389C26.0029 0.555389 33.4814 8.03389 33.4814 17.2591ZM5.28247 17.2591C5.28247 23.6077 10.429 28.7543 16.7777 28.7543C23.1263 28.7543 28.2729 23.6077 28.2729 17.2591C28.2729 10.9105 23.1263 5.76389 16.7777 5.76389C10.429 5.76389 5.28247 10.9105 5.28247 17.2591Z'
fill='#006666'
/>
</svg>
</LoadingSpinner>
</div>
</LoaderModal>
} }

View File

@ -1,9 +1,11 @@
import { useEffect, useRef, useState } from 'react'
import { I_QueryParams } from 'src/services/type'
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
type Props = { type Props = {
pageNumber: number queryParams: I_QueryParams
pageCount: number setQueryParams: (param: I_QueryParams) => void
setPageNumber: (param: number) => void totalCount: number
} }
interface I_Active { interface I_Active {
@ -12,7 +14,61 @@ interface I_Active {
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
gap: 8px; gap: 50px;
align-items: start;
.page {
display: flex;
gap: 8px;
}
.rowsPerPage {
display: flex;
align-items: center;
gap: 10px;
}
.switcher-container {
position: relative;
}
.switcher {
cursor: pointer;
display: flex;
align-items: center;
}
`
const Menu = styled.div<I_Active>`
flex-direction: column;
border-radius: 12px;
padding: 5px;
${(props) => props.theme.mainShadow}
position: absolute;
z-index: 5;
bottom: -20%;
left: -30%;
transform: translate(-50% -50%);
background: ${(props) => props.theme.bg};
${(props) =>
props.active
? css`
display: flex;
`
: css`
display: none;
`}
p {
padding: 5px 10px;
border-radius: 6px;
transition: 200ms;
&:hover {
background: ${(props) => props.theme.focusColor};
}
}
` `
const Button = styled.div<I_Active>` const Button = styled.div<I_Active>`
@ -28,10 +84,10 @@ const Button = styled.div<I_Active>`
props.active props.active
? css` ? css`
border: 1px solid #919eab; border: 1px solid #919eab;
` `
: css` : css`
cursor: default; cursor: default;
background: #919eab; background: #919eab;
`} `}
&:last-of-type { &:last-of-type {
@ -60,31 +116,202 @@ const Item = styled.div<I_Active>`
` `
export default function Pagination({ export default function Pagination({
pageNumber, queryParams,
pageCount, setQueryParams,
setPageNumber totalCount
}: Props) { }: Props) {
const [menuState, setMenuState] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setMenuState(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
const changeRowsPerPage = (value: number) => {
setQueryParams({ ...queryParams, count: value, page: 1 })
setMenuState(false)
}
return ( return (
<Container> <Container>
<Button active={false}> <div className='page'>
<img src='/src/images/arrow.svg' alt='' /> <Button
</Button> active={queryParams.page !== 1}
<Item active={true}> onClick={() => {
1 queryParams.page !== 1 &&
</Item> setQueryParams({ ...queryParams, page: queryParams.page - 1 })
<Item active={false}> }}
2 >
</Item> <img src='/src/images/arrow.svg' alt='' />
<Item active={false}>...</Item> </Button>
<Item active={false}> {Math.ceil(totalCount / queryParams.count) < 8 ? (
{pageCount - 1} [...Array(Math.ceil(totalCount / queryParams.count)).keys()].map(
</Item> (item) => (
<Item active={false}> <Item
{pageCount} key={item}
</Item> active={item + 1 === queryParams.page}
<Button active={true}> onClick={() =>
<img src='/src/images/arrow.svg' alt='' /> setQueryParams({ ...queryParams, page: item + 1 })
</Button> }
>
{item + 1}
</Item>
)
)
) : (
<>
{queryParams.page < 5 ? (
<>
{[...Array(5).keys()].map((item) => (
<Item
active={item + 1 === queryParams.page}
onClick={() =>
setQueryParams({ ...queryParams, page: item + 1 })
}
>
{item + 1}
</Item>
))}
<Item active={false}>...</Item>
<Item
active={totalCount / queryParams.count === queryParams.page}
onClick={() =>
setQueryParams({
...queryParams,
page: totalCount / queryParams.count
})
}
>
{totalCount / queryParams.count}
</Item>
</>
) : queryParams.page > 4 &&
queryParams.page <= totalCount / queryParams.count - 4 ? (
<>
<Item
active={false}
onClick={() => setQueryParams({ ...queryParams, page: 1 })}
>
1
</Item>
<Item active={false}>...</Item>
<Item
active={false}
onClick={() =>
setQueryParams({
...queryParams,
page: queryParams.page - 1
})
}
>
{queryParams.page - 1}
</Item>
<Item
active={true}
onClick={() =>
setQueryParams({ ...queryParams, page: queryParams.page })
}
>
{queryParams.page}
</Item>
<Item
active={false}
onClick={() =>
setQueryParams({
...queryParams,
page: queryParams.page + 1
})
}
>
{queryParams.page + 1}
</Item>
<Item active={false}>...</Item>
<Item
active={false}
onClick={() =>
setQueryParams({
...queryParams,
page: totalCount / queryParams.count
})
}
>
10
</Item>
</>
) : (
<>
<Item
active={false}
onClick={() =>
setQueryParams({
...queryParams,
page: 1
})
}
>
1
</Item>
<Item active={false}>...</Item>
{[...Array(totalCount / queryParams.count).keys()].map(
(item) => (
<>
{item + 1 > totalCount / queryParams.count - 5 ? (
<Item
active={item + 1 === queryParams.page}
onClick={() =>
setQueryParams({ ...queryParams, page: item + 1 })
}
>
{item + 1}
</Item>
) : (
<></>
)}
</>
)
)}
</>
)}
</>
)}
<Button
active={queryParams.page !== totalCount / queryParams.count}
onClick={() => {
queryParams.page !== totalCount / queryParams.count &&
setQueryParams({ ...queryParams, page: queryParams.page + 1 })
}}
>
<img src='/src/images/arrow.svg' alt='' />
</Button>
</div>
<div className='rowsPerPage'>
<p>Количество на странице:</p>
<div className='switcher-container'>
<div className='switcher' onClick={() => setMenuState(true)}>
<h3>{queryParams.count}</h3>
<img src='/src/images/bottom-arrow.svg' alt='' />
</div>
<Menu active={menuState} ref={menuRef}>
{/* for test */}
<p onClick={() => changeRowsPerPage(1)}>1</p>
<p onClick={() => changeRowsPerPage(5)}>5</p>
<p onClick={() => changeRowsPerPage(10)}>10</p>
<p onClick={() => changeRowsPerPage(15)}>15</p>
<p onClick={() => changeRowsPerPage(20)}>20</p>
</Menu>
</div>
</div>
</Container> </Container>
) )
} }

View File

@ -3,6 +3,8 @@ import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import SideBar from './SideBar' import SideBar from './SideBar'
import Footer from './Footer' import Footer from './Footer'
import axios from 'axios'
import { HOST_NAME } from 'src/constants/host'
type Props = { type Props = {
children: React.ReactNode children: React.ReactNode
@ -18,6 +20,16 @@ export default function Layout({ children }: Props) {
const currentPath = location.pathname const currentPath = location.pathname
const navigate = useNavigate() const navigate = useNavigate()
useEffect(() => {
const testQuery = async () => {
const response = await axios.get(`${HOST_NAME}/packages`)
console.log(response.data);
}
testQuery()
}, [])
// авторизация
// useEffect(() => { // useEffect(() => {
// const hasCookie = document.cookie.includes('_identid') // const hasCookie = document.cookie.includes('_identid')
// if (!hasCookie) { // if (!hasCookie) {
@ -25,6 +37,7 @@ export default function Layout({ children }: Props) {
// return // return
// } // }
// }, []) // }, [])
useEffect(() => { useEffect(() => {
if (currentPath === '/') { if (currentPath === '/') {
navigate('/registrations') navigate('/registrations')

View File

@ -0,0 +1,3 @@
<svg width="25" height="26" viewBox="0 0 25 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4999 6.75V19.25M12.4999 19.25L17.7083 14.0417M12.4999 19.25L7.29159 14.0417" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 274 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.41 9.33997L12 13.92L16.59 9.33997L18 10.75L12 16.75L6 10.75L7.41 9.33997Z" fill="#292929"/>
</svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@ -0,0 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.55556 30.0003C2.03959 30.0003 -0.0354614 32.0515 0.345123 34.5385C1.3056 40.815 4.2417 46.6684 8.7868 51.2135C13.3319 55.7586 19.1852 58.6947 25.4617 59.6552C27.9488 60.0357 30 57.9607 30 55.4447V55.2013C30 52.6854 27.9361 50.696 25.4812 50.1452C21.6935 49.2955 18.1897 47.3871 15.4014 44.5988C12.6131 41.8105 10.7048 38.3068 9.85503 34.5191C9.30429 32.0641 7.31491 30.0003 4.79895 30.0003H4.55556Z" fill="#006666"/>
</svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@ -0,0 +1,3 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M60 30C60 46.5685 46.5685 60 30 60C13.4315 60 0 46.5685 0 30C0 13.4315 13.4315 0 30 0C46.5685 0 60 13.4315 60 30ZM9.35451 30C9.35451 41.4022 18.5978 50.6455 30 50.6455C41.4022 50.6455 50.6455 41.4022 50.6455 30C50.6455 18.5978 41.4022 9.35451 30 9.35451C18.5978 9.35451 9.35451 18.5978 9.35451 30Z" fill="#006666"/>
</svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@ -1,81 +1,65 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton' import MainButton from 'src/components/UI/button/MainButton'
import SearchInput from 'src/components/UI/input/SearchInput' import SearchInput from 'src/components/UI/input/SearchInput'
import Loader from 'src/components/UI/loader/Loader'
import Pagination from 'src/components/UI/table/pagination/Pagination' import Pagination from 'src/components/UI/table/pagination/Pagination'
import RegistrationsService from 'src/services/registrationsServices' import RegistrationsService from 'src/services/registrationsServices'
import styled from 'styled-components' import { I_QueryParams } from 'src/services/type'
import NewRegModal from './blocks/NewRegModal' import NewRegModal from './blocks/NewRegModal'
import RegTable from './blocks/RegTable' import RegTable from './blocks/RegTable'
import {
RegContainer,
RegLoaderContainer,
RegNotification,
RegSelectNotification,
SearchNotification
} from './styles'
import { T_ColumnsState, T_NewRegistration, T_RegistrationItem } from './types' import { T_ColumnsState, T_NewRegistration, T_RegistrationItem } from './types'
type Props = {} type Props = {}
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
align-items: start;
padding: 40px 20px;
.bottom-menu {
display: flex;
align-items: start;
width: 100%;
justify-content: space-between;
}
`
const Notification = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
`
const SelectNotification = styled.div`
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
${(props) => props.theme.mainShadow}
border-radius: 12px;
img {
padding: 5px;
cursor: pointer;
border-radius: 6px;
transition: 300ms;
&:hover {
background: ${(props) => props.theme.focusColor};
transform: scale(1.1);
}
}
`
export default function Registrations({}: Props) { export default function Registrations({}: Props) {
const [pageCount, setPageCount] = useState(10) const [errorState, setErrorState] = useState(false)
const [pageNumber, setPageNumber] = useState(1) const [loadingState, setLoadingState] = useState(true)
const [searchNotification, setSearchNotification] = useState(false)
const [totalCount, setTotalCount] = useState(0)
const [registrations, setRegistrations] = useState<T_RegistrationItem[]>([]) const [registrations, setRegistrations] = useState<T_RegistrationItem[]>([])
const [searchQuery, setSearchQuery] = useState('')
const handleSearchQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleSearchQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value) setQueryParams({ ...queryParams, search: e.target.value })
} }
const [queryParams, setQueryParams] = useState<I_QueryParams>({
page: 1,
count: 10,
search: '',
order: undefined
})
const getReg = async () => {
setSearchNotification(false)
const response = await RegistrationsService.getRegistrations(queryParams)
if (response.status === 200) {
response.data.Data.length === 0
? setSearchNotification(true)
: (setRegistrations(response.data.Data),
setTotalCount(response.data.Total))
}
setLoadingState(false)
}
useEffect(() => {
getReg()
}, [queryParams])
const [activeColumns, setActiveColumns] = useState<T_ColumnsState>({ const [activeColumns, setActiveColumns] = useState<T_ColumnsState>({
RegNum: { name: 'Номер поставки', status: true }, RegNum: { name: 'Номер поставки', status: true },
ContNum: { name: 'Номер контракта', status: true }, ContNum: { name: 'Номер контракта', status: true },
Email: { name: 'Эл. почта', status: false }, Email: { name: 'Эл. почта', status: true },
Count: { name: 'Кол-во активаций', status: false }, Count: { name: 'Кол-во активаций', status: true },
EndDate: { name: 'Дата окончания', status: false }, EndDate: { name: 'Дата окончания', status: true },
CreatedDate: { name: 'Дата создания', status: true }, CreatedDate: { name: 'Дата создания', status: false },
UpdatedDate: { name: 'Дата изменения', status: true }, UpdatedDate: { name: 'Дата изменения', status: true },
Enabled: { name: 'Статус', status: true } Enabled: { name: 'Статус', status: true }
}) })
@ -89,6 +73,33 @@ export default function Registrations({}: Props) {
(setSelectAll(false), setNotChecked([])) (setSelectAll(false), setNotChecked([]))
}, [notChecked]) }, [notChecked])
const groupDelete = async () => {
const deletePromises = checkedColumns.map((item) =>
RegistrationsService.deleteRegistration(item)
)
await Promise.all(deletePromises)
getReg()
}
const groupStart = async () => {
const deletePromises = checkedColumns.map((item) =>
RegistrationsService.onRegistration(item)
)
await Promise.all(deletePromises)
getReg()
}
const groupStop = async () => {
const deletePromises = checkedColumns.map((item) =>
RegistrationsService.offRegistration(item)
)
await Promise.all(deletePromises)
getReg()
}
const [newReg, setNewReg] = useState<T_NewRegistration>({ const [newReg, setNewReg] = useState<T_NewRegistration>({
RegNum: '', RegNum: '',
ContNum: '', ContNum: '',
@ -113,74 +124,105 @@ export default function Registrations({}: Props) {
}) })
}, [modal]) }, [modal])
const getReg = async () => {
const response = await RegistrationsService.getRegistration()
if (response.status === 200) {
setRegistrations(response.data)
}
}
useEffect(() => { useEffect(() => {
getReg() getReg()
}, []) }, [])
return ( return (
<Container> <>
{registrations.length === 0 ? ( {loadingState ? (
<Notification> <RegLoaderContainer>
<h2>Активных лицензий нет</h2> <Loader />
<MainButton onClick={() => setModal(true)}> </RegLoaderContainer>
Добавить лицензию
</MainButton>
</Notification>
) : ( ) : (
<> <RegContainer>
<h2>Система управления и контроля ОСГОС</h2> {registrations.length === 0 && queryParams.search === '' ? (
<SearchInput <RegNotification>
value={searchQuery} <h2>Активных лицензий нет</h2>
onChange={handleSearchQueryChange} <MainButton onClick={() => setModal(true)}>
placeholder='Введите номер контракта, поставки или email' Добавить лицензию
/> </MainButton>
{(selectAll || checkedColumns.length !== 0) && ( </RegNotification>
<SelectNotification> ) : (
<h3> <>
{selectAll <h2>Система управления и контроля ОСГОС</h2>
? 'Количесво не определено' <SearchInput
: checkedColumns.length !== 0 && value={queryParams.search}
`Выбранно: ${checkedColumns.length}`} onChange={handleSearchQueryChange}
</h3> placeholder='Введите номер контракта, поставки или email'
<img src='/src/images/trash.svg' alt='' /> />
</SelectNotification> <>
{searchNotification ? (
<SearchNotification>
<h3>Ничего не найдено</h3>
</SearchNotification>
) : (
<>
{(selectAll || checkedColumns.length !== 0) && (
<RegSelectNotification>
<h3>
{selectAll
? `Выбранно: ${totalCount - notChecked.length}`
: checkedColumns.length !== 0 &&
`Выбранно: ${checkedColumns.length}`}
</h3>
<div className='menu'>
<img
src='/src/images/play.svg'
alt=''
onClick={groupStart}
/>
<img
src='/src/images/stop.svg'
alt=''
onClick={groupStop}
/>
<img
src='/src/images/trash.svg'
alt=''
onClick={groupDelete}
/>
</div>
</RegSelectNotification>
)}
<RegTable
selectAll={selectAll}
notChecked={notChecked}
setNotChecked={setNotChecked}
setSelectAll={setSelectAll}
checkedColumns={checkedColumns}
setCheckedColumns={setCheckedColumns}
activeColumns={activeColumns}
setActiveColumns={setActiveColumns}
registrations={registrations}
getReg={getReg}
queryParams={queryParams}
setQueryParams={setQueryParams}
/>
<div className='bottom-menu'>
<Pagination
queryParams={queryParams}
setQueryParams={setQueryParams}
totalCount={totalCount}
/>
<MainButton onClick={() => setModal(true)}>
Добавить
</MainButton>
</div>
</>
)}
</>
</>
)} )}
<RegTable <NewRegModal
selectAll={selectAll} modal={modal}
notChecked={notChecked} setModal={setModal}
setNotChecked={setNotChecked} newReg={newReg}
setSelectAll={setSelectAll} setNewReg={setNewReg}
checkedColumns={checkedColumns}
setCheckedColumns={setCheckedColumns}
activeColumns={activeColumns}
setActiveColumns={setActiveColumns}
registrations={registrations}
getReg={getReg} getReg={getReg}
/> />
<div className='bottom-menu'> </RegContainer>
<Pagination
setPageNumber={setPageNumber}
pageNumber={pageNumber}
pageCount={pageCount}
/>
<MainButton onClick={() => setModal(true)}>Добавить</MainButton>
</div>
</>
)} )}
<NewRegModal </>
modal={modal}
setModal={setModal}
newReg={newReg}
setNewReg={setNewReg}
getReg={getReg}
/>
</Container>
) )
} }

View File

@ -32,24 +32,34 @@ export default function EditRegModal({
}: Props) { }: Props) {
const [editRegData, setEditRegData] = useState(registration) const [editRegData, setEditRegData] = useState(registration)
const [errorMessage, setErrorMessage] = useState(false)
const handleContNumChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleContNumChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRegData({ ...registration, ContNum: e.target.value }) setEditRegData({ ...editRegData, ContNum: e.target.value })
} }
const handleRegNumChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleRegNumChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRegData({ ...registration, RegNum: e.target.value }) setEditRegData({ ...editRegData, RegNum: e.target.value })
} }
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRegData({ ...registration, Email: e.target.value }) setEditRegData({ ...editRegData, Email: e.target.value })
} }
const handleCountChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleCountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRegData({ ...registration, Count: e.target.value }) setEditRegData({ ...editRegData, Count: e.target.value })
} }
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditRegData({ ...registration, EndDate: e.target.value }) setEditRegData({ ...editRegData, EndDate: e.target.value })
} }
useEffect(() => { useEffect(() => {
console.log(editRegData) function formatDate(isoDateString: string): string {
const date = new Date(isoDateString)
const year = date.getFullYear()
const month = ('0' + (date.getMonth() + 1)).slice(-2)
const day = ('0' + date.getDate()).slice(-2)
return `${year}-${month}-${day}`
}
const formattedDate = formatDate(editRegData.EndDate)
setEditRegData({ ...editRegData, EndDate: formattedDate })
}, []) }, [])
return ( return (

View File

@ -1,3 +1,4 @@
import { useEffect, useState } from 'react'
import MainButton from 'src/components/UI/button/MainButton' import MainButton from 'src/components/UI/button/MainButton'
import MainInput from 'src/components/UI/input/MainInput' import MainInput from 'src/components/UI/input/MainInput'
import Modal from 'src/components/UI/modal/Modal' import Modal from 'src/components/UI/modal/Modal'
@ -17,6 +18,17 @@ const ModalContainer = styled.div`
} }
` `
const ErrorMessageContainer = styled.div`
display: flex;
flex-direction: column;
align-items: end;
gap: 20px;
`
const Test = styled.div`
z-index: 3;
`
type Props = { type Props = {
modal: boolean modal: boolean
setModal: (param: boolean) => void setModal: (param: boolean) => void
@ -32,12 +44,28 @@ export default function NewRegModal({
setNewReg, setNewReg,
getReg getReg
}: Props) { }: Props) {
const [errorMessage, setErrorMessage] = useState(false)
const CreateReg = async () => { const CreateReg = async () => {
await RegistrationsService.createRegistration(newReg) const response = await RegistrationsService.createRegistration(newReg)
getReg() if (response.status === 406) {
setModal(false) function formatDate(isoDateString: string): string {
const date = new Date(isoDateString)
const year = date.getFullYear()
const month = ('0' + (date.getMonth() + 1)).slice(-2)
const day = ('0' + date.getDate()).slice(-2)
return `${year}-${month}-${day}`
}
const formattedDate = formatDate(newReg.EndDate)
setNewReg({ ...newReg, EndDate: formattedDate, Count: `${newReg.Count}` })
setErrorMessage(true)
} else {
setModal(false)
getReg()
}
} }
const handleContNumChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleContNumChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewReg({ ...newReg, ContNum: e.target.value }) setNewReg({ ...newReg, ContNum: e.target.value })
} }
@ -55,47 +83,57 @@ export default function NewRegModal({
} }
return ( return (
<Modal modal={modal} setModal={setModal}> <>
<ModalContainer> <Modal modal={modal} setModal={setModal}>
<MainInput <ModalContainer>
placeholder='Номер контракта' <MainInput
value={newReg.ContNum} placeholder='Номер контракта'
onChange={handleContNumChange} value={newReg.ContNum}
/> onChange={handleContNumChange}
<MainInput />
placeholder='Номер поставки' <MainInput
value={newReg.RegNum} placeholder='Номер поставки'
onChange={handleRegNumChange} value={newReg.RegNum}
/> onChange={handleRegNumChange}
<MainInput />
placeholder='Email' <MainInput
value={newReg.Email} placeholder='Email'
onChange={handleEmailChange} value={newReg.Email}
/> onChange={handleEmailChange}
<MainInput />
placeholder='Кол-во активаций' <MainInput
value={typeof newReg.Count === 'string' ? newReg.Count : ''} placeholder='Кол-во активаций'
onChange={handleCountChange} value={typeof newReg.Count === 'string' ? newReg.Count : ''}
/> onChange={handleCountChange}
<MainInput />
placeholder='Дата окончания' <MainInput
value={newReg.EndDate} placeholder='Дата окончания'
onChange={handleEndDateChange} value={newReg.EndDate}
type='date' onChange={handleEndDateChange}
/> type='date'
<div className='buttonBlock'> />
<MainButton <div className='buttonBlock'>
color='secondary' <MainButton
fullWidth={true} color='secondary'
onClick={() => setModal(false)} fullWidth={true}
> onClick={() => setModal(false)}
Отмена >
</MainButton> Отмена
<MainButton fullWidth={true} onClick={CreateReg}> </MainButton>
Создать <MainButton fullWidth={true} onClick={CreateReg}>
</MainButton> Создать
</div> </MainButton>
</ModalContainer> </div>
</Modal> </ModalContainer>
</Modal>
<Test>
<Modal modal={errorMessage} setModal={setErrorMessage}>
<ErrorMessageContainer>
<h3>Номер контракта или поставки уже существует</h3>
<MainButton onClick={() => setErrorMessage(false)}>Ok</MainButton>
</ErrorMessageContainer>
</Modal>
</Test>
</>
) )
} }

View File

@ -1,10 +1,16 @@
import { useEffect, useRef, useState } from 'react' import { useState } from 'react'
import ContextMenu from 'src/components/UI/contextMenu/ContextMenu'
import Checkbox from 'src/components/UI/input/Checkbox' import Checkbox from 'src/components/UI/input/Checkbox'
import Table from 'src/components/UI/table/Table' import Table from 'src/components/UI/table/Table'
import { I_QueryParams } from 'src/services/type'
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import { T_ColumnsState, T_RegistrationItem } from '../types' import { T_ColumnsState, T_RegistrationItem } from '../types'
import RegTableItem from './RegTableItem' import RegTableItem from './RegTableItem'
import ContextMenu from 'src/components/UI/contextMenu/ContextMenu'
interface I_TableHeaderItem {
active: boolean
state: boolean
}
const Item = styled.div` const Item = styled.div`
display: flex; display: flex;
@ -12,6 +18,28 @@ const Item = styled.div`
cursor: pointer; cursor: pointer;
` `
const TableHeaderItem = styled.div<I_TableHeaderItem>`
display: flex;
align-items: center;
cursor: pointer;
img {
${(props) =>
props.state &&
css`
transform: rotate(180deg);
`}
${(props) =>
props.active
? css`
display: block;
`
: css`
display: none;
`}
}
`
type Props = { type Props = {
selectAll: boolean selectAll: boolean
notChecked: number[] notChecked: number[]
@ -22,7 +50,9 @@ type Props = {
activeColumns: T_ColumnsState activeColumns: T_ColumnsState
setActiveColumns: (param: any) => void setActiveColumns: (param: any) => void
registrations: T_RegistrationItem[] registrations: T_RegistrationItem[]
getReg: () => void getReg: () => void
queryParams: I_QueryParams
setQueryParams: (param: I_QueryParams) => void
} }
export default function RegTable({ export default function RegTable({
@ -35,11 +65,34 @@ export default function RegTable({
activeColumns, activeColumns,
setActiveColumns, setActiveColumns,
registrations, registrations,
getReg getReg,
queryParams,
setQueryParams
}: Props) { }: Props) {
const [contextMenuState, setContextMenuState] = useState(false) const [contextMenuState, setContextMenuState] = useState(false)
const [activeSort, setActiveSort] = useState('')
const [sortState, setSortState] = useState(false)
const sort = (value: string) => {
const sortOptions: { [key: string]: string } = {
'Номер поставки': 'reg',
'Номер контракта': 'cont',
'Эл. почта': 'email',
'Кол-во активаций': 'count',
'Дата окончания': 'end',
'Дата изменения': 'updated',
'Статус': 'enabled'
};
const sortParam = sortOptions[value];
if (!sortParam) return;
setActiveSort(value);
const newOrder = queryParams.order === sortParam ? `!${sortParam}` : sortParam;
setQueryParams({ ...queryParams, order: newOrder });
setSortState(queryParams.order === sortParam);
};
return ( return (
<Table> <Table>
@ -69,10 +122,25 @@ export default function RegTable({
</th> </th>
{Object.entries(activeColumns).map( {Object.entries(activeColumns).map(
([index, item]) => ([index, item]) =>
item.status === true && <th key={index}>{item.name}</th> item.status === true && (
<th>
<TableHeaderItem
key={index}
active={activeSort === item.name}
state={sortState}
onClick={() => sort(item.name)}
>
<img src='/src/images/arrow-for-table.svg' alt='' />
{item.name}
</TableHeaderItem>
</th>
)
)} )}
<th> <th>
<ContextMenu active={contextMenuState} setActive={setContextMenuState}> <ContextMenu
active={contextMenuState}
setActive={setContextMenuState}
>
<div className='target'> <div className='target'>
<img <img
src='/src/images/params.svg' src='/src/images/params.svg'
@ -113,7 +181,7 @@ export default function RegTable({
setNotChecked={setNotChecked} setNotChecked={setNotChecked}
setCheckedColumns={setCheckedColumns} setCheckedColumns={setCheckedColumns}
activeColumns={activeColumns} activeColumns={activeColumns}
getReg={getReg} getReg={getReg}
/> />
))} ))}
</tbody> </tbody>

View File

@ -0,0 +1,68 @@
import styled from 'styled-components'
export const RegLoaderContainer = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
export const RegContainer = styled.div`
display: flex;
flex-direction: column;
gap: 20px;
align-items: start;
padding: 40px 20px;
.bottom-menu {
display: flex;
align-items: start;
width: 100%;
justify-content: space-between;
}
`
export const RegNotification = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
`
export const SearchNotification = styled.div`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
export const RegSelectNotification = styled.div`
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
${(props) => props.theme.mainShadow}
border-radius: 12px;
.menu {
display: flex;
gap: 10px;
}
img {
padding: 5px;
cursor: pointer;
border-radius: 6px;
transition: 300ms;
&:hover {
background: ${(props) => props.theme.focusColor};
transform: scale(1.1);
}
}
`

View File

@ -1,11 +1,24 @@
import axios, { AxiosResponse } from 'axios' import axios, { AxiosResponse } from 'axios'
import { HOST_NAME } from 'src/constants/host' import { HOST_NAME } from 'src/constants/host'
import { T_NewRegistration } from 'src/pages/Registrations/types' import { T_NewRegistration } from 'src/pages/Registrations/types'
import { I_QueryParams } from './type'
export default class RegistrationsService { export default class RegistrationsService {
static async getRegistration(): Promise<AxiosResponse> { static async getRegistrations(
queryParams: I_QueryParams
): Promise<AxiosResponse> {
const { page, count, search, order } = queryParams
const url = `${HOST_NAME}/regs`
const params = new URLSearchParams()
if (page !== undefined) params.append('page', String(page))
if (count !== undefined) params.append('count', String(count))
if (search !== undefined && search !== '') params.append('search', search)
if (order !== undefined) params.append('order', order)
try { try {
const response = await axios.get(`${HOST_NAME}/regs`) const response = await axios.get(url, { params })
console.log(response.data)
return response return response
} catch (error: any) { } catch (error: any) {
return error.response return error.response
@ -56,7 +69,16 @@ export default class RegistrationsService {
} }
} }
static async editRegistration(id: number, reg: T_NewRegistration): Promise<AxiosResponse> { static async editRegistration(
id: number,
reg: T_NewRegistration
): Promise<AxiosResponse> {
const date = new Date(reg.EndDate)
const isoDateStr = date.toISOString()
reg.EndDate = isoDateStr
if (typeof reg.Count === 'string') {
reg.Count = parseInt(reg.Count, 10)
}
try { try {
const response = await axios.post(`${HOST_NAME}/regs/${id}`, reg) const response = await axios.post(`${HOST_NAME}/regs/${id}`, reg)
return response return response

6
src/services/type.ts Normal file
View File

@ -0,0 +1,6 @@
export interface I_QueryParams {
page: number
count: number
search: string
order?: string | undefined
}

View File

@ -1,6 +1,8 @@
import axios, { AxiosResponse } from 'axios' import axios, { AxiosResponse } from 'axios'
import { HOST_NAME } from 'src/constants/host' import { HOST_NAME } from 'src/constants/host'
//
export default class UserService { export default class UserService {
static async login(name: string, password: string): Promise<AxiosResponse> { static async login(name: string, password: string): Promise<AxiosResponse> {
try { try {

View File

@ -1,16 +1,16 @@
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import dotenv from 'dotenv'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import dotenv from 'dotenv'; import { VitePWA } from 'vite-plugin-pwa'
// https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react(), VitePWA()],
resolve: { resolve: {
alias: { alias: {
src: '/src' src: '/src'
} }
}, },
define: { define: {
'process.env': dotenv.config().parsed 'process.env': dotenv.config().parsed
}, }
}) })