-
Notifications
You must be signed in to change notification settings - Fork 252
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Enhancement : Carbonise Appointment widget
- Loading branch information
1 parent
3d41277
commit ea32b11
Showing
13 changed files
with
875 additions
and
673 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
89 changes: 89 additions & 0 deletions
89
packages/esm-patient-appointments-app/src/appointments/appointment-base.component.scss
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,89 @@ | ||
@import "~@openmrs/esm-styleguide/src/vars"; | ||
@import "~carbon-components/src/globals/scss/vars"; | ||
@import "~carbon-components/src/globals/scss/mixins"; | ||
@import "~carbon-components/scss/globals/scss/vendor/@carbon/elements/scss/type/styles"; | ||
@import "~carbon-components/src/globals/scss/vendor/@carbon/layout/scss/generated/spacing"; | ||
|
||
// TO DO Move this styles to style - guide | ||
// https://github.com/openmrs/openmrs-esm-core/blob/master/packages/framework/esm-styleguide/src/_vars.scss | ||
$color-blue-30 : #a6c8ff; | ||
$color-blue-10: #edf5ff; | ||
|
||
.card { | ||
border: 1px solid $ui-03; | ||
} | ||
|
||
.productiveHeading01 { | ||
@include carbon--type-style("productive-heading-01"); | ||
} | ||
|
||
.appointmentHeader { | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: center; | ||
padding: $spacing-04 0 0 $spacing-05; | ||
background-color: $ui-background; | ||
} | ||
|
||
.appointmentHeader > h4:after { | ||
content: ""; | ||
display: block; | ||
width: $spacing-07; | ||
padding-top: 0.188rem; | ||
border-bottom: 0.375rem solid $brand-teal-01; | ||
} | ||
|
||
.contentSwitcherWrapper { | ||
display: flex; | ||
width: fit-content; | ||
justify-content: flex-end; | ||
align-items: center; | ||
width: 60%; | ||
} | ||
|
||
|
||
.contentSwitcherWrapper > div > button { | ||
background-color: $ui-02; | ||
} | ||
|
||
.contentSwitcherWrapper > div button:first-child { | ||
border-top: 1px solid $color-blue-30; | ||
border-bottom: 1px solid $color-blue-30; | ||
border-left: 1px solid $color-blue-30; | ||
border-right: none; | ||
border-radius: $spacing-02 0 0px $spacing-02; | ||
} | ||
.contentSwitcherWrapper > div button:last-child { | ||
border-top: 1px solid $color-blue-30; | ||
border-bottom: 1px solid $color-blue-30; | ||
border-right: 1px solid $color-blue-30; | ||
border-left: none; | ||
border-radius: 0px $spacing-02 $spacing-02 0px; | ||
} | ||
|
||
.contentSwitcherWrapper > div > button[aria-selected=true], | ||
.contentSwitcherWrapper > div > button[aria-selected=true]:first-child { | ||
background-color: $color-blue-10; | ||
color: $color-blue-60-2; | ||
border-color: $color-blue-60-2; | ||
border-right: 1px solid $color-blue-60-2; | ||
} | ||
|
||
.contentSwitcherWrapper > div > button[aria-selected=true], | ||
.contentSwitcherWrapper > div > button[aria-selected=true]:last-child { | ||
background-color: $color-blue-10; | ||
color: $color-blue-60-2; | ||
border-color: $color-blue-60-2; | ||
border-left: 1px solid $color-blue-60-2; | ||
} | ||
|
||
.contentSwitcherWrapper > div > button[aria-selected=true]:focus { | ||
box-shadow: none; | ||
} | ||
|
||
|
||
.divider { | ||
border-left: 1px solid $ui-03; | ||
height: $spacing-05; | ||
margin: 0 $spacing-05 0 $spacing-06; | ||
} |
111 changes: 111 additions & 0 deletions
111
packages/esm-patient-appointments-app/src/appointments/appointment-base.component.test.tsx
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,111 @@ | ||
import React from 'react'; | ||
import AppointmentBase from './appointment-base.component'; | ||
import { screen, render } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { mockPatient } from '../../../../__mocks__/patient.mock'; | ||
import { useAppointments } from '../hooks/useAppointments'; | ||
import { mockAppointmentsData } from '../../../../__mocks__/appointments.mock'; | ||
import { usePagination, attach } from '@openmrs/esm-framework'; | ||
|
||
const mockUsePagination = usePagination as jest.Mock; | ||
const mockUseAppointments = useAppointments as jest.Mock; | ||
const mockAttach = attach as jest.Mock; | ||
|
||
jest.mock('../hooks/useAppointments', () => ({ | ||
useAppointments: jest.fn(), | ||
})); | ||
|
||
jest.mock('@openmrs/esm-framework', () => ({ | ||
usePagination: jest.fn(), | ||
attach: jest.fn(), | ||
})); | ||
|
||
describe('AppointmentBase', () => { | ||
const renderAppointments = () => { | ||
mockUseAppointments.mockReturnValue({ ...mockAppointmentsData, status: 'resolved', error: null }); | ||
mockUsePagination.mockReturnValue({ | ||
results: mockAppointmentsData.upComingAppointments, | ||
goTo: () => {}, | ||
currentPage: 1, | ||
}); | ||
return render(<AppointmentBase patientUuid={mockPatient.id} />); | ||
}; | ||
|
||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
it('should render an empty state when no appointment are available', () => { | ||
mockUseAppointments.mockReturnValue({ | ||
patientAppointments: [], | ||
pastAppointments: [], | ||
upcomingAppointments: [], | ||
status: 'resolved', | ||
error: null, | ||
}); | ||
render(<AppointmentBase patientUuid={mockPatient.id} />); | ||
|
||
expect(screen.getByText(/There are no appointments to display for this patient/i)).toBeInTheDocument(); | ||
const launchAppointmentsForm = screen.getByText(/Record appointments/); | ||
userEvent.click(launchAppointmentsForm); | ||
expect(mockAttach).toHaveBeenCalledWith('patient-chart-workspace-slot', 'appointment-form-workspace'); | ||
}); | ||
|
||
it('should render upcoming appointments by default', () => { | ||
renderAppointments(); | ||
|
||
expect(screen.getByText('Appointments')).toBeInTheDocument(); | ||
const upcomingTab = screen.getByRole('tab', { name: /Upcoming/ }); | ||
expect(upcomingTab).toBeInTheDocument(); | ||
const pastTab = screen.getByRole('tab', { name: /Past/ }); | ||
expect(pastTab).toBeInTheDocument(); | ||
|
||
// Upcomming appointment selected | ||
expect(upcomingTab).toHaveAttribute('aria-selected', 'true'); | ||
expect(pastTab).toHaveAttribute('aria-selected', 'false'); | ||
|
||
// Switch between upcoming and past appointment | ||
userEvent.click(pastTab); | ||
expect(upcomingTab).toHaveAttribute('aria-selected', 'false'); | ||
expect(pastTab).toHaveAttribute('aria-selected', 'true'); | ||
|
||
const launchAppointmentForm = screen.getByRole('button', { name: /Add/i }); | ||
userEvent.click(launchAppointmentForm); | ||
expect(mockAttach).toHaveBeenCalledWith('patient-chart-workspace-slot', 'appointment-form-workspace'); | ||
}); | ||
|
||
it('should display error state', () => { | ||
const mockError = { | ||
message: 'API Down', | ||
response: { | ||
status: 500, | ||
statusText: 'API is down', | ||
}, | ||
}; | ||
mockUseAppointments.mockReturnValue({ ...mockAppointmentsData, status: 'error', error: mockError }); | ||
render(<AppointmentBase patientUuid={mockPatient.id} />); | ||
|
||
expect(screen.getByText(/Appointments/)).toBeInTheDocument(); | ||
expect(screen.getByText(/API is down/)).toBeInTheDocument(); | ||
expect(screen.getByText(/Sorry, there was a problem displaying this information/i)).toBeInTheDocument(); | ||
}); | ||
|
||
it('should render appointment table correctly', () => { | ||
const { container } = renderAppointments(); | ||
|
||
// Contains table headers | ||
expect(screen.getByRole('columnheader', { name: /Date/ })).toBeInTheDocument(); | ||
expect(screen.getByRole('columnheader', { name: /Location/ })).toBeInTheDocument(); | ||
expect(screen.getByRole('columnheader', { name: /Service/ })).toBeInTheDocument(); | ||
|
||
// Display correct rows | ||
const rows = screen.getAllByRole('row'); | ||
expect(rows.length).toBe(3); | ||
expect(screen.getByRole('cell', { name: /InPatient/ })).toBeInTheDocument(); | ||
expect(screen.getByRole('cell', { name: /MTRH Clinic/ })).toBeInTheDocument(); | ||
expect(screen.getByRole('cell', { name: /14 - Sep -2021 , 10:09/ })).toBeInTheDocument(); | ||
expect(screen.getByRole('cell', { name: /Isolation Ward/ })).toBeInTheDocument(); | ||
expect(screen.getByRole('cell', { name: /14 - Sep -2021 , 15:09/ })).toBeInTheDocument(); | ||
expect(screen.getByRole('cell', { name: /Outpatient/ })).toBeInTheDocument(); | ||
}); | ||
}); |
73 changes: 73 additions & 0 deletions
73
packages/esm-patient-appointments-app/src/appointments/appointment-base.component.tsx
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,73 @@ | ||
import React, { useState } from 'react'; | ||
import styles from './appointment-base.component.scss'; | ||
import Add16 from '@carbon/icons-react/es/add/16'; | ||
import Button from 'carbon-components-react/es/components/Button'; | ||
import DataTableSkeleton from 'carbon-components-react/es/components/DataTableSkeleton'; | ||
import { EmptyState, ErrorState } from '@openmrs/esm-patient-common-lib'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { useAppointments } from '../hooks/useAppointments'; | ||
import ContentSwitcher from 'carbon-components-react/es/components/ContentSwitcher'; | ||
import Switch from 'carbon-components-react/es/components/Switch'; | ||
import AppointmentTable from './appointment-table.component'; | ||
import { attach } from '@openmrs/esm-framework'; | ||
|
||
interface AppointmentBaseProps { | ||
basePath?: string; | ||
patientUuid: string; | ||
} | ||
|
||
const AppointmentBase: React.FC<AppointmentBaseProps> = ({ patientUuid }) => { | ||
const { t } = useTranslation(); | ||
const [appointmentToDisplay, setAppointmentToDisplay] = useState(0); | ||
const { upcomingAppointments, pastAppointments, error, status, patientAppointments } = useAppointments(patientUuid); | ||
const headerTitle = t('appointments', 'Appointments'); | ||
|
||
const launchAppointmentsForm = () => { | ||
attach('patient-chart-workspace-slot', 'appointment-form-workspace'); | ||
}; | ||
|
||
return ( | ||
<> | ||
<> | ||
{status === 'pending' && <DataTableSkeleton rowCount={5} />} | ||
{status === 'resolved' && ( | ||
<> | ||
{patientAppointments.length > 0 ? ( | ||
<div className={styles.card}> | ||
<div className={styles.appointmentHeader}> | ||
<h4 className={`${styles.productiveHeading03} ${styles.text02}`}>{headerTitle}</h4> | ||
<div className={styles.contentSwitcherWrapper}> | ||
<ContentSwitcher size="md" onChange={({ index }) => setAppointmentToDisplay(index)}> | ||
<Switch name={'upcoming'} text={t('upcoming', 'Upcoming')} /> | ||
<Switch name={'second'} text={t('past', 'Past')} /> | ||
</ContentSwitcher> | ||
<div className={styles.divider}></div> | ||
<Button | ||
kind="ghost" | ||
renderIcon={Add16} | ||
iconDescription="Add Appointments" | ||
onClick={launchAppointmentsForm}> | ||
{t('add', 'Add')} | ||
</Button> | ||
</div> | ||
</div> | ||
|
||
{appointmentToDisplay === 0 && <AppointmentTable patientAppointments={upcomingAppointments} />} | ||
{appointmentToDisplay === 1 && <AppointmentTable patientAppointments={pastAppointments} />} | ||
</div> | ||
) : ( | ||
<EmptyState | ||
launchForm={launchAppointmentsForm} | ||
displayText={t('appointments', 'Appointments')} | ||
headerTitle={t('appointments', 'Appointments')} | ||
/> | ||
)} | ||
</> | ||
)} | ||
{status === 'error' && <ErrorState headerTitle={headerTitle} error={error} />} | ||
</> | ||
</> | ||
); | ||
}; | ||
|
||
export default AppointmentBase; |
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
94 changes: 94 additions & 0 deletions
94
packages/esm-patient-appointments-app/src/appointments/appointment-table.component.tsx
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,94 @@ | ||
import React, { useMemo } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Appointment } from '../types'; | ||
import DataTable, { | ||
Table, | ||
TableCell, | ||
TableContainer, | ||
TableBody, | ||
TableHead, | ||
TableHeader, | ||
TableRow, | ||
DataTableHeader, | ||
} from 'carbon-components-react/es/components/DataTable'; | ||
import styles from './appointment-table.component.scss'; | ||
import dayjs from 'dayjs'; | ||
import { PatientChartPagination } from '@openmrs/esm-patient-common-lib'; | ||
import { usePagination } from '@openmrs/esm-framework'; | ||
|
||
const pageSize = 5; | ||
|
||
interface AppointmentTableProps { | ||
patientAppointments: Array<Appointment>; | ||
} | ||
|
||
const AppointmentTable: React.FC<AppointmentTableProps> = ({ patientAppointments = [] }) => { | ||
const { t } = useTranslation(); | ||
const { results, currentPage, goTo } = usePagination(patientAppointments, pageSize); | ||
const tableHeader: Array<DataTableHeader> = useMemo( | ||
() => [ | ||
{ key: 'date', header: t('date', 'Date') }, | ||
{ key: 'location', header: t('location', 'Location') }, | ||
{ key: 'service', header: t('service', 'Service') }, | ||
], | ||
[t], | ||
); | ||
|
||
const tableRows = useMemo( | ||
() => | ||
results.map((appointment) => { | ||
return { | ||
id: `${appointment.uuid}`, | ||
date: dayjs((appointment.startDateTime / 1000) * 1000).format('DD - MMM -YYYY , HH:MM'), | ||
location: appointment.location.name, | ||
service: appointment.service.name, | ||
}; | ||
}), | ||
[results], | ||
); | ||
|
||
return ( | ||
<div> | ||
<TableContainer> | ||
<DataTable rows={tableRows} headers={tableHeader} isSortable={true} size="short"> | ||
{({ rows, headers, getHeaderProps, getTableProps }) => ( | ||
<Table {...getTableProps()} useZebraStyles> | ||
<TableHead> | ||
<TableRow> | ||
{headers.map((header) => ( | ||
<TableHeader | ||
className={`${styles.productiveHeading01} ${styles.text02}`} | ||
{...getHeaderProps({ | ||
header, | ||
isSortable: header.isSortable, | ||
})}> | ||
{header.header?.content ?? header.header} | ||
</TableHeader> | ||
))} | ||
</TableRow> | ||
</TableHead> | ||
<TableBody> | ||
{rows.map((row) => ( | ||
<TableRow key={row.id}> | ||
{row.cells.map((cell) => ( | ||
<TableCell key={cell.id}>{cell.value?.content ?? cell.value}</TableCell> | ||
))} | ||
</TableRow> | ||
))} | ||
</TableBody> | ||
</Table> | ||
)} | ||
</DataTable> | ||
</TableContainer> | ||
<PatientChartPagination | ||
currentItems={results.length} | ||
totalItems={patientAppointments.length} | ||
onPageNumberChange={({ page }) => goTo(page)} | ||
pageNumber={currentPage} | ||
pageSize={pageSize} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export default AppointmentTable; |
Oops, something went wrong.