Skip to content

Commit

Permalink
Merge pull request #10 from BCSDLab/feature/#2
Browse files Browse the repository at this point in the history
[회비 납부] 회비 납부 페이지 구현
  • Loading branch information
dooohun authored Feb 13, 2024
2 parents 3d105de + f17d4a2 commit b77c5d8
Show file tree
Hide file tree
Showing 15 changed files with 539 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ module.exports = {
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/quotes": ["error", "single"],
"react/jsx-props-no-spreading": "off",
"react/react-in-jsx-scope": "off",
"react/no-unknown-property": ["error", { ignore: ["css"] }],
"import/prefer-default-export": "off",
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ node_modules
.env.production.local
.history
/packages/*/bundles
/packages/*/lib/*
/packages/*/lib/*

# yarn berry
.yarn/install-state.gz
Binary file modified .yarn/install-state.gz
Binary file not shown.
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { Route, Routes } from 'react-router-dom';
import DefaultLayout from 'layout/DefaultLayout';
import MemberInfo from 'page/MemberInfo';
import SignUp from 'page/SignUp';
import DuesManagement from 'page/DuesManagement';

function App() {
return (
<Routes>
<Route path="/" element={<DefaultLayout />} />
<Route path="/" element={<MemberInfo />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="/member-info" element={<DefaultLayout />} />
<Route path="/dues" element={<DuesManagement />} />
</Routes>
);
}
Expand Down
12 changes: 12 additions & 0 deletions src/api/Dues/index.ts
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);
};
10 changes: 10 additions & 0 deletions src/layout/LoadingSpinner/index.tsx
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>
);
}
7 changes: 7 additions & 0 deletions src/layout/LoadingSpinner/style.ts
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%;
`;
23 changes: 23 additions & 0 deletions src/model/dues/allDues.ts
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;
}
261 changes: 261 additions & 0 deletions src/page/DuesManagement/index.tsx
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>
);
}
Loading

0 comments on commit b77c5d8

Please sign in to comment.