From 2a03fb0ce1b89498509094035a0512853f23cafb Mon Sep 17 00:00:00 2001 From: Isaac Akileng Date: Wed, 31 Jan 2024 17:13:24 +0300 Subject: [PATCH 1/2] Add tests ordered --- .idea/.gitignore | 8 + .idea/inspectionProfiles/Project_Default.xml | 6 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + package.json | 15 +- .../alis-header-illustration.component.tsx | 14 ++ .../alis-tests-ordered.component.tsx | 21 ++ src/alis-component/alis.scss | 12 + .../data-table/data-table.component.tsx | 212 +++++++++++++++++ src/components/data-table/data-tables.scss | 215 ++++++++++++++++++ .../empty-state-illustration.component.tsx | 53 +++++ src/components/header/header.component.tsx | 44 ++++ src/components/header/header.scss | 71 ++++++ src/constants.ts | 47 ++++ src/create-dashboard-link.component.tsx | 44 ++++ src/index.ts | 18 +- src/root.component.tsx | 37 +-- src/routes.json | 17 +- src/sample-data.tsx | 94 ++++++++ yarn.lock | 12 +- 21 files changed, 916 insertions(+), 44 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 src/alis-component/alis-header-illustration.component.tsx create mode 100644 src/alis-component/alis-tests-ordered.component.tsx create mode 100644 src/alis-component/alis.scss create mode 100644 src/components/data-table/data-table.component.tsx create mode 100644 src/components/data-table/data-tables.scss create mode 100644 src/components/empty-state/empty-state-illustration.component.tsx create mode 100644 src/components/header/header.component.tsx create mode 100644 src/components/header/header.scss create mode 100644 src/constants.ts create mode 100644 src/create-dashboard-link.component.tsx create mode 100644 src/sample-data.tsx diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..925eb9a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index e8729bc..e76e78b 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "@ugandaemr/esm-template-app", + "name": "@ugandaemr/esm-alis-app", "version": "1.0.0", "license": "MPL-2.0", - "description": "A template for creating frontend modules for UgandaEMR", - "browser": "dist/esm-ugandaemr-template-app.js", + "description": "An app for handling all ALIS related functionalities", + "browser": "dist/esm-alis-app.js", "main": "src/index.ts", "source": true, "scripts": { - "start": "openmrs develop", + "start": "openmrs develop --backend http://194.163.171.253:8282", "serve": "webpack serve --mode=development", "build": "webpack --mode production", "analyze": "webpack --mode=production --env analyze=true", @@ -34,14 +34,14 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/mets-programme/esm-ugandaemr-template-app.git" + "url": "git+https://github.com/mets-programme/esm-alis-app.git" }, - "homepage": "https://github.com/mets-programme/esm-ugandaemr-template-app#readme", + "homepage": "https://github.com/mets-programme/esm-alis-app#readme", "publishConfig": { "access": "public" }, "bugs": { - "url": "https://github.com/mets-programme/esm-ugandaemr-template-app/issues" + "url": "https://github.com/mets-programme/esm-alis-app/issues" }, "dependencies": { "@carbon/react": "^1.33.1", @@ -79,6 +79,7 @@ "eslint-config-prettier": "^8.8.0", "eslint-config-ts-react-important-stuff": "^3.0.0", "eslint-plugin-prettier": "^4.2.1", + "file-saver": "^2.0.5", "husky": "^8.0.0", "i18next": "^23.2.8", "i18next-parser": "^8.0.0", diff --git a/src/alis-component/alis-header-illustration.component.tsx b/src/alis-component/alis-header-illustration.component.tsx new file mode 100644 index 0000000..0b2fe9e --- /dev/null +++ b/src/alis-component/alis-header-illustration.component.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +const Illustration: React.FC = () => { + return ( + + + + ); +}; + +export default Illustration; diff --git a/src/alis-component/alis-tests-ordered.component.tsx b/src/alis-component/alis-tests-ordered.component.tsx new file mode 100644 index 0000000..e881ac1 --- /dev/null +++ b/src/alis-component/alis-tests-ordered.component.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import Header from "../components/header/header.component"; +import Illustration from "./alis-header-illustration.component"; +import styles from "./alis.scss"; +import DataList from "../components/data-table/data-table.component"; +import { ALISData } from "../sample-data"; +import { ALISHeaders } from "../constants"; +const AlisTestsOrderedComponent: React.FC = () => { + return ( + <> +
} /> + +
+

