diff --git a/packages/esm-lab-manifest-app/README.md b/packages/esm-lab-manifest-app/README.md new file mode 100644 index 000000000..fb95de62c --- /dev/null +++ b/packages/esm-lab-manifest-app/README.md @@ -0,0 +1,5 @@ +![Node.js CI](https://github.com/palladiumkenya/kenyaemr-esm-3.x/workflows/Node.js%20CI/badge.svg) + +# ESM Lab manifest + +This is a frontend module that provides lab manifest functionality. diff --git a/packages/esm-lab-manifest-app/jest.config.js b/packages/esm-lab-manifest-app/jest.config.js new file mode 100644 index 000000000..e53fc9033 --- /dev/null +++ b/packages/esm-lab-manifest-app/jest.config.js @@ -0,0 +1,8 @@ +const rootConfig = require('../../jest.config.js'); + +const packageConfig = { + ...rootConfig, + collectCoverage: false, +}; + +module.exports = packageConfig; diff --git a/packages/esm-lab-manifest-app/package.json b/packages/esm-lab-manifest-app/package.json new file mode 100644 index 000000000..f4e13bfef --- /dev/null +++ b/packages/esm-lab-manifest-app/package.json @@ -0,0 +1,54 @@ +{ + "name": "@kenyaemr/esm-lab-manifest-app", + "version": "5.2.0", + "description": "lab-manifest app for KenyaEMR", + "browser": "dist/kenyaemr-esm-lab-manifest-app.js", + "main": "src/index.ts", + "source": true, + "license": "MPL-2.0", + "homepage": "https://github.com/palladiumkenya/kenyaemr-esm-core#readme", + "scripts": { + "start": "openmrs develop", + "serve": "webpack serve --mode=development", + "debug": "npm run serve", + "build": "webpack --mode production", + "analyze": "webpack --mode=production --env.analyze=true", + "lint": "eslint src --ext ts,tsx", + "typescript": "tsc", + "extract-translations": "i18next 'src/**/*.component.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js", + "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests", + "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js", + "coverage": "yarn test --coverage" + }, + "browserslist": [ + "extends browserslist-config-openmrs" + ], + "keywords": [ + "openmrs" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/palladiumkenya/kenyaemr-esm-core#readme" + }, + "bugs": { + "url": "https://github.com/palladiumkenya/kenyaemr-esm-core/issues" + }, + "dependencies": { + "@carbon/react": "^1.42.1", + "lodash-es": "^4.17.15", + "react-to-print": "^2.14.13" + }, + "peerDependencies": { + "@openmrs/esm-framework": "5.x", + "react": "^18.1.0", + "react-i18next": "11.x", + "react-router-dom": "6.x", + "swr": "2.x" + }, + "devDependencies": { + "webpack": "^5.74.0" + } +} diff --git a/packages/esm-lab-manifest-app/src/component/lab-manifest-detail.component.tsx b/packages/esm-lab-manifest-app/src/component/lab-manifest-detail.component.tsx new file mode 100644 index 000000000..5513a2526 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/component/lab-manifest-detail.component.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import LabManifestDetailHeader from '../header/lab-manifest-detail-header.component'; +import { LabManifestHeader } from '../header/lab-manifest-header.component'; +import { LabManifestTabs } from '../tabs/lab-manifest-tabs-component'; + +const LabManifestDetail = () => { + const { manifestUuid } = useParams(); + const { t } = useTranslation(); + + return ( +
+ + + +
+ ); +}; + +export default LabManifestDetail; diff --git a/packages/esm-lab-manifest-app/src/component/lab-manifest.component.tsx b/packages/esm-lab-manifest-app/src/component/lab-manifest.component.tsx new file mode 100644 index 000000000..2df4649c0 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/component/lab-manifest.component.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { LabManifestHeader } from '../header/lab-manifest-header.component'; +import LabManifestMetrics from '../metrics/lab-manifest-metrics.component'; +import LabManifestsTable from '../tables/lab-manifest-table.component'; + +const LabManifestComponent: React.FC = () => { + const { t } = useTranslation(); + return ( +
+ + + +
+ ); +}; + +export default LabManifestComponent; diff --git a/packages/esm-lab-manifest-app/src/component/left-panel-link.component.tsx b/packages/esm-lab-manifest-app/src/component/left-panel-link.component.tsx new file mode 100644 index 000000000..fdff65c52 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/component/left-panel-link.component.tsx @@ -0,0 +1,41 @@ +import React, { useMemo } from 'react'; +import last from 'lodash-es/last'; +import { BrowserRouter, useLocation } from 'react-router-dom'; +import { ConfigurableLink } from '@openmrs/esm-framework'; + +export interface LinkConfig { + name: string; + title: string; +} + +export function LinkExtension({ config }: { config: LinkConfig }) { + const { name, title } = config; + const location = useLocation(); + const spaBasePath = window.getOpenmrsSpaBase() + 'home'; + + let urlSegment = useMemo(() => decodeURIComponent(last(location.pathname.split('/'))!), [location.pathname]); + + const isUUID = (value) => { + const regex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/; + return regex.test(value); + }; + + if (isUUID(urlSegment)) { + urlSegment = 'lab-manifest'; + } + + return ( + + {title} + + ); +} + +export const createLeftPanelLink = (config: LinkConfig) => () => + ( + + + + ); diff --git a/packages/esm-lab-manifest-app/src/config-schema.ts b/packages/esm-lab-manifest-app/src/config-schema.ts new file mode 100644 index 000000000..7f10afa8b --- /dev/null +++ b/packages/esm-lab-manifest-app/src/config-schema.ts @@ -0,0 +1,5 @@ +import { Type, validator } from '@openmrs/esm-framework'; + +export const configSchema = {}; + +export type Config = {}; diff --git a/packages/esm-lab-manifest-app/src/counties.json b/packages/esm-lab-manifest-app/src/counties.json new file mode 100644 index 000000000..f94d29eb6 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/counties.json @@ -0,0 +1,1494 @@ +[ + { + "name": "Mombasa", + "number": "1", + "capital": "Mombasa City", + "constituencies": [ + { + "name":"Changamwe", + "code": "1" + }, + { + "name": "Jomvu", + "code": "2" + }, + { + "name": "Kisauni", + "code": "3" + }, + { + "name": "Nyali", + "code": "4" + }, + { + "name": "Likoni", + "code": "5" + }, + { + "name": "Mvita", + "code": "6" + } + ] + }, + { + "name": "Kwale", + "number": "2", + "capital": "Kwale", + "constituencies": [ + { + "name":"Msambweni", + "code": "7" + }, + { + "name": "Lunga Lunga", + "code": "8" + }, + { + "name": "Matuga", + "code": "9" + }, + { + "name": "Kinango", + "code": "10" + } + ] + }, + { + "name": "Kilifi", + "number": "3", + "capital": "Kilifi", + "constituencies": [ + { + "name":"Kilifi North", + "code": "11" + }, + { + "name": "Kilifi South", + "code": "12" + }, + { + "name": "Kaloleni", + "code": "13" + }, + { + "name": "Rabai", + "code": "14" + }, + { + "name": "Ganze", + "code": "15" + }, + { + "name": "Malindi", + "code": "16" + }, + { + "name": "Magarini", + "code": "17" + } + ] + }, + { + "name": "Tana River", + "number": "4", + "capital": "Hola", + "constituencies": [ + { + "name":"Garsen", + "code": "18" + }, + { + "name": "Galole", + "code": "19" + }, + { + "name": "Bura", + "code": "20" + } + ] + }, + { + "name": "Lamu", + "number": "5", + "capital": "Lamu", + "constituencies": [ + { + "name":"Lamu East", + "code": "21" + }, + { + "name": "Lamu West", + "code": "22" + } + ] + }, + { + "name": "Taita Taveta", + "number": "6", + "capital": "Mwatate", + "constituencies": [ + { + "name":"Taveta", + "code": "23" + }, + { + "name": "Wundanyi", + "code": "24" + }, + { + "name": "Mwatate", + "code": "25" + }, + { + "name": "Voi", + "code": "26" + } + ] + }, + { + "name": "Garissa", + "number": "7", + "capital": "Garissa", + "constituencies": [ + { + "name":"Garissa Township", + "code": "27" + }, + { + "name": "Balambala", + "code": "28" + }, + { + "name": "Lagdera", + "code": "29" + }, + { + "name": "Dadaab", + "code": "30" + }, + { + "name": "Fafi", + "code": "31" + }, + { + "name": "Ijara", + "code": "32" + } + ] + }, + { + "name": "Wajir", + "number": "8", + "capital": "Wajir", + "constituencies": [ + { + "name":"Wajir North", + "code": "33" + }, + { + "name": "Wajir East", + "code": "34" + }, + { + "name": "Tarbaj", + "code": "35" + }, + { + "name": "Wajir West", + "code": "36" + }, + { + "name": "Eldas", + "code": "37" + }, + { + "name": "Wajir South", + "code": "38" + } + ] + }, + { + "name": "Mandera", + "number": "9", + "capital": "Mandera", + "constituencies": [ + { + "name":"Mandera West", + "code": "39" + }, + { + "name": "Banissa", + "code": "40" + }, + { + "name": "Mandera North", + "code": "41" + }, + { + "name": "Mandera South", + "code": "42" + }, + { + "name": "Mandera East", + "code": "43" + }, + { + "name": "Lafey", + "code": "44" + } + ] + }, + { + "name": "Marsabit", + "number": "10", + "capital": "Marsabit", + "constituencies": [ + { + "name":"Moyale", + "code": "45" + }, + { + "name": "North Horr", + "code": "46" + }, + { + "name": "Saku", + "code": "47" + }, + { + "name": "Laisamis", + "code": "48" + } + ] + }, + { + "name": "Isiolo", + "number": "11", + "capital": "Isiolo", + "constituencies": [ + { + "name":"Isiolo North", + "code": "49" + }, + { + "name": "Isiolo South", + "code": "50" + } + ] + }, + { + "name": "Meru", + "number": "12", + "capital": "Meru", + "constituencies": [ + { + "name":"Igembe South", + "code": "51" + }, + { + "name": "Igembe Central", + "code": "52" + }, + { + "name": "Igembe North", + "code": "53" + }, + { + "name": "Tigania West", + "code": "54" + }, + { + "name": "Tigania East", + "code": "55" + }, + { + "name": "North Imenti", + "code": "56" + }, + { + "name": "Buuri", + "code": "57" + }, + { + "name": "Central Imenti", + "code": "58" + }, + { + "name": "South Imenti", + "code": "59" + } + ] + }, + { + "name": "Tharaka-Nithi", + "number": "13", + "capital": "Chuka", + "constituencies": [ + { + "name":"Maara", + "code": "60" + }, + { + "name": "Chuka", + "code": "61" + }, + { + "name": "Tharaka", + "code": "62" + } + ] + }, + { + "name": "Embu", + "number": "14", + "capital": "Embu", + "constituencies": [ + { + "name":"Manyatta", + "code": "63" + }, + { + "name": "Runyenjes", + "code": "64" + }, + { + "name": "Mbeere South", + "code": "65" + }, + { + "name": "Mbeere North", + "code": "66" + } + ] + }, + { + "name": "Kitui", + "number": "15", + "capital": "Kitui", + "constituencies": [ + { + "name":"Mwingi North", + "code": "67" + }, + { + "name": "Mwingi West", + "code": "68" + }, + { + "name": "Mwingi Central", + "code": "69" + }, + { + "name": "Kitui West", + "code": "70" + }, + { + "name": "Kitui Rural", + "code": "71" + }, + { + "name": "Kitui Central", + "code": "72" + }, + { + "name": "Kitui East", + "code": "73" + }, + { + "name": "Kitui South", + "code": "74" + } + ] + }, + { + "name": "Machakos", + "number": "16", + "capital": "Machakos", + "constituencies": [ + { + "name":"Masinga", + "code": "75" + }, + { + "name": "Yatta", + "code": "76" + }, + { + "name": "Kangundo", + "code": "77" + }, + { + "name": "Matungulu", + "code": "78" + }, + { + "name": "Kathiani", + "code": "79" + }, + { + "name": "Mavoko", + "code": "80" + }, + { + "name": "Machakos Town", + "code": "81" + }, + { + "name": "Mwala", + "code": "82" + } + ] + }, + { + "name": "Makueni", + "number": "17", + "capital": "Wote", + "constituencies": [ + { + "name":"Mbooni", + "code": "83" + }, + { + "name": "Kilome", + "code": "84" + }, + { + "name": "Kaiti", + "code": "85" + }, + { + "name": "Makueni", + "code": "86" + }, + { + "name": "Kibwezi West", + "code": "87" + }, + { + "name": "Kibwezi East", + "code": "88" + } + ] + }, + { + "name": "Nyandarua", + "number": "18", + "capital": "Ol kalou", + "constituencies": [ + { + "name":"Kinangop", + "code": "89" + }, + { + "name": "Kipipiri", + "code": "90" + }, + { + "name": "Ol kalou", + "code": "91" + }, + { + "name": "Ol Jorok", + "code": "92" + }, + { + "name": "Ndaragwa", + "code": "93" + } + ] + }, + { + "name": "Nyeri", + "number": "19", + "capital": "Nyeri", + "constituencies": [ + { + "name":"Tetu", + "code": "94" + }, + { + "name": "Kieni", + "code": "95" + }, + { + "name": "Mathira", + "code": "96" + }, + { + "name": "Othaya", + "code": "97" + }, + { + "name": "Mukurweini", + "code": "98" + }, + { + "name": "Nyeri Town", + "code": "99" + } + ] + }, + { + "name": "Kirinyaga", + "number": "20", + "capital": "Kutus", + "constituencies": [ + { + "name":"Mwea", + "code": "100" + }, + { + "name": "Gichugu", + "code": "101" + }, + { + "name": "Ndia", + "code": "102" + }, + { + "name": "Kirinyaga Central", + "code": "103" + } + ] + }, + { + "name": "Murang'a", + "number": "21", + "capital": "Murang'a", + "constituencies": [ + { + "name":"Kangema", + "code": "104" + }, + { + "name": "Mathioya", + "code": "105" + }, + { + "name": "Kiharu", + "code": "106" + }, + { + "name": "Kigumo", + "code": "107" + }, + { + "name": "Maragwa", + "code": "108" + }, + { + "name": "Kandara", + "code": "109" + }, + { + "name": "Gatanga", + "code": "110" + } + ] + }, + { + "name": "Kiambu", + "number": "22", + "capital": "Kiambu", + "constituencies": [ + { + "name":"Gatundu South", + "code": "111" + }, + { + "name": "Gatundu North", + "code": "112" + }, + { + "name": "Juja", + "code": "113" + }, + { + "name": "Thika Town", + "code": "114" + }, + { + "name": "Ruiru", + "code": "115" + }, + { + "name": "Githunguri", + "code": "116" + }, + { + "name": "Kiambu", + "code": "117" + }, + { + "name": "Kiambaa", + "code": "118" + }, + { + "name": "Kabete", + "code": "119" + }, + { + "name": "Kikuyu", + "code": "120" + }, + { + "name": "Limuru", + "code": "121" + }, + { + "name": "Lari", + "code": "122" + } + ] + }, + { + "name": "Turkana", + "number": "23", + "capital": "Lodwar", + "constituencies": [ + { + "name":"Turkana North", + "code": "123" + }, + { + "name": "Turkana West", + "code": "124" + }, + { + "name": "Turkana Central", + "code": "125" + }, + { + "name": "Loima", + "code": "126" + }, + { + "name": "Turkana South", + "code": "127" + }, + { + "name": "Turkana East", + "code": "128" + } + ] + }, + { + "name": "West Pokot", + "number": "24", + "capital": "Kapenguria", + "constituencies": [ + { + "name":"Kapenguria", + "code": "129" + }, + { + "name": "Sigor", + "code": "130" + }, + { + "name": "Kacheliba", + "code": "131" + }, + { + "name": "Pokot South", + "code": "132" + } + ] + }, + + { + "name": "Samburu", + "number": "25", + "capital": "Maralal", + "constituencies": [ + { + "name":"Samburu West", + "code": "133" + }, + { + "name": "Samburu North", + "code": "134" + }, + { + "name": "Samburu East", + "code": "135" + } + ] + }, + { + "name": "Trans-Nzoia", + "number": "26", + "capital": "Kitale", + "constituencies": [ + { + "name":"Kwanza", + "code": "136" + }, + { + "name": "Endebess", + "code": "137" + }, + { + "name": "Saboti", + "code": "138" + }, + { + "name": "Kiminini", + "code": "139" + }, + { + "name": "Cherangany", + "code": "140" + } + ] + }, + { + "name": "Uasin Gishu", + "number": "27", + "capital": "Eldoret", + "constituencies": [ + { + "name":"Soy", + "code": "141" + }, + { + "name": "Turbo", + "code": "142" + }, + { + "name": "Moiben", + "code": "143" + }, + { + "name": "Ainabkoi", + "code": "144" + }, + { + "name": "Kapseret", + "code": "145" + }, + { + "name": "Kesses", + "code": "146" + } + ] + }, + { + "name": "Elgeyo-Marakwet", + "number": "28", + "capital": "Iten", + "constituencies": [ + { + "name":"Marakwet East", + "code": "147" + }, + { + "name": "Marakwet West", + "code": "148" + }, + { + "name": "Keiyo North", + "code": "149" + }, + { + "name": "Keiyo South", + "code": "150" + } + ] + }, + { + "name": "Nandi", + "number": "29", + "capital": "Kapsabet", + "constituencies": [ + { + "name":"Tinderet", + "code": "151" + }, + { + "name": "Aldai", + "code": "152" + }, + { + "name": "Nandi Hiils", + "code": "153" + }, + { + "name": "Chesumei", + "code": "154" + }, + { + "name": "Emgwen", + "code": "155" + }, + { + "name": "Mosop", + "code": "156" + } + ] + }, + { + "name": "Baringo", + "number": "30", + "capital": "Kabarnet", + "constituencies": [ + { + "name":"Tiaty", + "code": "157" + }, + { + "name": "Baringo North", + "code": "158" + }, + { + "name": "Baringo Central", + "code": "159" + }, + { + "name": "Baringo South", + "code": "160" + }, + { + "name": "Mogotio", + "code": "161" + }, + { + "name": "Eldama Ravine", + "code": "162" + } + ] + }, + { + "name": "Laikipia", + "number": "31", + "capital": "Rumuruti", + "constituencies": [ + { + "name":"Laikipia West", + "code": "163" + }, + { + "name": "Laikipia East", + "code": "164" + }, + { + "name": "Laikipia North", + "code": "165" + } + ] + }, + { + "name": "Nakuru", + "number": "32", + "capital": "Nakuru", + "constituencies": [ + { + "name":"Molo", + "code": "166" + }, + { + "name": "Njoro", + "code": "167" + }, + { + "name": "Naivasha", + "code": "168" + }, + { + "name": "Gilgil", + "code": "169" + }, + { + "name": "Kuresoi South", + "code": "170" + }, + { + "name": "Kuresoi North", + "code": "171" + }, + { + "name": "Subukia", + "code": "172" + }, + { + "name": "Rongai", + "code": "173" + }, + { + "name": "Bahati", + "code": "174" + }, + { + "name": "Nakuru Town West", + "code": "175" + }, + { + + "name": "Nakuru Town East", + "code": "176" + } + ] + }, + { + "name": "Narok", + "number": "33", + "capital": "Narok", + "constituencies": [ + { + "name":"Kilgoris", + "code": "177" + }, + { + "name": "Emurua Dikirr", + "code": "178" + }, + { + "name": "Narok North", + "code": "179" + }, + { + "name": "Narok East", + "code": "180" + }, + { + "name": "Narok South", + "code": "181" + }, + { + "name": "Narok West", + "code": "182" + } + ] + }, + { + "name": "Kajiado", + "number": "34", + "capital": "Kajiado", + "constituencies": [ + { + "name":"Kajiado North", + "code": "183" + }, + { + "name": "Kajiado Central", + "code": "184" + }, + { + "name": "Kajiado East", + "code": "185" + }, + { + "name": "Kajiado West", + "code": "186" + }, + { + "name": "Kajiado South", + "code": "187" + } + ] + }, + { + "name": "Kericho", + "number": "35", + "capital": "Kericho", + "constituencies": [ + { + "name":"Kipkelion East", + "code": "188" + }, + { + "name": "Kipkelion West", + "code": "189" + }, + { + "name": "Ainamoi", + "code": "190" + }, + { + "name": "Bureti", + "code": "191" + }, + { + "name": "Belgut", + "code": "192" + }, + { + "name": "Sigowet-Soin", + "code": "193" + } + ] + }, + { + "name": "Bomet", + "number": "36", + "capital": "Bomet", + "constituencies": [ + { + "name":"Sotik", + "code": "194" + }, + { + "name": "Chepalungu", + "code": "195" + }, + { + "name": "Bomet East", + "code": "196" + }, + { + "name": "Bomet Central", + "code": "197" + }, + { + "name": "Konoin", + "code": "198" + } + ] + }, + { + "name": "Kakamega", + "number": "37", + "capital": "Kakamega", + "constituencies": [ + { + "name":"Lugari", + "code": "199" + }, + { + "name": "Likuyani", + "code": "200" + }, + { + "name": "Malava", + "code": "201" + }, + { + "name": "Lurambi", + "code": "202" + }, + { + "name": "Navakholo", + "code": "203" + }, + { + "name": "Mumias West", + "code": "204" + }, + { + "name": "Mumias East", + "code": "205" + }, + { + "name": "Matungu", + "code": "206" + }, + { + "name": "Butere", + "code": "207" + }, + { + "name": "Khwisero", + "code": "208" + }, + { + "name": "Shinyalu", + "code": "209" + }, + { + "name": "Ikolomani", + "code": "210" + } + ] + }, + { + "name": "Vihiga", + "number": "38", + "capital": "Mbale", + "constituencies": [ + { + "name":"Vihiga", + "code": "211" + }, + { + "name": "Sabatia", + "code": "212" + }, + { + "name": "Hamisi", + "code": "213" + }, + { + "name": "Luanda", + "code": "214" + }, + { + "name": "Emuhaya", + "code": "215" + } + ] + }, + { + "name": "Bungoma", + "number": "39", + "capital": "Bungoma", + "constituencies": [ + { + "name":"Mount Elgon", + "code": "216" + }, + { + "name": "Sirisia", + "code": "217" + }, + { + "name": "Kabuchai", + "code": "218" + }, + { + "name": "Bumula", + "code": "219" + }, + { + "name": "Kanduyi", + "code": "220" + }, + { + "name": "Webuye East", + "code": "221" + }, + { + "name": "Webuye West", + "code": "222" + }, + { + "name": "Kimilili", + "code": "223" + }, + { + "name": "Tongaren", + "code": "224" + } + ] + }, + { + "name": "Busia", + "number": "40", + "capital": "Busia", + "constituencies": [ + { + "name":"Teso North", + "code": "225" + }, + { + "name": "Teso South", + "code": "226" + }, + { + "name": "Nambale", + "code": "227" + }, + { + "name": "Matayos", + "code": "228" + }, + { + "name": "Butula", + "code": "229" + }, + { + "name": "Funyula", + "code": "230" + }, + { + "name": "Budalangi", + "code": "231" + } + ] + }, + { + "name": "Siaya", + "number": "41", + "capital": "Siaya", + "constituencies": [ + { + "name":"Ugenya", + "code": "232" + }, + { + "name": "Ugunja", + "code": "233" + }, + { + "name": "Alego Usonga", + "code": "234" + }, + { + "name": "Gem", + "code": "235" + }, + { + "name": "Bondo", + "code": "236" + }, + { + "name": "Rarieda", + "code": "237" + } + ] + }, + { + "name": "Kisumu", + "number": "42", + "capital": "Kisumu", + "constituencies": [ + { + "name":"Kisumu East", + "code": "238" + }, + { + "name": "Kisumu West", + "code": "239" + }, + { + "name": "Kisumu Central", + "code": "240" + }, + { + "name": "Seme", + "code": "241" + }, + { + "name": "Nyando", + "code": "242" + }, + { + "name": "Muhoroni", + "code": "243" + }, + { + "name": "Nyakach", + "code": "244" + } + ] + }, + { + "name": "Homa Bay", + "number": "43", + "capital": "Homa Bay", + "constituencies": [ + { + "name":"Kasipul", + "code": "245" + }, + { + "name": "Kabondo Kasipul", + "code": "246" + }, + { + "name": "Karachuonyo", + "code": "247" + }, + { + "name": "Rangwe", + "code": "248" + }, + { + "name": "Homa Bay Town", + "code": "249" + }, + { + "name": "Ndhiwa", + "code": "250" + }, + { + "name": "Mbita", + "code": "251" + }, + { + "name": "Suba", + "code": "252" + } + ] + }, + { + "name": "Migori", + "number": "44", + "capital": "Migori", + "constituencies": [ + { + "name":"Rongo", + "code": "253" + }, + { + "name": "Awendo", + "code": "254" + }, + { + "name": "Suna East", + "code": "255" + }, + { + "name": "Suna West", + "code": "256" + }, + { + "name": "Uriri", + "code": "257" + }, + { + "name": "Nyatike", + "code": "258" + }, + { + "name": "Kuria West", + "code": "259" + }, + { + "name": "Kuria East", + "code": "260" + } + ] + }, + { + "name": "Kisii", + "number": "45", + "capital": "Kisii", + "constituencies": [ + { + "name":"Bonchari", + "code": "261" + }, + { + "name": "South Mugirango", + "code": "262" + }, + { + "name": "Bomachoge Borabu", + "code": "263" + }, + { + "name": "Bobasi", + "code": "264" + }, + { + "name": "Bomachoge Chache", + "code": "265" + }, + { + "name": "Nyaribari Masaba", + "code": "266" + }, + { + "name": "Nyaibari Chache", + "code": "267" + }, + { + "name": "Kitutu Chache North", + "code": "268" + }, + { + "name": "Kitutu Chache South", + "code": "269" + } + ] + }, + { + "name": "Nyamira", + "number": "46", + "capital": "Nyamira", + "constituencies": [ + { + "name":"Kitutu Masaba", + "code": "270" + }, + { + "name": "West Mugirango", + "code": "271" + }, + { + "name": "North Mugirango", + "code": "272" + }, + { + "name": "Borabu", + "code": "273" + } + ] + }, + { + "name": "Nairobi", + "number": "47", + "capital": "Nairobi City", + "constituencies": [ + { + "name":"Westlands", + "code": "274" + }, + { + "name": "Dagoretti North", + "code": "275" + }, + { + "name": "Dagoretti South", + "code": "276" + }, + { + "name": "Lang'ata", + "code": "277" + }, + { + "name": "Kibra", + "code": "278" + }, + { + "name": "Roysambu", + "code": "279" + }, + { + "name": "Kasarani", + "code": "280" + }, + { + "name": "Ruaraka", + "code": "281" + }, + { + "name": "Embakasi South", + "code": "282" + }, + { + "name": "Embakasi North", + "code": "283" + }, + { + "name": "Embakasi Central", + "code": "284" + }, + { + "name": "Embakasi East", + "code": "285" + }, + { + "name": " Embakasi West", + "code": "286" + }, + { + "name": "Makadara", + "code": "287" + }, + { + "name": " Kamukunji", + "code": "288" + }, + { + "name": "Starehe", + "code": "289" + }, + { + "name": "Mathare", + "code": "290" + } + ] + } + +] diff --git a/packages/esm-lab-manifest-app/src/declarations.d.ts b/packages/esm-lab-manifest-app/src/declarations.d.ts new file mode 100644 index 000000000..dda6181b4 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/declarations.d.ts @@ -0,0 +1,6 @@ +declare module '@carbon/react'; +declare module '*.css'; +declare module '*.scss'; +declare module '*.png'; + +declare type SideNavProps = object; diff --git a/packages/esm-lab-manifest-app/src/forms/lab-manifest-form.scss b/packages/esm-lab-manifest-app/src/forms/lab-manifest-form.scss new file mode 100644 index 000000000..73e4dfe92 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/forms/lab-manifest-form.scss @@ -0,0 +1,126 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@use '@carbon/layout'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.heading { + @include type.type-style('heading-compact-01'); + margin: spacing.$spacing-05 0 spacing.$spacing-05; +} + +.warningContainer { + background-color: $carbon--red-50; + padding: spacing.$spacing-04; + margin: spacing.$spacing-03 0 spacing.$spacing-03; + display: flex; + justify-content: space-between; + .warning { + @include type.type-style('heading-compact-01'); + color: $ui-05; + } +} + +.form { + display: flex; + flex-direction: column; + justify-content: space-between; + width: 50%; +} + +.grid { + margin: 0 spacing.$spacing-05; + padding: 0rem; +} + +.input { + margin-top: spacing.$spacing-05; +} + +.inputRow { + margin-top: spacing.$spacing-05; + width: 50%; // Adjust width as per your design requirements +} + +.datePickersRow { + display: flex; + flex-wrap: nowrap; + gap: spacing.$spacing-05; // Adjust gap between columns + align-items: center; +} + +.datePickerInput { + width: 100%; +} + +.button { + height: spacing.$spacing-10; + display: flex; + align-content: flex-start; + align-items: baseline; + min-width: 20%; +} + +.buttonSet { + padding: 0rem; + margin-top: spacing.$spacing-05; + display: flex; + justify-content: flex-end; + gap: spacing.$spacing-05; + margin-bottom: spacing.$spacing-05; +} +.inlineActions { + display: flex; + gap: spacing.$spacing-05; /* Adjust the spacing as needed */ +} + +.contactFormTitle { + @include type.type-style('heading-02'); + display: flex; + align-items: center; + justify-content: space-between; + margin: spacing.$spacing-05; + row-gap: 1.5rem; + position: relative; + + &::after { + content: ''; + display: block; + width: 2rem; + border-bottom: 0.375rem solid var(--brand-03); + position: absolute; + bottom: -0.75rem; + left: 0; + } + + & > span { + @include type.type-style('body-01'); + } +} + +.sectionHeader { + @include type.type-style('heading-02'); +} + +:global(.omrs-breakpoint-lt-desktop) { + .form { + height: var(--tablet-workspace-window-height); + } + + .buttonSet { + padding: spacing.$spacing-06 spacing.$spacing-05; + background-color: $ui-02; + justify-content: flex-end; + gap: spacing.$spacing-05; + } +} + +/* New Styles for Facility and Visit Type */ +.facilityVisitRow { + display: flex; + flex-wrap: nowrap; + gap: spacing.$spacing-05; /* Adjust gap between columns */ +} + +.facilityColumn { + flex: 1 1 0%; /* Makes columns take up equal space */ +} diff --git a/packages/esm-lab-manifest-app/src/forms/lab-manifest-form.workspace.tsx b/packages/esm-lab-manifest-app/src/forms/lab-manifest-form.workspace.tsx new file mode 100644 index 000000000..01543883f --- /dev/null +++ b/packages/esm-lab-manifest-app/src/forms/lab-manifest-form.workspace.tsx @@ -0,0 +1,357 @@ +import { + Button, + ButtonSet, + Column, + DatePicker, + DatePickerInput, + Dropdown, + Form, + Stack, + TextInput, +} from '@carbon/react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { DefaultWorkspaceProps, parseDate, showSnackbar, useLayoutType } from '@openmrs/esm-framework'; +import React, { useEffect } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { LabManifestFilters, labManifestFormSchema, manifestTypes, saveLabManifest } from '../lab-manifest.resources'; +import styles from './lab-manifest-form.scss'; +import { County, MappedLabManifest } from '../types'; +import { mutate } from 'swr'; +interface LabManifestFormProps extends DefaultWorkspaceProps { + patientUuid: string; + manifest?: MappedLabManifest; +} + +type ContactListFormType = z.infer; + +const LabManifestForm: React.FC = ({ closeWorkspace, manifest }) => { + const counties = require('../counties.json') as County[]; + const form = useForm({ + defaultValues: { + ...manifest, + dispatchDate: manifest?.dispatchDate ? parseDate(manifest.dispatchDate) : undefined, + startDate: manifest?.startDate ? parseDate(manifest.startDate) : undefined, + endDate: manifest?.endDate ? parseDate(manifest.endDate) : undefined, + }, + resolver: zodResolver(labManifestFormSchema), + }); + const { t } = useTranslation(); + const observableSelectedCounty = form.watch('county'); + const layout = useLayoutType(); + const controlSize = layout === 'tablet' ? 'xl' : 'sm'; + const onSubmit = async (values: ContactListFormType) => { + try { + await saveLabManifest(values, manifest?.uuid); + if (manifest?.uuid) { + mutate((key) => { + return ( + typeof key === 'string' && + key.startsWith(`/ws/rest/v1/labmanifest/${manifest!.uuid}?status=${values.manifestStatus}`) + ); + }); + } else { + mutate((key) => { + return typeof key === 'string' && key.startsWith(`/ws/rest/v1/labmanifest?status=${values.manifestStatus}`); + }); + } + closeWorkspace(); + showSnackbar({ title: 'Success', kind: 'success', subtitle: 'Lab manifest created successfully!' }); + } catch (error) { + showSnackbar({ title: 'Failure', kind: 'error', subtitle: 'Error creating lab manifest' }); + } + }; + return ( +
+ {t('formTitle', 'Fill in the form details')} + + + ( + + + + )} + /> + + + ( + + + + )} + /> + + Manifest type + + ( + { + field.onChange(e.selectedItem); + }} + initialSelectedItem={field.value} + label="Choose option" + items={manifestTypes.map((r) => r.value)} + itemToString={(item) => manifestTypes.find((r) => r.value === item)?.label ?? ''} + /> + )} + /> + + Dispatch status + + + ( + + + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + Address + + ( + { + field.onChange(e.selectedItem); + form.setValue('subCounty', undefined); + }} + initialSelectedItem={field.value} + label="Select county" + items={counties.map((r) => r.name)} + itemToString={(item) => item ?? ''} + /> + )} + /> + + + ( + { + field.onChange(e.selectedItem); + }} + label="Select subcounty" + items={(counties.find((c) => c.name == observableSelectedCounty)?.constituencies ?? []).map( + (r) => r.name, + )} + itemToString={(item) => + (counties.find((c) => c.name == observableSelectedCounty)?.constituencies ?? []).find( + (c) => c.name === item, + )?.name ?? 'Select subcounty' + } + /> + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + Manifest status + + ( + { + field.onChange(e.selectedItem); + }} + initialSelectedItem={field.value} + label="Select status" + items={LabManifestFilters.map((r) => r.value)} + itemToString={(item) => LabManifestFilters.find((r) => r.value === item)?.label ?? ''} + /> + )} + /> + + + + + + + +
+ ); +}; + +export default LabManifestForm; diff --git a/packages/esm-lab-manifest-app/src/header/lab-manifest-detail-header.component.tsx b/packages/esm-lab-manifest-app/src/header/lab-manifest-detail-header.component.tsx new file mode 100644 index 000000000..a6f826801 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/header/lab-manifest-detail-header.component.tsx @@ -0,0 +1,74 @@ +import { Button, ButtonSet, SkeletonText } from '@carbon/react'; +import { ArrowLeft, Edit } from '@carbon/react/icons'; +import { formatDate, launchWorkspace, navigate, parseDate } from '@openmrs/esm-framework'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLabManifest } from '../hooks'; +import styles from './lab-manifest-header.scss'; + +interface LabManifestDetailHeaderProps { + manifestUuid: string; +} + +const LabManifestDetailHeader: React.FC = ({ manifestUuid }) => { + const { isLoading, manifest } = useLabManifest(manifestUuid); + const { t } = useTranslation(); + + const handleGoBack = () => { + navigate({ to: window.getOpenmrsSpaBase() + `home/lab-manifest` }); + }; + + const handleEditManifest = () => { + launchWorkspace('lab-manifest-form', { + workspaceTitle: 'Lab Manifest Form', + manifest, + }); + }; + + if (isLoading) { + return ( +
+
+ {Array.from({ length: 3 }).map((_) => ( + + ))} +
+
+ ); + } + + return ( +
+
+
+
+ Date: + {manifest.startDate ? formatDate(parseDate(manifest.startDate)) : '--'} To{' '} + {manifest.endDate ? formatDate(parseDate(manifest.endDate)) : '--'} +
+
+ Status: + {manifest.manifestStatus} | Type : {manifest.manifestType} | Courrier: + {manifest.courierName} +
+
+ Dispatch Date: + {manifest.dispatchDate ? formatDate(parseDate(manifest.dispatchDate)) : '--'} |{' '} + Lab person Contact: + {manifest.labPersonContact} +
+
+
+ + + + +
+ ); +}; + +export default LabManifestDetailHeader; diff --git a/packages/esm-lab-manifest-app/src/header/lab-manifest-header.component.tsx b/packages/esm-lab-manifest-app/src/header/lab-manifest-header.component.tsx new file mode 100644 index 000000000..484ae4d59 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/header/lab-manifest-header.component.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Calendar, Location } from '@carbon/react/icons'; +import { useSession, formatDate } from '@openmrs/esm-framework'; +import styles from './lab-manifest-header.scss'; +import LabManifestIllustration from './lab-manifest-illustration.component'; + +interface LabManifestHeaderProps { + title: string; +} +export const LabManifestHeader: React.FC = ({ title }) => { + const { t } = useTranslation(); + const userSession = useSession(); + const userLocation = userSession?.sessionLocation?.display; + + return ( +
+
+ +
+

{t('labManifest', 'Lab manifest Management')}

+

{title}

+
+
+
+
+ + {userLocation} + · + + {formatDate(new Date(), { mode: 'standard' })} +
+
+
+ ); +}; diff --git a/packages/esm-lab-manifest-app/src/header/lab-manifest-header.scss b/packages/esm-lab-manifest-app/src/header/lab-manifest-header.scss new file mode 100644 index 000000000..6931821f9 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/header/lab-manifest-header.scss @@ -0,0 +1,105 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.header { + @include type.type-style('body-compact-02'); + color: $text-02; + height: spacing.$spacing-12; + background-color: $ui-02; + border: 1px solid $ui-03; + border-left: 0px; + display: flex; + justify-content: space-between; + margin-bottom: 2rem; +} + +.leftJustifiedItems { + display: flex; + flex-direction: row; + align-items: center; + margin-left: 0.75rem; +} + +.rightJustifieditems { + @include type.type-style('body-compact-02'); + color: $text-02; + padding-top: 1rem; +} + +.pageName { + @include type.type-style('heading-04'); +} + +.pageLabels { + margin-left: 1rem; + + p:first-of-type { + margin-bottom: 0.25rem; + } +} + +.dateAndLocation { + 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'); +} + +svg.iconOverrides { + width: 72 !important; + height: 72 !important; + fill: var(--brand-03); +} + +.svgContainer svg { + width: 72px; + height: 72px; + fill: var(--brand-03); +} + +.cardContainer { + background-color: $ui-02; + display: flex; + justify-content: space-between; + padding: 0 spacing.$spacing-05 spacing.$spacing-07 spacing.$spacing-03; + flex-flow: row wrap; + margin-top: -(spacing.$spacing-03); +} + +.manifestDetailHeader { + display: flex; + justify-content: space-between; + padding: spacing.$spacing-05; + flex-flow: row wrap; + align-items: center; + background-color: $ui-02; + gap: 2rem; +} + +.manifestDetailContent { + flex-grow: 1; + gap: 0.5rem; + flex-direction: column; + display: flex; +} + +.btnSet { + display: flex; + justify-content: space-between; + padding: spacing.$spacing-05; + flex-flow: row wrap; + align-items: center; +} diff --git a/packages/esm-lab-manifest-app/src/header/lab-manifest-illustration.component.tsx b/packages/esm-lab-manifest-app/src/header/lab-manifest-illustration.component.tsx new file mode 100644 index 000000000..0e750ab22 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/header/lab-manifest-illustration.component.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './lab-manifest-header.scss'; +import { ChemistryReference } from '@carbon/react/icons'; + +const LabManifestIllustration: React.FC = () => { + return ( +
+ +
+ ); +}; + +export default LabManifestIllustration; diff --git a/packages/esm-lab-manifest-app/src/hooks/index.ts b/packages/esm-lab-manifest-app/src/hooks/index.ts new file mode 100644 index 000000000..b4cb54e63 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/hooks/index.ts @@ -0,0 +1,2 @@ +export { default as useLabManifests } from './useLabManifests'; +export { default as useLabManifest } from './useLabManifest'; diff --git a/packages/esm-lab-manifest-app/src/hooks/useActiveRequests.ts b/packages/esm-lab-manifest-app/src/hooks/useActiveRequests.ts new file mode 100644 index 000000000..a90f2bae3 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/hooks/useActiveRequests.ts @@ -0,0 +1,26 @@ +import { restBaseUrl } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { activeRequests } from '../lab-manifest.mock'; +import { ActiveRequests } from '../types'; + +const mockeFetch = async (url: string) => { + const status = url.split('=').at(-1); + return await new Promise>((resolve, _) => { + setTimeout(() => { + resolve(activeRequests); + }, 3000); + }); +}; + +const useActiveRequests = () => { + const url = `${restBaseUrl}/active-request`; + const { isLoading, error, data } = useSWR>(url, mockeFetch); + + return { + isLoading, + requests: data ?? [], + error, + }; +}; + +export default useActiveRequests; diff --git a/packages/esm-lab-manifest-app/src/hooks/useLabManifest.tsx b/packages/esm-lab-manifest-app/src/hooks/useLabManifest.tsx new file mode 100644 index 000000000..b0b8ca133 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/hooks/useLabManifest.tsx @@ -0,0 +1,17 @@ +import { FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { extractLabManifest } from '../lab-manifest.resources'; +import { LabManifest } from '../types'; + +const useLabManifest = (manifestUuid: string) => { + const url = `${restBaseUrl}/labmanifest/${manifestUuid}`; + const { isLoading, error, data } = useSWR>(url, openmrsFetch); + + return { + isLoading, + error, + manifest: data?.data ? extractLabManifest(data!.data!) : undefined, + }; +}; + +export default useLabManifest; diff --git a/packages/esm-lab-manifest-app/src/hooks/useLabManifests.ts b/packages/esm-lab-manifest-app/src/hooks/useLabManifests.ts new file mode 100644 index 000000000..acc00ecdc --- /dev/null +++ b/packages/esm-lab-manifest-app/src/hooks/useLabManifests.ts @@ -0,0 +1,17 @@ +import { FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { extractLabManifest } from '../lab-manifest.resources'; +import { LabManifest } from '../types'; + +const useLabManifests = (status: string) => { + const url = `${restBaseUrl}/labmanifest?v=full&status=${status}`; + const { isLoading, error, data } = useSWR }>>(url, openmrsFetch); + + return { + isLoading, + manifests: (data?.data?.results ?? []).map(extractLabManifest), + error, + }; +}; + +export default useLabManifests; diff --git a/packages/esm-lab-manifest-app/src/index.ts b/packages/esm-lab-manifest-app/src/index.ts new file mode 100644 index 000000000..f4f6164d3 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/index.ts @@ -0,0 +1,36 @@ +import { getAsyncLifecycle, defineConfigSchema, getSyncLifecycle, registerBreadcrumbs } from '@openmrs/esm-framework'; +import { configSchema } from './config-schema'; +import { createLeftPanelLink } from './component/left-panel-link.component'; + +const moduleName = '@kenyaemr/esm-lab-manifest-app'; + +const options = { + featureName: 'esm-lab-manifest-app', + moduleName, +}; + +export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); + +export function startupApp() { + const labManifestBasepath = `${window.spaBase}/home/lab-manifest`; + + defineConfigSchema(moduleName, configSchema); + registerBreadcrumbs([ + { + title: 'lab-manifest', + path: labManifestBasepath, + parent: `${window.spaBase}/home`, + }, + ]); +} + +export const root = getAsyncLifecycle(() => import('./root.component'), options); +export const labManifestForm = getAsyncLifecycle(() => import('./forms/lab-manifest-form.workspace'), options); + +export const labManifestDashboardLink = getSyncLifecycle( + createLeftPanelLink({ + name: 'lab-manifest', + title: 'Lab Manifest', + }), + options, +); diff --git a/packages/esm-lab-manifest-app/src/lab-manifest.mock.ts b/packages/esm-lab-manifest-app/src/lab-manifest.mock.ts new file mode 100644 index 000000000..3298ebd0a --- /dev/null +++ b/packages/esm-lab-manifest-app/src/lab-manifest.mock.ts @@ -0,0 +1,5 @@ +import { ActiveRequests, MappedLabManifest, LabManifestSample } from './types'; + +export const labManifestSamples: Array = []; + +export const activeRequests: Array = []; diff --git a/packages/esm-lab-manifest-app/src/lab-manifest.resources.ts b/packages/esm-lab-manifest-app/src/lab-manifest.resources.ts new file mode 100644 index 000000000..c07cdfcb1 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/lab-manifest.resources.ts @@ -0,0 +1,108 @@ +import { generateOfflineUuid, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import { z } from 'zod'; +import { LabManifest, MappedLabManifest } from './types'; + +export const LabManifestFilters = [ + { + label: 'Draft', + value: 'Draft', + }, + { + label: 'Ready To send', + value: 'Ready to send', + }, + { + label: 'On Hold', + value: 'On Hold', + }, + { + label: 'Sending', + value: 'Sending', + }, + { + label: 'Submitted', + value: 'Submitted', + }, + { + label: 'Incomplete with Errors', + value: 'Incomplete errors', + }, + { + label: 'Incomplete With Results', + value: 'Incomplete results', + }, + { + label: 'Complete with Errors', + value: 'Complete errors', + }, + { + label: 'Complete with Results', + value: 'Complete results', + }, +]; +const PHONE_NUMBER_REGEX = /^(\+?254|0)((7|1)\d{8})$/; + +export const labManifestFormSchema = z.object({ + startDate: z.date({ coerce: true }), + endDate: z.date({ coerce: true }), + manifestType: z.string(), + dispatchDate: z.date({ coerce: true }), + courierName: z.string().optional(), + personHandedTo: z.string().optional(), + county: z.string().optional(), + subCounty: z.string().optional(), + facilityEmail: z.string().email(), + facilityPhoneContact: z.string().regex(PHONE_NUMBER_REGEX, { message: 'Invalid phone number' }), + clinicianName: z.string(), + clinicianContact: z.string().regex(PHONE_NUMBER_REGEX, { message: 'Invalid phone number' }), + labPersonContact: z.string().regex(PHONE_NUMBER_REGEX, { message: 'Invalid phone number' }), + manifestStatus: z.string(), +}); + +export const manifestTypes = [ + { + value: 'VL', + label: 'Viral load', + }, +]; + +export const saveLabManifest = async (data: z.infer, manifestId: string | undefined) => { + let url; + const abortController = new AbortController(); + + if (!manifestId) { + url = `${restBaseUrl}/labmanifest`; + } else { + url = `${restBaseUrl}/labmanifest/${manifestId}`; + } + + return openmrsFetch(url, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify(data), + signal: abortController.signal, + }); +}; + +export const extractLabManifest = (manifest: LabManifest) => + ({ + uuid: manifest.uuid, + dispatchDate: manifest.dispatchDate, + endDate: manifest.dispatchDate, + startDate: manifest.startDate, + clinicianContact: manifest.clinicianPhoneContact, + clinicianName: manifest.clinicianName, + county: manifest.county, + courierName: manifest.courier, + facilityEmail: manifest.facilityEmail, + facilityPhoneContact: manifest.facilityPhoneContact, + labPersonContact: manifest.labPocPhoneNumber, + manifestId: manifest.identifier, + manifestStatus: manifest.status, + // manifestType: manifest.manifestType, + personHandedTo: manifest.courierOfficer, + subCounty: manifest.subCounty, + samples: manifest.labManifestOrders ?? [], + } as MappedLabManifest); diff --git a/packages/esm-lab-manifest-app/src/metrics/lab-manifest-header.scss b/packages/esm-lab-manifest-app/src/metrics/lab-manifest-header.scss new file mode 100644 index 000000000..46ca5fbca --- /dev/null +++ b/packages/esm-lab-manifest-app/src/metrics/lab-manifest-header.scss @@ -0,0 +1,22 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.metricsContainer { + display: flex; + justify-content: space-between; + background-color: $ui-02; + height: spacing.$spacing-10; + align-items: center; + padding: 0 spacing.$spacing-05; +} + +.metricsTitle { + @include type.type-style('heading-03'); + color: $ui-05; +} + +.actionBtn { + display: flex; + column-gap: 0.5rem; +} diff --git a/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metric-value.component.tsx b/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metric-value.component.tsx new file mode 100644 index 000000000..786b7d963 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metric-value.component.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { useLabManifests } from '../hooks'; +import { SkeletonText } from '@carbon/react'; +import styles from './lab-manifest-metrics.scss'; + +interface LabManifestMetricValueProps { + status: string; +} + +const LabManifestMetricValue: React.FC = ({ status }) => { + const { error, isLoading, manifests } = useLabManifests(status); + if (isLoading) { + return ; + } + if (error) { + return; + } + return ( + + {status}: {manifests.length} + + ); +}; + +export default LabManifestMetricValue; diff --git a/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metrics-header.component.tsx b/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metrics-header.component.tsx new file mode 100644 index 000000000..a52095a5c --- /dev/null +++ b/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metrics-header.component.tsx @@ -0,0 +1,34 @@ +import { Button } from '@carbon/react'; +import { ArrowRight } from '@carbon/react/icons'; +import { launchWorkspace } from '@openmrs/esm-framework'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './lab-manifest-header.scss'; + +const MetricsHeader = () => { + const { t } = useTranslation(); + const metricsTitle = t('labManifestSummary', 'Lab Manifest Summary'); + + const handleAddLabManifest = () => { + launchWorkspace('lab-manifest-form', { + workspaceTitle: 'Lab Manifest Form', + }); + }; + + return ( +
+ {metricsTitle} +
+ +
+
+ ); +}; + +export default MetricsHeader; diff --git a/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metrics.component.tsx b/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metrics.component.tsx new file mode 100644 index 000000000..fdb055db6 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metrics.component.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { LabManifestFilters } from '../lab-manifest.resources'; +import MetricsHeader from './lab-manifest-metrics-header.component'; +import styles from './lab-manifest-metrics.scss'; +import LabManifestMetricValue from './lab-manifest-metric-value.component'; + +export interface Service { + uuid: string; + display: string; +} + +function LabManifestMetrics() { + const { t } = useTranslation(); + return ( + <> + +
+ {LabManifestFilters.map((f, index) => ( + + ))} +
+ + ); +} + +export default LabManifestMetrics; diff --git a/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metrics.scss b/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metrics.scss new file mode 100644 index 000000000..0ba17571f --- /dev/null +++ b/packages/esm-lab-manifest-app/src/metrics/lab-manifest-metrics.scss @@ -0,0 +1,17 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/colors'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.cardContainer { + background-color: $color-gray-30; + display: flex; + justify-content: space-between; + padding: spacing.$spacing-05; + flex-flow: row wrap; + margin-top: spacing.$spacing-05; + gap: spacing.$spacing-05; +} + +.metricContainer { + max-width: spacing.$spacing-13; +} diff --git a/packages/esm-lab-manifest-app/src/root.component.tsx b/packages/esm-lab-manifest-app/src/root.component.tsx new file mode 100644 index 000000000..2ecb21c62 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/root.component.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import LabManifestComponent from './component/lab-manifest.component'; +import LabManifestDetail from './component/lab-manifest-detail.component'; + +const Root: React.FC = () => { + const baseName = window.getOpenmrsSpaBase() + 'home/lab-manifest'; + + return ( + + + } /> + } /> + + + ); +}; + +export default Root; diff --git a/packages/esm-lab-manifest-app/src/root.scss b/packages/esm-lab-manifest-app/src/root.scss new file mode 100644 index 000000000..93dfeb4cb --- /dev/null +++ b/packages/esm-lab-manifest-app/src/root.scss @@ -0,0 +1,15 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; + +.container { + padding: spacing.$spacing-07; +} + +.welcome { + @include type.type-style('heading-04'); + margin: spacing.$spacing-05 0; +} + +.explainer { + margin-bottom: 2rem; +} diff --git a/packages/esm-lab-manifest-app/src/routes.json b/packages/esm-lab-manifest-app/src/routes.json new file mode 100644 index 000000000..15456c3ca --- /dev/null +++ b/packages/esm-lab-manifest-app/src/routes.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json.openmrs.org/routes.schema.json", + "backendDependencies": { + "webservices.rest": "^2.24.0" + }, + "extensions": [ + { + "component": "labManifestDashboardLink", + "name": "lab-manifest-dashboard-link", + "slot": "homepage-dashboard-slot", + "meta": { + "name": "lab-manifest", + "title": "lab-manifest", + "slot": "lab-manifest-dashboard-slot" + } + }, + { + "name": "lab-manifest-form", + "component": "labManifestForm" + }, + { + "component": "root", + "name": "lab-manifest-dashboard-root", + "slot": "lab-manifest-dashboard-slot" + } + ], + "pages": [ + { + "component": "root", + "route": "lab-manifest" + } + ] +} diff --git a/packages/esm-lab-manifest-app/src/setup-tests.ts b/packages/esm-lab-manifest-app/src/setup-tests.ts new file mode 100644 index 000000000..666127af3 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/setup-tests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/extend-expect'; diff --git a/packages/esm-lab-manifest-app/src/tables/lab-manifest-active-requests.component.tsx b/packages/esm-lab-manifest-app/src/tables/lab-manifest-active-requests.component.tsx new file mode 100644 index 000000000..3688b5928 --- /dev/null +++ b/packages/esm-lab-manifest-app/src/tables/lab-manifest-active-requests.component.tsx @@ -0,0 +1,161 @@ +import { + Button, + DataTable, + DataTableSkeleton, + Pagination, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from '@carbon/react'; +import { View } from '@carbon/react/icons'; +import { ErrorState, navigate, useLayoutType, usePagination } from '@openmrs/esm-framework'; +import { CardHeader, EmptyState, usePaginationInfo } from '@openmrs/esm-patient-common-lib'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './lab-manifest-table.scss'; +import useActiveRequests from '../hooks/useActiveRequests'; +import { ActiveRequests } from '../types'; + +interface LabManifestActiveRequestsProps { + manifestUuid: string; +} + +const LabManifestActiveRequests: React.FC = ({ manifestUuid }) => { + const { error, isLoading, requests } = useActiveRequests(); + + const { t } = useTranslation(); + const [pageSize, setPageSize] = useState(10); + const headerTitle = t('activeRequests', 'Active Requests'); + const { results, totalPages, currentPage, goTo } = usePagination(requests, pageSize); + const { pageSizes } = usePaginationInfo(pageSize, totalPages, currentPage, results.length); + + const headers = [ + { + header: t('patientName', 'Patient name'), + key: 'startDate', + }, + { + header: t('cccKDODNumber', 'CCC/KDOD Number'), + key: 'cccKDODNumber', + }, + { + header: t('batchNumber', 'Batch Number'), + key: 'batchNumber', + }, + { + header: t('sampleType', 'Sample type'), + key: 'sampleType', + }, + { + header: t('manifestId', 'Manifest Id'), + key: 'manifestId', + }, + { + header: t('labPersonContact', 'Lab person Contact'), + key: 'labPersonContact', + }, + { + header: t('status', 'Status'), + key: 'status', + }, + { + header: t('dispatch', 'Dispatch'), + key: 'dispatch', + }, + { + header: t('actions', 'Actions'), + key: 'actions', + }, + ]; + + const handleViewManifestSamples = (manifestUuid: string) => { + navigate({ to: window.getOpenmrsSpaBase() + `home/lab-manifest/${manifestUuid}` }); + }; + + const tableRows = + (results as ActiveRequests[])?.map((activeRequest) => { + return { + id: `${activeRequest.uuid}`, + actions: ( +