Skip to content

Commit 3d8f3c0

Browse files
authored
Merge pull request #255 from swiftss-org/staging
Implemented a first version of a surgeon dashboard landing page
2 parents f81fc72 + 49b34d4 commit 3d8f3c0

File tree

13 files changed

+318
-7
lines changed

13 files changed

+318
-7
lines changed

src/api/patientsAPI/patientsAPI.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@ export default {
2929
dischargePatient: (params: DischargePayload) => request(METHODS.POST, `/discharges/`, { params }),
3030
followUpPatient: (params: FollowUpPayload) => request(METHODS.POST, `/follow-ups/`, { params }),
3131
getPreferredHospital: () => request(METHODS.GET, '/preferred-hospital/retrieve_for_current_user/', {}),
32+
getSurgeonEpisodeSummary: () => request(METHODS.GET, '/surgeon-episode-summary/', {}),
33+
getOwnedEpisodes: () => request(METHODS.GET, '/owned-episodes/', {}),
3234
};

src/hooks/api/patientHooks.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ import {
1414
HospitalMappingPayload,
1515
HospitalsAPI,
1616
HospitalsResponse,
17+
OwnedEpisodeAPI,
1718
PaginationParams,
1819
PatientAPI,
1920
PatientsPayload,
20-
PatientsResponse, PreferredHospital,
21+
PatientsResponse,
22+
PreferredHospital,
2123
RegisterEpisodePayload,
2224
RegisterPatientPayload,
25+
SurgeonEpisodeSummaryAPI,
2326
SurgeonsResponse,
2427
} from '../../models/apiTypes';
2528
import { RegisterEpisodeFormType } from '../../pages/RegisterEpisode/types';
@@ -59,6 +62,38 @@ export const useGetPreferredHospital = () => {
5962
);
6063
};
6164

65+
export const useGetSurgeonEpisodeSummary = () => {
66+
return useQuery<SurgeonEpisodeSummaryAPI, AxiosError, SurgeonEpisodeSummaryAPI>(
67+
ReactQueryKeys.SurgeonEpisodeSummaryQuery,
68+
async () => {
69+
const { request } = patientsAPI.single.getSurgeonEpisodeSummary();
70+
return await request();
71+
},
72+
{
73+
onError: (error) => {
74+
console.error("Error fetching surgeon episode stats:", error);
75+
},
76+
retry: false,
77+
}
78+
);
79+
};
80+
81+
export const useGetOwnedEpisodes = () => {
82+
return useQuery<OwnedEpisodeAPI[], AxiosError>(
83+
ReactQueryKeys.OwnedEpisodesQuery,
84+
async () => {
85+
const { request } = patientsAPI.single.getOwnedEpisodes();
86+
return await request();
87+
},
88+
{
89+
onError: (error: AxiosError) => {
90+
console.error("Error fetching owned episodes:", error);
91+
},
92+
retry: false,
93+
}
94+
);
95+
};
96+
6297
export const useGetHospital = (id: string) => {
6398
return useQuery<HospitalsAPI, AxiosError, HospitalsAPI>(
6499
[ReactQueryKeys.HospitalsQuery, id],
@@ -333,3 +368,4 @@ export const useFollowUp = (episodeID: string) => {
333368
}
334369
);
335370
};
371+