Test Requests

+ +
+ + ); +}; + +export default AlisTestsOrderedComponent; diff --git a/src/alis-component/alis.scss b/src/alis-component/alis.scss new file mode 100644 index 0000000..0073ec6 --- /dev/null +++ b/src/alis-component/alis.scss @@ -0,0 +1,12 @@ +@use 'node_modules/@carbon/styles/scss/colors'; +@use 'node_modules/@carbon/styles/scss/spacing'; +@use 'node_modules/@carbon/styles/scss/type/index'; +@use '@carbon/styles/scss/type'; + +.container { + padding: 1.5rem 1.5rem 0; +} + +.listHeading { + @include type.type-style('heading-04'); +} diff --git a/src/components/data-table/data-table.component.tsx b/src/components/data-table/data-table.component.tsx new file mode 100644 index 0000000..d665504 --- /dev/null +++ b/src/components/data-table/data-table.component.tsx @@ -0,0 +1,212 @@ +import { + DataTable, + Pagination, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, + TableToolbar, + TableToolbarAction, + TableToolbarContent, + TableToolbarMenu, + TableToolbarSearch, + Tile, +} from "@carbon/react"; +import { + isDesktop, + useLayoutType, + usePagination, +} from "@openmrs/esm-framework"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import styles from "./data-tables.scss"; +import { saveAs } from "file-saver"; + +type FilterProps = { + rowIds: Array; + headers: any; + cellsById: any; + inputValue: string; + getCellId: (row, key) => string; +}; + +interface ListProps { + columns: any; + data: any; +} + +type DocumentType = "csv" | "pdf" | "json"; + +const DataList: React.FC = ({ columns, data }) => { + const { t } = useTranslation(); + const layout = useLayoutType(); + const isTablet = useLayoutType() === "tablet"; + const responsiveSize = isTablet ? "lg" : "sm"; + const [allRows, setAllRows] = useState([]); + const [list] = useState(data); + const [documentType, setDocumentType] = useState(null); + const pageSizes = [10, 20, 30, 40, 50]; + const [currentPageSize, setPageSize] = useState(10); + const { + goTo, + results: paginatedList, + currentPage, + } = usePagination(data, currentPageSize); + + const handleFilter = ({ + rowIds, + headers, + cellsById, + inputValue, + getCellId, + }: FilterProps): Array => { + return rowIds.filter((rowId) => + headers.some(({ key }) => { + const cellId = getCellId(rowId, key); + const filterableValue = cellsById[cellId].value; + const filterTerm = inputValue.toLowerCase(); + + if (typeof filterableValue === "boolean") { + return false; + } + + return ("" + filterableValue).toLowerCase().includes(filterTerm); + }) + ); + }; + + useEffect(() => { + let rows: Array> = []; + + paginatedList.map((item: any, index) => { + return rows.push({ ...item, id: index++ }); + }); + setAllRows(rows); + }, [paginatedList, allRows]); + + useEffect(() => { + const csvString = convertToCSV(list, columns); + if (documentType === "csv") { + const blob = new Blob([csvString], { type: "text/csv;charset=utf-8" }); + saveAs(blob, "data.csv"); + } else if (documentType === "json") { + const jsonBlob = new Blob([csvString], { type: "application/json" }); + saveAs(jsonBlob, "data.json"); + } + }, [list, columns, documentType]); + + const convertToCSV = (data, columns) => { + const header = columns.map((col) => col.header).join(","); + const rows = data.map((row) => + columns.map((col) => JSON.stringify(row[col.key])).join(",") + ); + return [header, ...rows].join("\n"); + }; + + return ( + + {({ rows, headers, getHeaderProps, getTableProps, onInputChange }) => ( +
+ +
+ + + + + + setDocumentType("csv")} + > + Download as CSV + + setDocumentType("pdf")} + > + Download as PDF + + setDocumentType("json")} + > + Download as JSON + + + + +
+ + + + {headers.map((header) => ( + + {header.header} + + ))} + + + + {rows.map((row) => ( + + {row.cells.map((cell) => ( + {cell.value} + ))} + + ))} + +
+ {rows.length === 0 ? ( +
+ +
+

+ {t("No data", "No data to display")} +

+
+
+
+ ) : null} + { + if (pageSize !== currentPageSize) { + setPageSize(pageSize); + } + if (page !== currentPage) { + goTo(page); + } + }} + /> +
+
+ )} +
+ ); +}; + +export default DataList; diff --git a/src/components/data-table/data-tables.scss b/src/components/data-table/data-tables.scss new file mode 100644 index 0000000..77baa8e --- /dev/null +++ b/src/components/data-table/data-tables.scss @@ -0,0 +1,215 @@ +@use '@carbon/styles/scss/colors'; +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; + +.widgetContainer { + background-color: colors.$white-0; +} + +.widgetHeaderContainer { + display: flex; + justify-content: space-between; + align-items: center; + padding: spacing.$spacing-04 0 spacing.$spacing-04 spacing.$spacing-05; + + h4 { + &::after { + content: ''; + display: block; + width: 2rem; + padding-top: 0.188rem; + border-bottom: 0.375rem solid var(--brand-01); + } + } +} + +.toggleButtons { + width: fit-content; + margin: 0 spacing.$spacing-03; +} + +.searchField { + width: 100%; + max-width: 250px; + border: 0 !important; +} + +.container { + margin-top: 16px; + margin-left: inherit; + width: 100%; + overflow-y: hidden; +} + +.tabContainer { + margin-top: 16px; + padding-left: 1rem; + background-color: colors.$white-0; +} + +.searchbox { + max-width: 16rem; + + input:focus { + outline: 2px solid colors.$orange-40 !important; + } +} + +.tabContainer li button { + width: 100% !important; +} + +.tileContainer { + background-color: colors.$white-0; + border-top: 1px solid colors.$gray-20; + padding: 5rem 0; +} + +.tile { + margin: auto; + width: fit-content; +} + +.tileContent { + display: flex; + flex-direction: column; + align-items: center; +} + +.menuItem { + max-width: none; +} + +.radioButtonGroup { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-top: spacing.$spacing-03; + min-height: spacing.$spacing-10; + width: 100%; + @include type.type-style('body-compact-01'); +} + +.radioButton { + padding: spacing.$spacing-02 spacing.$spacing-02; + margin: spacing.$spacing-03 0; +} + +.sectionTitle { + @include type.type-style('heading-compact-02'); + color: colors.$gray-70; + margin-bottom: spacing.$spacing-04; +} + +.modalBody { + padding-bottom: spacing.$spacing-05; +} + +.tabContainer div[role='tabpanel'] { + padding: 0 !important; +} + +.tabContainer li button { + width: 100% !important; +} + +.tableContainer { + margin: 0; + padding: 0; +} + +.search { + max-width: 16rem; + + input { + background-color: colors.$white-0 !important; + } +} + +.toolbarWrapper { + position: relative; + display: flex; + justify-content: flex-end; +} + +:global(.omrs-breakpoint-lt-desktop) { + .toolbarWrapper { + height: spacing.$spacing-09; + } +} + +:global(.omrs-breakpoint-gt-tablet) { + .toolbarWrapper { + height: spacing.$spacing-07; + } +} + + +.tableContainer { + padding: 0; + + :global(.cds--data-table-content) { + border: 1px solid colors.$gray-20; + border-bottom: none; + overflow: visible; + } + + :global(.cds--table-toolbar) { + position: relative; + height: 2rem; + min-height: 0; + overflow: visible; + top: 0; + } + + &:global(.cds--data-table-container) { + padding-top: 0; + } + + a { + text-decoration: none; + } + + tr { + width: spacing.$spacing-01; + } + + .cds--table-sort__description { + display: none; + } +} + +.toolbarAction { + max-width: none; +} + +.patientListDownload { + margin-top: 10px; +} + +.redCell { + background-color: colors.$red-40 !important; +} + +.grayCell { + background-color: colors.$gray-30 !important; +} + +.greenCell { + background-color: colors.$green-30 !important; +} + +.cellStyling { + border-right: 1px solid colors.$gray-20; + border-left: 1px solid colors.$gray-20; +} + +.tableHeaderStyle { + font-weight: 600; + font-size: 12px; + line-height: 1px; +} + +.tableHeaderFlag { + writing-mode: sideways-lr; +} diff --git a/src/components/empty-state/empty-state-illustration.component.tsx b/src/components/empty-state/empty-state-illustration.component.tsx new file mode 100644 index 0000000..ab1f5e1 --- /dev/null +++ b/src/components/empty-state/empty-state-illustration.component.tsx @@ -0,0 +1,53 @@ +import React from "react"; + +const EmptyStateIllustration = () => { + return ( + + Empty data illustration + + + + + + + + + + + + + + ); +}; + +export default EmptyStateIllustration; diff --git a/src/components/header/header.component.tsx b/src/components/header/header.component.tsx new file mode 100644 index 0000000..6e5058a --- /dev/null +++ b/src/components/header/header.component.tsx @@ -0,0 +1,44 @@ +import React, { JSX } from "react"; +import { useTranslation } from "react-i18next"; +import { Calendar, Location } from "@carbon/react/icons"; +import { formatDate, useSession } from "@openmrs/esm-framework"; +import styles from "./header.scss"; + +const Header: React.FC<{ + title?: string; + illustrationComponent: JSX.Element; +}> = ({ title, illustrationComponent }) => { + const { t } = useTranslation(); + const userSession = useSession(); + const userLocation = userSession?.sessionLocation?.display; + + return ( + <> +
+
+ {illustrationComponent} +
+

{t("home", "Home")}

+

+ {title ?? + t("alis", "Africa Laboratory Information System - ALIS")} +

+
+
+
+
+ + {userLocation} + · + + + {formatDate(new Date(), { mode: "standard" })} + +
+
+
+ + ); +}; + +export default Header; diff --git a/src/components/header/header.scss b/src/components/header/header.scss new file mode 100644 index 0000000..8c6da5b --- /dev/null +++ b/src/components/header/header.scss @@ -0,0 +1,71 @@ +@use '@carbon/styles/scss/colors'; +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; + +.header { + @include type.type-style('body-compact-02'); + color: colors.$gray-70; + height: spacing.$spacing-12; + background-color: colors.$white-0; + border-bottom: 1px solid colors.$gray-20; + display: flex; + justify-content: space-between; +} + +.left-justified-items { + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; +} + +.right-justified-items { + @include type.type-style('body-compact-02'); + color: colors.$gray-70; + margin: 0.5rem; +} + +.page-name { + @include type.type-style('heading-04'); +} + +.page-labels { + p:first-of-type { + margin-bottom: 0.25rem; + } +} + +.date-and-location { + display: flex; + justify-content: flex-end; + align-items: center; + margin-right: 1rem; +} + +.value { + margin-left: 0.25rem; +} + +.middot { + margin: 0 0.5rem; +} + +.view { + @include type.type-style('label-01'); +} + +.dropdown { + display: flex; + align-items: center; + justify-content: flex-end; + margin-top: 0.25rem; + + :global(.cds--dropdown__wrapper--inline) { + align-items: center; + display: flex; + } + + :global(.cds--list-box__menu) { + left: -10.15rem; + } +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..d1e4a36 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,47 @@ +export const ALISHeaders = [ + { + key: "date_ordered", + header: "Date Ordered", + accessor: "date_ordered", + }, + { + key: "patient_no", + header: "Patient No", + accessor: "patient_no", + }, + { + key: "lab_number", + header: "Lab Number", + accessor: "lab_number", + }, + { + key: "patient_name", + header: "Patient Name", + accessor: "patient_name", + }, + { + key: "test_id", + header: "Test ID", + accessor: "test_id", + }, + { + key: "test", + header: "Test", + accessor: "test", + }, + { + key: "visit_type", + header: "Visit Type", + accessor: "visit_type", + }, + { + key: "actions", + header: "Actions", + accessor: "actions", + }, + // { + // key: "test_status", + // header: "Test Status", + // accessor: "test_status", + // }, +]; diff --git a/src/create-dashboard-link.component.tsx b/src/create-dashboard-link.component.tsx new file mode 100644 index 0000000..c37438c --- /dev/null +++ b/src/create-dashboard-link.component.tsx @@ -0,0 +1,44 @@ +import React, { useMemo } from "react"; +import { ConfigurableLink } from "@openmrs/esm-framework"; +import { BrowserRouter, useLocation } from "react-router-dom"; + +export interface DashboardLinkConfig { + name: string; + title: string; + slot?: string; +} + +function DashboardExtension({ + dashboardLinkConfig, +}: { + dashboardLinkConfig: DashboardLinkConfig; +}) { + const { name, title } = dashboardLinkConfig; + const location = useLocation(); + const spaBasePath = `${window.spaBase}/home`; + + const navLink = useMemo(() => { + const pathArray = location.pathname.split("/home"); + const lastElement = pathArray[pathArray.length - 1]; + return decodeURIComponent(lastElement); + }, [location.pathname]); + + return ( + + {title} + + ); +} + +export const createDashboardLink = + (dashboardLinkConfig: DashboardLinkConfig) => () => + ( + + + + ); diff --git a/src/index.ts b/src/index.ts index a96b4da..b489055 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,12 @@ -import { getAsyncLifecycle, defineConfigSchema } from "@openmrs/esm-framework"; +import { + getAsyncLifecycle, + getSyncLifecycle, + defineConfigSchema, +} from "@openmrs/esm-framework"; import { configSchema } from "./config-schema"; +import { createDashboardLink } from "./create-dashboard-link.component"; -const moduleName = "@ugandaemr/esm-template-app"; +const moduleName = "@ugandaemr/esm-alis-app"; const options = { featureName: "root", @@ -23,3 +28,12 @@ export const root = getAsyncLifecycle( () => import("./root.component"), options ); + +export const alisDashboardLink = getSyncLifecycle( + createDashboardLink({ + name: "alis", + slot: "alis-dashboard-slot", + title: "ALIS", + }), + options +); diff --git a/src/root.component.tsx b/src/root.component.tsx index d4fde0f..9cfe812 100644 --- a/src/root.component.tsx +++ b/src/root.component.tsx @@ -1,37 +1,14 @@ import React from "react"; -import logo from "./assets/images/logo.svg"; -import styles from "./root.scss"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import AlisTestsOrderedComponent from "./alis-component/alis-tests-ordered.component"; const Root: React.FC = () => { return ( -
- Uganda EMR logo -

Welcome to the template app

-

- Use this template as a starter for set up custom UgandaEMR frontend - modules. -

-
-

Next steps

- - -
-
+ + + } /> + + ); }; diff --git a/src/routes.json b/src/routes.json index bb54ff6..70200b8 100644 --- a/src/routes.json +++ b/src/routes.json @@ -4,10 +4,21 @@ "fhir2": "^1.2.0", "webservices.rest": "^2.24.0" }, - "pages": [ + "extensions": [{ + "component": "root", + "name": "alis-dashboard", + "slot": "alis-dashboard-slot" + }, { - "component": "root", - "route": "root" + "name": "alis-dashboard-link", + "component": "alisDashboardLink", + "slot": "homepage-dashboard-slot", + "order": 5, + "meta": { + "name": "alis", + "slot": "alis-dashboard-slot", + "title": "ALIS" + } } ] } diff --git a/src/sample-data.tsx b/src/sample-data.tsx new file mode 100644 index 0000000..d1fa0f2 --- /dev/null +++ b/src/sample-data.tsx @@ -0,0 +1,94 @@ +import { Button, ButtonSet } from "@carbon/react"; +import { Edit, View, SendAlt } from "@carbon/react/icons"; +import React from "react"; + +const getActions = () => { + return ( + +