Skip to content

Commit d646798

Browse files
committed
feat: add General Information and Patient Details screen
1 parent 9c66f1a commit d646798

File tree

16 files changed

+360
-1
lines changed

16 files changed

+360
-1
lines changed

src/api/patientsAPI/patientsAPI.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { METHODS, request } from '../axiosInstances';
44

55
export default {
66
getHospitals: (params?: PaginationParams) => request(METHODS.GET, '/hospitals/', { params }),
7+
getHospital: (id: string) => request(METHODS.GET, `/hospitals/${id}`, {}),
78
getPatients: (params?: PatientsPayload) => request(METHODS.GET, '/patients/', { params }),
9+
getPatient: (id: string) => request(METHODS.GET, `/patients/${id}`, {}),
810
registerPatient: (params: RegisterPatientPayload) =>
911
request(METHODS.POST, '/patients/', { params }),
1012
};

src/components/Tabs/Tabs.style.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import styled from '@emotion/styled';
2+
import { flex } from '@orfium/ictinus/dist/theme/functions';
3+
import { rem } from 'polished';
4+
5+
export const TabWrapper = styled.button<{ isActive: boolean }>`
6+
background: transparent;
7+
border: none;
8+
color: ${({
9+
theme: {
10+
utils: { getColor },
11+
},
12+
isActive,
13+
}) => (isActive ? getColor('primary', 500, 'normal') : getColor('lightGray', 600))};
14+
15+
cursor: pointer;
16+
font-size: ${({ theme }) => theme.typography.fontSizes[18]};
17+
font-weight: ${({ theme, isActive }) => theme.typography.weights[isActive ? 'bold' : 'regular']};
18+
margin-right: ${rem(42)};
19+
margin-top: ${({ theme }) => theme.spacing.sm};
20+
padding: 0;
21+
position: relative;
22+
transition: color 1s;
23+
24+
${({ theme, isActive }) =>
25+
isActive
26+
? `:after {
27+
content: ' ';
28+
position: absolute;
29+
top: ${rem(26)};
30+
left: 0;
31+
width: 100%;
32+
border-bottom: 2px solid ${theme.utils.getColor('primary', 400, 'normal')};
33+
animation-name: load;
34+
animation-duration: 1s;
35+
}`
36+
: ``} {
37+
}
38+
39+
@keyframes load {
40+
0% {
41+
opacity: 0;
42+
}
43+
44+
100% {
45+
opacity: 1;
46+
}
47+
}
48+
`;
49+
50+
export const TabsContainer = styled.ul`
51+
${flex};
52+
align-items: start;
53+
justify-content: start;
54+
margin: 0 0 ${({ theme }) => theme.spacing.md} 0;
55+
padding: 0 0 ${({ theme }) => theme.spacing.sm} ${({ theme }) => theme.spacing.md};
56+
width: 100%;
57+
> li {
58+
list-style: none;
59+
}
60+
`;

src/components/Tabs/Tabs.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { FC } from 'react';
2+
3+
import Tab, { TabConfig } from 'components/Tabs/components/Tab';
4+
5+
import { TabsContainer } from './Tabs.style';
6+
7+
interface Props {
8+
tabs: TabConfig[];
9+
shouldDisplayTabs?: boolean;
10+
matchActiveDataType: string;
11+
onTabClick(value: string): void;
12+
}
13+
14+
const Tabs: FC<Props> = ({
15+
children,
16+
onTabClick,
17+
matchActiveDataType,
18+
tabs,
19+
shouldDisplayTabs = false,
20+
}) => {
21+
return (
22+
<>
23+
{shouldDisplayTabs && (
24+
<TabsContainer role={'tablist'}>
25+
{tabs.map((tab) => (
26+
<Tab
27+
isActive={tab.value === matchActiveDataType}
28+
key={tab.value}
29+
tab={tab}
30+
onClick={() => {
31+
onTabClick(tab.value);
32+
}}
33+
/>
34+
))}
35+
</TabsContainer>
36+
)}
37+
38+
{children}
39+
</>
40+
);
41+
};
42+
43+
export default Tabs;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, { FC } from 'react';
2+
3+
import { TabWrapper } from 'components/Tabs/Tabs.style';
4+
5+
export type TabConfig = {
6+
label: string;
7+
value: string;
8+
};
9+
10+
interface Props {
11+
tab: TabConfig;
12+
onClick: () => void;
13+
isActive: boolean;
14+
}
15+
16+
const Tab: FC<Props> = ({ tab, onClick, isActive }) => {
17+
const { label } = tab;
18+
19+
return (
20+
<li role="presentation">
21+
<TabWrapper
22+
isActive={isActive}
23+
aria-selected={isActive ? 'true' : 'false'}
24+
role="tab"
25+
tabIndex={0}
26+
onClick={onClick}
27+
>
28+
{label}
29+
</TabWrapper>
30+
</li>
31+
);
32+
};
33+
34+
export default Tab;

src/components/Tabs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as Tabs } from './Tabs';

src/hooks/api/patientHooks.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { useHistory } from 'react-router-dom';
55

66
import patientsAPI from '../../api/patientsAPI';
77
import {
8+
HospitalsAPI,
89
HospitalsResponse,
910
PaginationParams,
11+
PatientAPI,
1012
PatientsPayload,
1113
PatientsResponse,
1214
RegisterPatientPayload,
@@ -31,6 +33,23 @@ export const useGetHospitals = (params?: PaginationParams) => {
3133
);
3234
};
3335

