Skip to content

Commit

Permalink
Enhancement : Carbonise Appointment widget
Browse files Browse the repository at this point in the history
  • Loading branch information
donaldkibet committed Sep 14, 2021
1 parent 3d41277 commit ea32b11
Show file tree
Hide file tree
Showing 13 changed files with 875 additions and 673 deletions.
712 changes: 419 additions & 293 deletions __mocks__/appointments.mock.ts

Large diffs are not rendered by default.

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;
}
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();
});
});
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;
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,18 @@
@import "~carbon-components/src/globals/scss/vars";
@import "~carbon-components/src/globals/scss/mixins";

.widgetCard {
border: 1px solid $ui-03;
}

.appointmentsHeader {
.appointmentTableHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: $spacing-04 0 $spacing-04 $spacing-05;
background-color: $ui-background;
}

.appointmentsHeader > h4:after {
.appointmentTableHeader > h4:after {
content: "";
display: block;
width: 2rem;
padding-top: 0.188rem;
border-bottom: 0.375rem solid $brand-teal-01;
}

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;
Loading

0 comments on commit ea32b11

Please sign in to comment.