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 (
+
+ );
+};
+
+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: (
+