36+
export const useGetHospital = (id: string) => {
37+
return useQuery<HospitalsAPI, AxiosError, HospitalsAPI>(
38+
[ReactQueryKeys.HospitalsQuery, id],
39+
async () => {
40+
const { request } = patientsAPI.single.getHospital(id);
41+
return await request();
42+
},
43+
{
44+
onError: (errors) => {
45+
console.log(errors);
46+
},
47+
48+
retry: false,
49+
}
50+
);
51+
};
52+
3453
export const useGetPatients = (params?: PatientsPayload) => {
3554
return useQuery<PatientsResponse, AxiosError, PatientsResponse>(
3655
[
@@ -58,6 +77,22 @@ export const useGetPatients = (params?: PatientsPayload) => {
5877
);
5978
};
6079

80+
export const useGetPatient = (id: string) => {
81+
return useQuery<PatientAPI, AxiosError, PatientAPI>(
82+
[ReactQueryKeys.PatientsQuery, id],
83+
async () => {
84+
const { request } = patientsAPI.single.getPatient(id);
85+
return await request();
86+
},
87+
{
88+
onError: (errors) => {
89+
console.log(errors);
90+
},
91+
retry: false,
92+
}
93+
);
94+
};
95+
6196
export const useRegisterPatient = () => {
6297
const history = useHistory();
6398

src/models/apiTypes.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type HospitalsAPI = {
3636
patient_hospital_id?: number;
3737
};
3838

39+
// export type EpisodesAPI = {};
40+
3941
export interface HospitalsResponse extends PaginationResponse, PaginationParams {
4042
results: HospitalsAPI[];
4143
}
@@ -59,6 +61,7 @@ export interface PatientsPayload extends PaginationParams {
5961
}
6062

6163
export type PatientAPI = {
64+
id: number; // DB ID
6265
full_name: string;
6366
national_id: string;
6467
age: number;
@@ -70,6 +73,7 @@ export type PatientAPI = {
7073
phone_2: string;
7174
address: string;
7275
hospital_mappings: HospitalsAPI[];
76+
// episodes: EpisodesAPI[];
7377
};
7478

7579
export interface PatientsResponse extends PaginationResponse, PaginationParams {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import styled from '@emotion/styled';
2+
3+
export const ComponentWrapper = styled.div`
4+
padding: ${({ theme }) => theme.spacing.md};
5+
`;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/** @jsxImportSource @emotion/react */
2+
import React, { useState } from 'react';
3+
4+
import { Button, Icon } from '@orfium/ictinus';
5+
import { useGetHospital, useGetPatient } from 'hooks/api/patientHooks';
6+
import { useHistory, useRouteMatch } from 'react-router-dom';
7+
8+
import { ButtonContainer, PageTitle, PageWrapper } from '../../common.style';
9+
import { Tabs } from '../../components/Tabs';
10+
import urls from '../../routing/urls';
11+
import GeneralInformation from './components/GeneralInformation';
12+
import { ComponentWrapper } from './PatientDetails.style';
13+
14+
const tabs = [
15+
{ label: 'General Information', value: 'info' },
16+
{ label: 'Episodes', value: 'episodes' },
17+
];
18+
19+
const PatientDetails: React.FC = () => {
20+
const match = useRouteMatch<{ hospitalID?: string; patientID?: string }>();
21+
const [activeTab, setActiveTab] = useState('info');
22+
const history = useHistory();
23+
24+
const { hospitalID, patientID } = match.params;
25+
26+
const { data: patient, isLoading: isPatientLoading } = useGetPatient(patientID ?? '');
27+
const { data: hospital, isLoading: isHospitalLoading } = useGetHospital(hospitalID ?? '');
28+
29+
const isLoading = isHospitalLoading || isPatientLoading;
30+
31+
return (
32+
<PageWrapper>
33+
<PageTitle>
34+
<Icon
35+
name="fatArrowLeft"
36+
size={24}
37+
color={'lightGray-700'}
38+
onClick={() => {
39+
history.push(urls.patients());
40+
}}
41+
/>
42+
Patient Details
43+
</PageTitle>
44+
<Tabs
45+
matchActiveDataType={activeTab}
46+
onTabClick={(tabId) => {
47+
setActiveTab(tabId);
48+
}}
49+
tabs={tabs}
50+
shouldDisplayTabs
51+
/>
52+
<ComponentWrapper>
53+
{activeTab === 'info' ? (
54+
<GeneralInformation patient={patient} hospital={hospital} />
55+
) : (
56+
<div>Episodes</div>
57+
)}
58+
</ComponentWrapper>
59+
<ButtonContainer>
60+
<Button color={'blue-500'} buttonType="button" disabled={isLoading} block size="md">
61+
Register new episode
62+
</Button>
63+
</ButtonContainer>
64+
</PageWrapper>
65+
);
66+
};
67+
68+
export default PatientDetails;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import styled from '@emotion/styled';
2+
3+
import { flex } from 'theme/functions';
4+
5+
export const ComponentWrapper = styled.div`
6+
padding: ${({ theme }) => theme.spacing.md};
7+
`;
8+
9+
export const Container = styled.div`
10+
${flex};
11+
flex-direction: column;
12+
gap: 8px;
13+
`;

0 commit comments

Comments
 (0)