diff --git a/src/App.jsx b/src/App.jsx index ca25782..a9b26f3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -20,6 +20,7 @@ import TheraExerciseAddPage from "./pages/Therapist/TheraExerciseAddPage.jsx"; import TheraMakeAssignPage from "./pages/Therapist/TheraMakeAssignPage.jsx"; import styled from "styled-components"; import "./App.scss"; +import { ReducerContext } from "./reducer/context.js"; const Container = styled.div` margin-top: 60px; @@ -29,46 +30,54 @@ const Container = styled.div` function App() { return ( - -
- - - } /> - } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - } - /> - } - /> - } /> - } - /> - } /> - } /> - } - /> - } /> - } /> - - - + + +
+ + + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } + /> + } + /> + } /> + } + /> + } + /> + } /> + } + /> + } + /> + } /> + + + + ); } diff --git a/src/assets/images/role/role_doctor.png b/src/assets/images/role/role_doctor.png new file mode 100644 index 0000000..600c03e Binary files /dev/null and b/src/assets/images/role/role_doctor.png differ diff --git a/src/assets/images/role/role_patient.png b/src/assets/images/role/role_patient.png new file mode 100644 index 0000000..3e77933 Binary files /dev/null and b/src/assets/images/role/role_patient.png differ diff --git a/src/assets/images/role/role_therapist.png b/src/assets/images/role/role_therapist.png new file mode 100644 index 0000000..d6c0322 Binary files /dev/null and b/src/assets/images/role/role_therapist.png differ diff --git a/src/components/Chart/ChartSummary.jsx b/src/components/Chart/ChartSummary.jsx new file mode 100644 index 0000000..8caa0c6 --- /dev/null +++ b/src/components/Chart/ChartSummary.jsx @@ -0,0 +1,92 @@ +import styled from "styled-components"; +import { useState, useEffect } from "react"; +import { userLogin } from "../../librarys/dummy-api"; + +const Container = styled.div` + width: 380px; + height: 200px; + margin: 0 auto; + padding: 20px; + border: 1px solid #0064ff; + border-radius: 10px; + background-color: #ffffff; + font-family: "Spoqa Han Sans Neo", "sans-serif"; + position: relative; + display: flex; + flex-wrap: wrap; + flex-direction: column; +`; + +const Title = styled.h1` + font-size: 28px; + font-weight: bold; + color: #333; + display: inline-block; +`; + +const Divider = styled.hr` + width: 100%; + height: 1px; + background-color: #d9d9d9; + border: none; + margin-top: 0px; +`; + +const Row = styled.div` + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 5px; +`; + +const Label = styled.span` + color: #000000; + font-size: 16px; +`; + +const Value = styled.span` + color: #908b8b; +`; + +const ChartSummary = ({ ...props }) => { + const [patientInfo, setPatientInfo] = useState({}); + + useEffect(() => { + async function fetchPatientInfo() { + const doctorInfo = await userLogin("doctor", "123456"); + const patient = doctorInfo?.patient; + + if (patient) { + setPatientInfo(patient); + } + } + + fetchPatientInfo(); + }, []); + + return ( + + 차트 정보 + + + + {patientInfo.diseaseCode} + + + + {patientInfo.recentVisitDate} + + + + {patientInfo.nextReservationDate} + + + + {patientInfo.assignedTherapist} + + + ); +}; + +export default ChartSummary; diff --git a/src/components/Input/InputArea.jsx b/src/components/Input/InputArea.jsx index 3a0d4dc..292ccd7 100644 --- a/src/components/Input/InputArea.jsx +++ b/src/components/Input/InputArea.jsx @@ -11,15 +11,23 @@ const Item = styled.textarea` border: 1px solid #bbbbbb; padding-left: 12px; resize: none; + overflow: hidden; &:focus { outline: none; } `; -function InputArea({ value, onInput, className }) { +function InputArea({ value, onInput, disabled, className }) { return ( - + ); } @@ -27,6 +35,11 @@ InputArea.propTypes = { value: PropTypes.string, onInput: PropTypes.func, className: PropTypes.string, + disabled: PropTypes.bool, +}; + +InputArea.defaultProps = { + disabled: false, }; export default InputArea; diff --git a/src/components/Input/InputAreaContainer.jsx b/src/components/Input/InputAreaContainer.jsx index b1fce12..e1c0f07 100644 --- a/src/components/Input/InputAreaContainer.jsx +++ b/src/components/Input/InputAreaContainer.jsx @@ -1,6 +1,7 @@ import styled from "styled-components"; import PropTypes from "prop-types"; import InputArea from "./InputArea"; +import classNames from "classnames"; const Input = styled(InputArea)` width: 100% !important; @@ -17,11 +18,11 @@ const Label = styled.p` margin-bottom: 6px; `; -function InputAreaContainer({ label, value, onInput, ...props }) { +function InputAreaContainer({ label, value, onInput, disabled, ...props }) { return ( - + ); } @@ -30,6 +31,7 @@ InputAreaContainer.propTypes = { label: PropTypes.string, value: PropTypes.string, onInput: PropTypes.func, + disabled: PropTypes.bool, }; export default InputAreaContainer; diff --git a/src/components/Pagination/Pagination.jsx b/src/components/Pagination/Pagination.jsx index 4e815e1..0fa2599 100644 --- a/src/components/Pagination/Pagination.jsx +++ b/src/components/Pagination/Pagination.jsx @@ -66,7 +66,7 @@ const StyledReactPaginate = styled(ReactPaginate)` function Pagination({}) { const [state, dispatch] = useContext(ReducerContext); - const { totalPage } = state; + const { totalPage } = state || {}; const handlePageClick = (data) => { console.log(data); diff --git a/src/components/Reservation/ReservationItem.jsx b/src/components/Reservation/ReservationItem.jsx new file mode 100644 index 0000000..1ee6cd6 --- /dev/null +++ b/src/components/Reservation/ReservationItem.jsx @@ -0,0 +1,200 @@ +import { useMemo } from "react"; +import styled from "styled-components"; +import PropTypes from "prop-types"; +import Button from "../Button/Button.jsx"; + +import PatientIcon from "../../assets/images/role/role_patient.png"; +import DoctorIcon from "../../assets/images/role/role_doctor.png"; +import TherapistIcon from "../../assets/images/role/role_therapist.png"; + +import { MdLocalHospital, MdCalendarMonth, MdPerson } from "react-icons/md"; +import { DAYJS_FORMAT, ROLE_TYPE } from "../../librarys/type.js"; +import dayjs from "dayjs"; +import classNames from "classnames"; +import { useDispatch } from "react-redux"; +import { show } from "../../redux/modalSlice.js"; + +const Container = styled.div` + height: 110px; + padding: 0 24px; + border: 1px solid #bbbbbb; + border-radius: 10px; + display: flex; + align-items: center; + gap: 18px; +`; + +const Image = styled.img` + width: 72px; + height: 72px; + background-color: #d9d9d9; + border-radius: 36px; + object-fit: contain; + overflow: hidden; +`; + +const Info = styled.div` + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 6px; + + font-size: 16px; + font-weight: 400; +`; + +const Big = styled.span` + margin-right: 8px; + font-size: 22px; + font-weight: 800; +`; + +const Line = styled.div` + display: flex; + align-items: center; + gap: 24px; +`; + +const Item = styled.div` + width: ${(props) => props.width || "auto"}; + vertical-align: middle; + + &.user { + display: none; + } + + & > svg { + width: 18px; + height: 18px; + margin-right: 8px; + vertical-align: middle; + } +`; + +const ButtonContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +const Btn = styled(Button)` + width: 160px; + height: 32px; + font-size: 14px; + font-weight: 500; +`; + +/* +1. Close !isOpen && !isDone 아직 열리지 않은 예약 +2. Open isOpen && !isDone 열려서 들어갈 수 있는 예약 +3. Done isOpen && isDone 완료된 예약 +*/ + +const dummyText = `그러나 한 시와 강아지, 가을 보고, 새워 까닭입니다. 까닭이요, 이름을 옥 별들을 많은 까닭입니다. 그리워 동경과 둘 이런 이런 계절이 거외다. 나의 오면 언덕 하나 무덤 이런 아직 있습니다. 자랑처럼 하나 무성할 패, 까닭입니다. 하나의 별 사람들의 너무나 별 피어나듯이 당신은 북간도에 봅니다.그러나 한 시와 강아지, 가을 보고, 새워 까닭입니다. 까닭이요, 이름을 옥 별들을 많은 까닭입니다. 그리워 동경과 둘 이런 이런 계절이 거외다. 나의 오면 언덕 하나 무덤 이런 아직 있습니다. 자랑처럼 하나 무성할 패, 까닭입니다. 하나의 별 사람들의 너무나 별 피어나듯이 당신은 북간도에 봅니다.`; + +const notReadyText = `아직 비대면 진료 요약이 생성되지 않았습니다.`; + +const ReservationItem = ({ name, role, dept, date, index }) => { + const dispatch = useDispatch(); + + const image = useMemo(() => { + switch (role) { + case "ADMIN_DOCTOR": + return DoctorIcon; + case "ADMIN_THERAPIST": + return TherapistIcon; + default: + return PatientIcon; + } + }, [role]); + + const roleDisplay = useMemo(() => { + const find = ROLE_TYPE.find((item) => item.key === role); + + if (find) return find.value; + return "환자"; + }, [role]); + + const fullDate = useMemo( + () => dayjs(date).add(index * 30, "minute"), + [date, index], + ); + + const time = useMemo(() => dayjs().diff(fullDate, "minute"), [fullDate]); + const isUser = useMemo(() => classNames({ user: role === "USER" }), [role]); + + const isOpen = time >= -10; + const isDone = time > 30; + + function firstButton() { + if (isDone) { + return 종료되었습니다; + } else if (isOpen) { + return 입장; + } else { + return 예약 시간이 아닙니다; + } + } + + function showDetail() { + dispatch( + show({ + id: "reservation_detail", + props: { + chartDetail: null, + description: dummyText, + aiSummary: notReadyText, + }, + }), + ); + } + + return ( + + + + + + {name}님 + + + + + + {roleDisplay} + + + + {fullDate.format(DAYJS_FORMAT)} + + + + + + {dept} + + + + + {firstButton()} + + 상세 정보 + + + + ); +}; + +ReservationItem.propTypes = { + name: PropTypes.string, + role: PropTypes.string, + date: PropTypes.string, + dept: PropTypes.string, + index: PropTypes.number, +}; + +ReservationItem.defaultProps = { + role: "USER", +}; + +export default ReservationItem; diff --git a/src/components/Reservation/ReservationList.jsx b/src/components/Reservation/ReservationList.jsx new file mode 100644 index 0000000..07bf9a7 --- /dev/null +++ b/src/components/Reservation/ReservationList.jsx @@ -0,0 +1,76 @@ +import styled from "styled-components"; +import ReservationItem from "./ReservationItem"; +import Pagination from "../Pagination/Pagination"; +import { ReducerContext } from "../../reducer/context.js"; +import { useEffect, useReducer } from "react"; +import { + intialReservationListState, + reservationListReducer, +} from "../../reducer/reservation-list.js"; +import BlockContainer from "../Common/BlockContainer.jsx"; +import TitleText from "../Common/TitleText.jsx"; +import { getAdminReservationList } from "../../librarys/api/reservation.js"; +import ReservationModal from "./ReservationModal.jsx"; + +const List = styled.div` + margin: 28px 0; + display: flex; + flex-direction: column; + gap: 28px; +`; + +const ReservationList = () => { + const [state, dispatch] = useReducer( + reservationListReducer, + intialReservationListState, + ); + const { list, page } = state; + + useEffect(() => { + (async () => { + const data = await getAdminReservationList("ldh", page); + dispatch({ + type: "data", + payload: data, + }); + })(); + }, [page]); + + return ( + + + + + + {list.map((item) => ( + + ))} + + + + + + + ); +}; + +export default ReservationList; diff --git a/src/components/Reservation/ReservationModal.jsx b/src/components/Reservation/ReservationModal.jsx new file mode 100644 index 0000000..36b2f09 --- /dev/null +++ b/src/components/Reservation/ReservationModal.jsx @@ -0,0 +1,60 @@ +import styled from "styled-components"; +import PropTypes from "prop-types"; + +import Modal from "../Common/Modal.jsx"; + +import { selectProps } from "../../redux/modalSlice.js"; +import { useSelector } from "react-redux"; +import ModalTitleText from "../Common/ModalTitleText.jsx"; +import ChartSummary from "../Chart/ChartSummary.jsx"; +import InputAreaContainer from "../Input/InputAreaContainer.jsx"; +import Button from "../Button/Button.jsx"; + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 28px; +`; + +const Chart = styled(ChartSummary)` + margin-top: 16px; + width: 100%; +`; + +const Input = styled(InputAreaContainer)` + width: 100%; +`; + +const ButtonContainer = styled.div` + margin-top: 8px; + margin-bottom: 32px; + display: flex; + gap: 24px; +`; + +const id = "reservation_detail"; + +const ReservationModal = () => { + const value = useSelector(selectProps(id)); + const { description, aiSummary } = value || {}; + + return ( + + + + + + + + + + + + + ); +}; + +ReservationModal.propTypes = {}; + +export default ReservationModal; diff --git a/src/index.scss b/src/index.scss index dd1b9b0..1d92363 100644 --- a/src/index.scss +++ b/src/index.scss @@ -13,4 +13,9 @@ html { } +body { + // 가로 스크롤 생기면 이중 스크롤 생기는 문제 수정 + overflow-y: hidden; +} + @import url(//spoqa.github.io/spoqa-han-sans/css/SpoqaHanSansNeo.css); diff --git a/src/librarys/api/reservation.js b/src/librarys/api/reservation.js new file mode 100644 index 0000000..428c864 --- /dev/null +++ b/src/librarys/api/reservation.js @@ -0,0 +1,12 @@ +import { getSpringAxios } from "./axios.js"; + +export async function getAdminReservationList(id, page = undefined) { + const axios = getSpringAxios(); + + const params = { + page, + }; + + const response = await axios.get("/reservation-admin/" + id, { params }); + return response.data; +} diff --git a/src/librarys/type.js b/src/librarys/type.js index 346f347..8415af5 100644 --- a/src/librarys/type.js +++ b/src/librarys/type.js @@ -1,8 +1,8 @@ export const ROLE_TYPE = [ - { key: "VISITOR", value: 0 }, - { key: "USER", value: 1 }, - { key: "ADMIN_DOCTOR", value: 2 }, - { key: "ADMIN_THERAPIST", value: 3 }, + { key: "VISITOR", value: "방문자" }, + { key: "USER", value: "환자" }, + { key: "ADMIN_DOCTOR", value: "전문의" }, + { key: "ADMIN_THERAPIST", value: "재활치료사" }, ]; export const CATEGORY_TYPE = [ @@ -11,3 +11,5 @@ export const CATEGORY_TYPE = [ { key: "KNEE", value: "무릎" }, { key: "THIGH", value: "허벅지" }, ]; + +export const DAYJS_FORMAT = "YYYY/MM/DD HH:mm"; diff --git a/src/pages/Doctor/DoctorUntactReservePage.jsx b/src/pages/Doctor/DoctorUntactReservePage.jsx index 8b9c2d7..f3a5368 100644 --- a/src/pages/Doctor/DoctorUntactReservePage.jsx +++ b/src/pages/Doctor/DoctorUntactReservePage.jsx @@ -1,28 +1,12 @@ -import styled from "styled-components"; import BackButton from "../../components/Button/BackButton"; -import DoctorUntactFullList from "../../components/DoctorDashBoard/DoctorUntactFullList"; - -const PageContainer = styled.div` - display: flex; - flex-direction: column; - height: 100vh; -`; - -const CenteredContainer = styled.div` - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - flex: 1; -`; +import PageContainer from "../../components/Common/PageContainer"; +import ReservationList from "../../components/Reservation/ReservationList.jsx"; const DoctorUntactReservePage = () => { return ( - - - - + + ); }; diff --git a/src/reducer/reservation-list.js b/src/reducer/reservation-list.js new file mode 100644 index 0000000..a5a1ca1 --- /dev/null +++ b/src/reducer/reservation-list.js @@ -0,0 +1,26 @@ +export const intialReservationListState = { + list: [], + page: 1, + totalPage: 1, +}; + +export function reservationListReducer(state, action) { + switch (action.type) { + case "page": + return { + ...state, + page: action.payload, + }; + case "data": + return { + list: action.payload.dtoList || [], + page: action.payload.page || 1, + totalPage: action.payload.end || 1, + }; + default: + console.error( + "[ReservationListReducer] Undefined action: " + action.type, + ); + return state; + } +} diff --git a/src/reducer/video-list.js b/src/reducer/video-list.js index a91d1f8..e4c1816 100644 --- a/src/reducer/video-list.js +++ b/src/reducer/video-list.js @@ -1,12 +1,9 @@ -import { CATEGORY_TYPE } from "../librarys/type.js"; - export const intialVideoListState = { query: "", category: null, list: [], page: 1, - totalItems: 80, - itemsPerPage: 10, + totalPage: 1, }; export function videoListReducer(state, action) {