src/hooks/api/userHooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const useSignIn = () => {
3333
setUserStorageItem(__EMAIL__, data?.user.email ?? '');
3434
setAxiosToken(data?.token ?? '');
3535

36-
history.replace(urls.patients());
36+
history.replace(urls.landingPage());
3737
},
3838
onError: (errors) => {
3939
setNotification('Invalid credential combination.', 'error');

src/hooks/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ export const ReactQueryKeys = {
44
SurgeonsQuery: 'surgeonsQuery',
55
EpisodesQuery: 'episodesQuery',
66
PreferredHospitalQuery: 'preferredHospital',
7+
SurgeonEpisodeSummaryQuery: 'surgeonEpisodeSummary',
8+
OwnedEpisodesQuery: 'ownedEpisodes',
79
};

src/models/apiTypes.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,19 @@ export interface PreferredHospital {
220220
hospital: {
221221
id: number;
222222
};
223-
}
223+
}
224+
225+
export type SurgeonEpisodeSummaryAPI = {
226+
episode_count: number;
227+
last_episode_date: string;
228+
};
229+
230+
export type OwnedEpisodeAPI = {
231+
id: number;
232+
surgery_date: string;
233+
patient_name: string;
234+
discharge: DischargeAPI | null;
235+
follow_up_dates: FollowUpAPI[];
236+
patient_id: number;
237+
hospital_id: number;
238+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import styled from '@emotion/styled';
2+
3+
import { scrollBar } from '../../common.style';
4+
5+
export const DashboardWrapper = styled.div`
6+
${scrollBar};
7+
margin-bottom: 16px;
8+
overflow-y: auto;
9+
padding: 0px 16px 18px 16px;
10+
@media (max-width: 1200px) {
11+
height: calc(100vh - 280px);
12+
}
13+
}
14+
`;
15+
16+
export const DashboardText = styled.div`
17+
color: ${(props) => props.theme.utils.getColor('darkGray', 400)};
18+
`;
19+
20+
export const DashboardTextHeader = styled.div`
21+
color: ${(props) => props.theme.utils.getColor('darkGray', 400)};
22+
font-size: 14px;
23+
font-weight: 400;
24+
`;
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import React, { useState } from 'react';
2+
3+
import { Button } from '@orfium/ictinus';
4+
import { useGetSurgeonEpisodeSummary, useGetOwnedEpisodes } from 'hooks/api/patientHooks';
5+
import { useHistory } from 'react-router-dom';
6+
import urls from 'routing/urls';
7+
8+
import {
9+
ButtonContainer, PageTitle,
10+
PageWrapper,
11+
} from '../../common.style';
12+
import { useResponsiveLayout } from '../../hooks/useResponsiveSidebar';
13+
import { OwnedEpisodeAPI } from '../../models/apiTypes';
14+
import { DashboardText, DashboardTextHeader, DashboardWrapper } from './LandingPage.style';
15+
16+
const LandingPage: React.FC = () => {
17+
const { data: surgeonEpisodeSummary, error: surgeonError } = useGetSurgeonEpisodeSummary();
18+
const { data: ownedEpisodes = [], error: episodesError } = useGetOwnedEpisodes();
19+
const history = useHistory();
20+
const { isDesktop } = useResponsiveLayout();
21+
22+
// State to handle sorting
23+
const [sortConfig, setSortConfig] = useState<{
24+
key: keyof OwnedEpisodeAPI | 'discharged';
25+
direction: 'ascending' | 'descending';
26+
}>({
27+
key: 'surgery_date', // Default sort by surgery date
28+
direction: 'descending',
29+
});
30+
31+
// Function to handle sorting when a column header is clicked
32+
const handleSort = (key: keyof OwnedEpisodeAPI | 'discharged') => {
33+
let direction: 'ascending' | 'descending' = 'ascending';
34+
if (sortConfig.key === key && sortConfig.direction === 'ascending') {
35+
direction = 'descending';
36+
}
37+
setSortConfig({ key, direction });
38+
};
39+
40+
// Sort ownedEpisodes based on sortConfig
41+
const sortedEpisodes = ownedEpisodes
42+
? [...ownedEpisodes].sort((a: OwnedEpisodeAPI, b: OwnedEpisodeAPI) => {
43+
44+
// Custom sorting for follow_up_dates
45+
if (sortConfig.key === 'follow_up_dates') {
46+
const aFollowUps = a.follow_up_dates.length;
47+
const bFollowUps = b.follow_up_dates.length;
48+
return sortConfig.direction === 'ascending'
49+
? aFollowUps - bFollowUps
50+
: bFollowUps - aFollowUps;
51+
}
52+
53+
// Custom sorting for discharge status
54+
if (sortConfig.key === 'discharged') {
55+
const aHasDischarge = a.discharge !== null ? 1 : 0; // 1 if has discharge, 0 if not
56+
const bHasDischarge = b.discharge !== null ? 1 : 0; // 1 if has discharge, 0 if not
57+
return sortConfig.direction === 'ascending' ? aHasDischarge - bHasDischarge : bHasDischarge - aHasDischarge;
58+
}
59+
60+
// Fallback to other sorting keys
61+
const aValue = a[sortConfig.key] ?? '';
62+
const bValue = b[sortConfig.key] ?? '';
63+
64+
if (aValue < bValue) {
65+
return sortConfig.direction === 'ascending' ? -1 : 1;
66+
}
67+
if (aValue > bValue) {
68+
return sortConfig.direction === 'ascending' ? 1 : -1;
69+
}
70+
return 0;
71+
})
72+
: [];
73+
74+
const handleRowClick = (episode: OwnedEpisodeAPI) => {
75+
const { hospital_id, patient_id, id } = episode;
76+
history.push(`${urls.patients()}/${hospital_id}/${patient_id}${urls.episodes()}/${id}`);
77+
};
78+
79+
return (
80+
<PageWrapper isDesktop={isDesktop}>
81+
<PageTitle>
82+
Surgeon Dashboard
83+
</PageTitle>
84+
<DashboardWrapper>
85+
<DashboardText>
86+
This is your personalized landing page with key insights on the episodes you have performed.
87+
</DashboardText>
88+
{/* Handle error state */}
89+
{surgeonError && <div style={{ color: 'red' }}>Failed to load surgeon stats: {surgeonError.message}</div>}
90+
{episodesError && <div style={{ color: 'red' }}>Failed to load episodes: {episodesError.message}</div>}
91+
92+
{/* Render surgeon summary data */}
93+
{surgeonEpisodeSummary ? (
94+
<DashboardText>
95+
<p>Number of episodes: {surgeonEpisodeSummary.episode_count}</p>
96+
<p>
97+
Last episode:{' '}
98+
{surgeonEpisodeSummary.last_episode_date
99+
? new Date(surgeonEpisodeSummary.last_episode_date).toLocaleDateString()
100+
: 'N/A'}
101+
</p>
102+
</DashboardText>
103+
) : (
104+
105+
<DashboardText>Loading surgeon summary...</DashboardText>
106+
)}
107+
108+
{/* Render owned episodes table */}
109+
{ownedEpisodes ? (
110+
ownedEpisodes.length > 0 ? (
111+
<div>
112+
<DashboardTextHeader>
113+
<h2>Your Episodes</h2>
114+
</DashboardTextHeader>
115+
<DashboardText>
116+
<p>
117+
Click on a column header to order results by that column, or click on a row to see the details of that
118+
episode.
119+
</p>
120+
</DashboardText>
121+
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
122+
<thead>
123+
<tr>
124+
<th
125+
onClick={() => handleSort('surgery_date')}
126+
style={{ border: '1px solid lightgrey', padding: '8px', cursor: 'pointer' }}
127+
>
128+
Surgery Date{' '}
129+
{sortConfig.key === 'surgery_date'
130+
? sortConfig.direction === 'ascending'
131+
? '↑'
132+
: '↓'
133+
: ''}
134+
</th>
135+
<th
136+
onClick={() => handleSort('patient_name')}
137+
style={{ border: '1px solid lightgrey', padding: '8px', cursor: 'pointer' }}
138+
>
139+
Patient Name{' '}
140+
{sortConfig.key === 'patient_name'
141+
? sortConfig.direction === 'ascending'
142+
? '↑'
143+
: '↓'
144+
: ''}
145+
</th>
146+
<th
147+
onClick={() => handleSort('follow_up_dates')}
148+
style={{ border: '1px solid lightgrey', padding: '8px', cursor: 'pointer' }}
149+
>
150+
Follow-ups{' '}
151+
{sortConfig.key === 'follow_up_dates'
152+
? sortConfig.direction === 'ascending'
153+
? '↑'
154+
: '↓'
155+
: ''}
156+
</th>
157+
<th
158+
onClick={() => handleSort('discharged')}
159+
style={{ border: '1px solid lightgrey', padding: '8px', cursor: 'pointer' }}
160+
>
161+
Discharged{' '}
162+
{sortConfig.key === 'discharged'
163+
? sortConfig.direction === 'ascending'
164+
? '↑'
165+
: '↓'
166+
: ''}
167+
</th>
168+
</tr>
169+
</thead>
170+
<tbody>
171+
{sortedEpisodes.map((episode) => (
172+
<tr
173+
key={episode.id}
174+
onClick={() => handleRowClick(episode)}
175+
style={{
176+
cursor: 'pointer',
177+
borderBottom: '1px solid lightgrey',
178+
backgroundColor: episode.discharge ? 'inherit' : '#fc7c7c',
179+
}}
180+
>
181+
<td style={{ border: '1px solid lightgrey', padding: '8px' }}>
182+
{new Date(episode.surgery_date).toLocaleDateString()}
183+
</td>
184+
<td style={{ border: '1px solid lightgrey', padding: '8px' }}>{episode.patient_name}</td>
185+
<td style={{ border: '1px solid lightgrey', padding: '8px' }}>{episode.follow_up_dates.length}</td>
186+
<td style={{ border: '1px solid lightgrey', padding: '8px' }}>
187+
{episode.discharge ? 'Yes' : 'No'}
188+
</td>
189+
</tr>
190+
))}
191+
</tbody>
192+
</table>
193+
</div>
194+
) : (
195+
<div>
196+
<DashboardTextHeader>
197+
<h2>Your Episodes</h2>
198+
</DashboardTextHeader>
199+
<DashboardText>
200+
<p>You have not recorded any episodes yet.</p>
201+
</DashboardText>
202+
</div>
203+
)
204+
) : (
205+
<DashboardText>Loading episodes...</DashboardText>
206+
)}
207+
</DashboardWrapper>
208+
<ButtonContainer isDesktop={isDesktop}>
209+
<Button
210+
buttonType="button"
211+
block
212+
filled
213+
size="md"
214+
onClick={() => history.push(urls.patients())}
215+
>
216+
Go to Patient Directory
217+
</Button>
218+
</ButtonContainer>
219+
</PageWrapper>
220+
);
221+
};
222+
223+
export default LandingPage;

src/pages/LandingPage/index.ts

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

src/pages/Layout/Layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ const Layout: React.FC<Props> = ({ component: Component }) => {
6363
url: '/patients/register',
6464
options: [],
6565
},
66+
{
67+
name: 'Dashboard',
68+
visible: true,
69+
url: '/patients/landing',
70+
options: [],
71+
},
6672
// {
6773
// name: 'My Account',
6874
// visible: true,

src/routing/PrivateRoute.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const PrivateRoute: React.FC<CustomRouteProps> = ({ component: Component, ...res
1212
const token = getUserStorageItem(__TOKEN__);
1313

1414
if (!token) {
15-
return <Route {...rest} render={() => <Redirect to={urls.login()} />} />;
15+
return <Route {...rest} render={() => <Redirect to={urls.landingPage()} />} />;
1616
}
1717
if (rest.path === '/' || !Component) {
1818
return <Redirect to={urls.patients()} />;

0 commit comments

Comments
 (0)