-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
[회비 납부] 회비 납부 페이지 구현
- Loading branch information
Showing
15 changed files
with
539 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { accessClient } from 'api'; | ||
import { DuesInfo } from 'model/dues/allDues'; | ||
|
||
export interface DuesOptions { | ||
year: number; | ||
track?: string; | ||
} | ||
|
||
export const getAllDues = ({ year, track }: DuesOptions) => { | ||
const query = track ? `/dues?year=${year}&track=${track}` : `/dues?year=${year}`; | ||
return accessClient.get<DuesInfo>(query); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { CircularProgress } from '@mui/material'; | ||
import * as S from './style'; | ||
|
||
export default function LoadingSpinner() { | ||
return ( | ||
<div css={S.loading}> | ||
<CircularProgress /> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { css } from '@emotion/react'; | ||
|
||
export const loading = css` | ||
position: absolute; | ||
top: 50%; | ||
left: 50%; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
export interface DuesInfo { | ||
year: number; | ||
dues: Dues[]; | ||
} | ||
|
||
interface Dues { | ||
memberId: number; | ||
name: string; | ||
track: TrackInfo; | ||
unpaidCount: number; | ||
detail: DuesDetail[]; | ||
} | ||
|
||
export interface DuesDetail { | ||
month: number; | ||
status: '납부' | '면제' | '미납' | null; | ||
memo?: string; | ||
} | ||
|
||
interface TrackInfo { | ||
id: number; | ||
name: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
import { useNavigate } from 'react-router-dom'; | ||
import Table from '@mui/material/Table'; | ||
import TableBody from '@mui/material/TableBody'; | ||
import TableCell from '@mui/material/TableCell'; | ||
import TableContainer from '@mui/material/TableContainer'; | ||
import TableHead from '@mui/material/TableHead'; | ||
import TableRow from '@mui/material/TableRow'; | ||
import { | ||
Button, Checkbox, FormControl, FormControlLabel, FormGroup, FormLabel, Input, Popover, | ||
} from '@mui/material'; | ||
import Modal from '@mui/material/Modal'; | ||
import { | ||
ChangeEvent, Suspense, useEffect, useState, | ||
} from 'react'; | ||
import { useGetAllDues } from 'query/dues'; | ||
import useBooleanState from 'util/hooks/useBooleanState.ts'; | ||
import { DuesDetail } from 'model/dues/allDues'; | ||
import { STATUS_MAPPING } from 'util/constants/status'; | ||
import { useGetTracks } from 'query/tracks'; | ||
import LoadingSpinner from 'layout/LoadingSpinner'; | ||
import { ArrowBackIosNewOutlined, ArrowForwardIosOutlined } from '@mui/icons-material'; | ||
import useQueryParam from 'util/hooks/useQueryParam'; | ||
import * as S from './style'; | ||
|
||
function DefaultTable() { | ||
const navigate = useNavigate(); | ||
const page = useQueryParam('page', 'number') as number; | ||
const currentYear = new Date().getFullYear(); | ||
const [duesYear, setDuesYear] = useState(currentYear - page + 1); | ||
const [trackFilter, setTrackFilter] = useState([true, true, true, true, true, true]); | ||
const [name, setName] = useState(''); | ||
const [detail, setDetail] = useState<DuesDetail>({ month: 0, status: null }); | ||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); | ||
const memoPopOverOpen = Boolean(anchorEl); | ||
const { | ||
value: isFilterModalOpen, | ||
setTrue: openFilterModal, | ||
setFalse: closeFilterModal, | ||
} = useBooleanState(false); | ||
|
||
const { data: allDues } = useGetAllDues({ year: duesYear }); | ||
const [filteredValue, setFilteredValue] = useState(allDues.dues); | ||
|
||
const { data: tracks } = useGetTracks(); | ||
|
||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
const searchName = e.target.value; | ||
if (searchName === '') { | ||
if (trackFilter.every((value) => value)) { | ||
setFilteredValue(allDues.dues); | ||
} else { | ||
setFilteredValue(allDues.dues.filter((row) => trackFilter[tracks.map((track) => track.name).indexOf(row.track.name)])); | ||
} | ||
} | ||
setName(searchName); | ||
}; | ||
|
||
const handleNameSearchClick = () => { | ||
if (filteredValue.some((row) => row.name.includes(name))) { | ||
setFilteredValue(allDues.dues.filter((row) => row.name.includes(name))); | ||
} | ||
}; | ||
|
||
const handleNameSearchKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||
if (e.key === 'Enter') { | ||
handleNameSearchClick(); | ||
} | ||
}; | ||
|
||
const handleTrackFilterChange = (e: ChangeEvent<HTMLInputElement>) => { | ||
const selectedTrack = e.target.name; | ||
const trackIndex = tracks.filter((track) => track.name === selectedTrack)[0].id - 1; | ||
setTrackFilter((prevTrack) => { | ||
const updatedTrack = [...prevTrack]; | ||
updatedTrack[trackIndex] = !updatedTrack[trackIndex]; | ||
setFilteredValue(allDues.dues.filter( | ||
(row) => updatedTrack[tracks.map((track) => track.name).indexOf(row.track.name)], | ||
)); | ||
return updatedTrack; | ||
}); | ||
}; | ||
|
||
const handleMemoClick = (e: React.MouseEvent<HTMLTableCellElement>, dueDetail: DuesDetail) => { | ||
if (dueDetail.status === '미납' || dueDetail.status === '면제') { | ||
setAnchorEl(e.currentTarget); | ||
if (dueDetail.memo) { | ||
setDetail(dueDetail); | ||
} | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
setDuesYear(currentYear - page + 1); | ||
}, [currentYear, page]); | ||
|
||
const goToPrevYear = () => { | ||
// 재학생 회비 내역이 2021년부터 시작하므로 2021년 이전으로 이동할 수 없음 | ||
if (page < 4) { | ||
const prevYear = page + 1; | ||
navigate(`/dues?page=${prevYear}`); | ||
} | ||
}; | ||
|
||
const goToNextYear = () => { | ||
if (page > 1) { | ||
navigate(`/dues?page=${page - 1}`); | ||
} | ||
}; | ||
return ( | ||
<> | ||
<div css={S.searchAndPagination}> | ||
<div css={S.pagination}> | ||
<Button onClick={goToPrevYear}> | ||
<ArrowBackIosNewOutlined /> | ||
</Button> | ||
<span css={S.paginationTitle}>{duesYear}</span> | ||
<Button onClick={goToNextYear}> | ||
<ArrowForwardIosOutlined /> | ||
</Button> | ||
</div> | ||
<div> | ||
<Input | ||
value={name} | ||
id="memberName" | ||
onKeyDown={handleNameSearchKeyDown} | ||
onChange={handleNameChange} | ||
placeholder="이름을 입력하세요" | ||
/> | ||
<Button onClick={handleNameSearchClick}>검색</Button> | ||
</div> | ||
</div> | ||
<div css={S.dues}> | ||
<TableContainer css={S.tableContainer}> | ||
<Table stickyHeader> | ||
<TableHead> | ||
<TableRow> | ||
<TableCell css={S.tableHeader}> | ||
<div css={S.trackTableCell}> | ||
<span>트랙</span> | ||
<button | ||
type="button" | ||
onClick={openFilterModal} | ||
css={S.filterModalButton} | ||
> | ||
필터 | ||
</button> | ||
<Modal | ||
open={isFilterModalOpen} | ||
onClose={closeFilterModal} | ||
> | ||
<div css={S.filterModalContainer}> | ||
<h2> | ||
트랙 선택 | ||
</h2> | ||
<div css={S.filterModalContent}> | ||
<FormControl css={S.checkboxFieldset} component="fieldset" variant="standard"> | ||
<FormLabel component="legend">원하는 트랙을 선택하세요.</FormLabel> | ||
<FormGroup> | ||
{tracks.map((track, index) => { | ||
return ( | ||
<FormControlLabel | ||
key={track.id} | ||
control={ | ||
<Checkbox checked={trackFilter[index]} onChange={handleTrackFilterChange} name={track.name} /> | ||
} | ||
label={track.name} | ||
/> | ||
); | ||
})} | ||
</FormGroup> | ||
</FormControl> | ||
</div> | ||
</div> | ||
</Modal> | ||
</div> | ||
</TableCell> | ||
<TableCell css={S.tableHeader}>미납 횟수</TableCell> | ||
<TableCell css={S.tableHeader}>이름</TableCell> | ||
{Array.from({ length: 12 }, (_, i) => i + 1).map((month) => ( | ||
<TableCell key={month} css={S.tableHeader}> | ||
{month} | ||
월 | ||
</TableCell> | ||
))} | ||
</TableRow> | ||
</TableHead> | ||
<TableBody> | ||
{filteredValue.map((row) => ( | ||
<TableRow key={row.memberId}> | ||
<TableCell component="th" scope="row"> | ||
{row.track.name} | ||
</TableCell> | ||
<TableCell>{row.unpaidCount}</TableCell> | ||
<TableCell>{row.name}</TableCell> | ||
{row.detail.map((dueDetail) => ( | ||
<TableCell | ||
css={S.memoTableCell(dueDetail)} | ||
onClick={(e) => handleMemoClick(e, dueDetail)} | ||
key={dueDetail.month} | ||
> | ||
{/* TODO: detail.status에 따른 UI */} | ||
{/* 미납 X(빨강), 면제 -(초록), 납부 O(초록), null -(default) */} | ||
{dueDetail.status !== null ? STATUS_MAPPING[dueDetail.status] : '-'} | ||
</TableCell> | ||
))} | ||
</TableRow> | ||
))} | ||
<Popover | ||
id="simple-popover" | ||
open={memoPopOverOpen} | ||
anchorEl={anchorEl} | ||
onClose={() => setAnchorEl(null)} | ||
anchorOrigin={{ | ||
vertical: 'bottom', | ||
horizontal: 'left', | ||
}} | ||
> | ||
<div css={S.memoPopover}> | ||
{/* TODO: 면제 혹은 미납의 구체적인 사유 */} | ||
<h3> | ||
{detail.status} | ||
{' '} | ||
사유 | ||
</h3> | ||
{detail.memo} | ||
</div> | ||
</Popover> | ||
</TableBody> | ||
</Table> | ||
</TableContainer> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
export default function DuesManagement() { | ||
const page = useQueryParam('page', 'number') as number; | ||
const currentYear = new Date().getFullYear(); | ||
const duesYear = currentYear - page + 1; | ||
return ( | ||
<div css={S.container}> | ||
<div css={S.sidebar}> | ||
<img src="https://image.bcsdlab.com/banner.png" alt="logo" css={S.logo} /> | ||
<Button variant="outlined" color="secondary" sx={{ marginTop: '20px' }}>회원정보</Button> | ||
</div> | ||
<div css={S.content}> | ||
<div css={S.topBar}> | ||
<h1 css={S.topBarTitle}> | ||
{duesYear} | ||
년 회비 내역 | ||
</h1> | ||
</div> | ||
<div css={S.mainContent}> | ||
<Suspense fallback={<LoadingSpinner />}> | ||
<DefaultTable /> | ||
</Suspense> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.