diff --git a/README.md b/README.md
index 24f4fd3..c68f548 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,24 @@ Follow the mongodb setup instructions in the [REMS End to End Setup Guide](https
If you would rather run with docker, follow the setup found in the [REMS Simple Setup Guide](https://github.com/mcode/rems-setup/blob/main/SimpleSetupGuide.md) (this will also setup the other REMS applications in docker as well).
+## Starting the frontend
+
+Cd into the frontend repository
+
+### `cd frontend/`
+
+Next, install the required dependencies by running the following:
+
+### `npm install`
+
+Next, start the frontend with the following:
+
+### `npm start`
+
+Go to the UI running on http://localhost:5173/ (or whichever port it was run on)
+
+Still need to update docker to start the UI automatically.
+
## Available Scripts
In the project directory, you can run:
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..74872fd
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,50 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
+
+- Configure the top-level `parserOptions` property like this:
+
+```js
+export default tseslint.config({
+ languageOptions: {
+ // other options...
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+})
+```
+
+- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
+- Optionally add `...tseslint.configs.stylisticTypeChecked`
+- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
+
+```js
+// eslint.config.js
+import react from 'eslint-plugin-react'
+
+export default tseslint.config({
+ // Set the react version
+ settings: { react: { version: '18.3' } },
+ plugins: {
+ // Add the react plugin
+ react,
+ },
+ rules: {
+ // other rules...
+ // Enable its recommended rules
+ ...react.configs.recommended.rules,
+ ...react.configs['jsx-runtime'].rules,
+ },
+})
+```
diff --git a/frontend/config.json b/frontend/config.json
new file mode 100644
index 0000000..2f2a382
--- /dev/null
+++ b/frontend/config.json
@@ -0,0 +1,6 @@
+{
+ "realm": "ClientFhirServer",
+ "client": "app-login",
+ "auth": "http://localhost:8180/",
+ "scopeId": "pims"
+}
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000..f3bd4ef
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,25 @@
+import js from '@eslint/js';
+import globals from 'globals';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
+import tseslint from 'typescript-eslint';
+
+export default tseslint.config(
+ { ignores: ['dist'] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }]
+ }
+ }
+);
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..96e2c90
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Rems Admin UI
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..7f69441
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "start": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.13.3",
+ "@emotion/styled": "^11.13.0",
+ "@mui/icons-material": "^6.1.0",
+ "@mui/material": "^6.1.0",
+ "axios": "^1.7.7",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.26.2"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.9.0",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "eslint": "^9.9.0",
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
+ "eslint-plugin-react-refresh": "^0.4.9",
+ "globals": "^15.9.0",
+ "typescript": "^5.5.3",
+ "typescript-eslint": "^8.0.1",
+ "vite": "^5.4.1"
+ }
+}
diff --git a/frontend/public/admin.png b/frontend/public/admin.png
new file mode 100644
index 0000000..6691ba1
Binary files /dev/null and b/frontend/public/admin.png differ
diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/frontend/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..d1b03b7
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,81 @@
+.App {
+ text-align: center;
+ background-color: #2F6A47;
+ padding: 20px;
+ margin-bottom: 20px;
+}
+
+.App-logo {
+ height: 40vmin;
+ pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .App-logo {
+ animation: App-logo-spin infinite 20s linear;
+ }
+}
+
+.App-header {
+ background-color: #2F6A47;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
+}
+
+.App-link {
+ color: #61dafb;
+}
+
+.navButtons {
+ list-style: none;
+ text-decoration: none;
+ margin-left: 5px;
+}
+
+.white-btn {
+ color: white !important;
+ border-color: white !important;
+ margin-right: 5px !important;
+}
+.right-btn {
+ display: flex;
+ justify-content: flex-end;
+ margin: 10px 16px 10px 0;
+}
+
+.App h1 {
+ color: white;
+ line-height: 1.3;
+ letter-spacing: 0.00938em;
+ font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
+}
+
+.logo {
+ grid-area: main;
+ text-align: left;
+ margin-right: auto;
+ display: inline-flex;
+ box-sizing: unset;
+}
+
+.links {
+ grid-area: right;
+ margin-left: auto;
+ margin-top: 25px;
+}
+
+.containerg {
+ display: grid;
+ grid-template-areas: ' main right';
+}
+
+.err-msg {
+ color: red;
+ font-style: italic;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..895151d
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,93 @@
+import './App.css';
+import { useState } from 'react';
+import { Button } from '@mui/material';
+import Box from '@mui/material/Box';
+import { Container } from '@mui/system';
+import DatasetIcon from '@mui/icons-material/Dataset';
+import Login from './views/Login';
+import Data from './views/DataViews/Data';
+import axios from 'axios';
+import Dialog from '@mui/material/Dialog';
+import DialogActions from '@mui/material/DialogActions';
+import DialogContent from '@mui/material/DialogContent';
+import DialogContentText from '@mui/material/DialogContentText';
+import DialogTitle from '@mui/material/DialogTitle';
+
+function App() {
+ const [token, setToken] = useState(null);
+ const [open, setOpen] = useState(false);
+ const [forceRefresh, setForceRefresh] = useState(false);
+
+ const handleClickOpen = () => {
+ setOpen(true);
+ };
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ const resetDB = async () => {
+ setOpen(false);
+ await axios
+ .post('http://localhost:8090/etasu/reset')
+ .then(function (response: any) {
+ console.log(response);
+ setForceRefresh(true);
+ })
+ .catch((error: any) => {
+ console.log('Error resetting the DB -- > ', error);
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
Rems Admin
+
+ {token ? (
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ {token ? : }
+
+ );
+}
+
+export default App;
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..ec2585e
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..ea9e363
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import App from './App.tsx';
+import './index.css';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/frontend/src/views/DataViews/CaseCollection.tsx b/frontend/src/views/DataViews/CaseCollection.tsx
new file mode 100644
index 0000000..865c6b4
--- /dev/null
+++ b/frontend/src/views/DataViews/CaseCollection.tsx
@@ -0,0 +1,180 @@
+import axios from 'axios';
+import { useEffect, useState, SetStateAction } from 'react';
+import {
+ Button,
+ Card,
+ CardContent,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow
+} from '@mui/material';
+import IconButton from '@mui/material/IconButton';
+import DeleteIcon from '@mui/icons-material/Delete';
+import { Refresh } from '@mui/icons-material';
+
+export type RemsCase = {
+ case_number?: string;
+ auth_number?: string;
+ patientFirstName?: string;
+ patientLastName?: string;
+ patientDOB?: string;
+ drugCode?: string;
+ drugName?: number;
+ status?: string;
+ dispenseStatus?: string;
+ metRequirements:
+ | {
+ metRequirementId: string;
+ requirementsDescription: string;
+ requirementName: string;
+ stakeholderId: string;
+ completed: boolean;
+ }[];
+ _id: string;
+};
+const CaseCollection = (props: { refresh: boolean }) => {
+ const [allData, setAllData] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ if (props.refresh) {
+ getAllRemsCase();
+ }
+ }, [props.refresh]);
+
+ useEffect(() => {
+ getAllRemsCase();
+ }, []);
+
+ const getAllRemsCase = async () => {
+ const url = 'http://localhost:8090/api/all/remscase';
+ await axios
+ .get(url)
+ .then(function (response: { data: SetStateAction }) {
+ setAllData(response.data);
+ setIsLoading(false);
+ })
+ .catch((error: any) => {
+ setIsLoading(false);
+ console.log('Error -- > ', error);
+ });
+ };
+
+ const deleteSingleRow = async (event: any, row: RemsCase) => {
+ const url = 'http://localhost:8090/api/remsCase/deleteOne';
+ await axios
+ .post(url, { data: { params: row } })
+ .then(function (response: { data: any; status: number }) {
+ if (response.status === 200) {
+ getAllRemsCase();
+ }
+ })
+ .catch((error: any) => {
+ setIsLoading(false);
+ console.log('Error -- > ', error);
+ });
+ };
+
+ const formattedReqs = (row: RemsCase) => {
+ let reqNames: String[] = [];
+ row.metRequirements.forEach((req: any) => {
+ const completed = req.completed ? 'Completed' : 'Not completed';
+ reqNames.push(`${req.requirementName}: ${completed}`);
+ });
+ return reqNames.join(', ');
+ };
+
+ if (allData.length < 1 && !isLoading) {
+ return (
+
+
+ }
+ onClick={() => {
+ getAllRemsCase();
+ }}
+ >
+ Refresh
+
+
+ No data
+
+ );
+ } else {
+ return (
+
+
+ }
+ onClick={() => {
+ getAllRemsCase();
+ }}
+ >
+ Refresh
+
+
+
+
+
+
+
+
+
+ Case Number
+ Patient First Name
+ Patient Last Name
+ Drug Name
+ Drug Code
+ Patient DOB
+ Status
+ Dispense Status
+ Authorization Number
+ Met Requirements
+ Delete
+
+
+
+ {allData.map(row => {
+ const metReq = formattedReqs(row);
+ return (
+
+ {row.case_number}
+ {row.patientFirstName}
+ {row.patientLastName}
+ {row.drugName}
+ {row.drugCode}
+ {row.patientDOB}
+ {row.status}
+ {row.dispenseStatus}
+ {row.auth_number}
+ {metReq}
+
+ deleteSingleRow(event, row)}
+ >
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+ }
+};
+
+export default CaseCollection;
diff --git a/frontend/src/views/DataViews/Data.tsx b/frontend/src/views/DataViews/Data.tsx
new file mode 100644
index 0000000..5c5e30e
--- /dev/null
+++ b/frontend/src/views/DataViews/Data.tsx
@@ -0,0 +1,67 @@
+import { SyntheticEvent, useState } from 'react';
+import { Box, Tab, Tabs } from '@mui/material';
+import { Container } from '@mui/system';
+import CaseCollection from './CaseCollection';
+import Medications from './Medications';
+import MetRequirements from './MetRequirements';
+
+function a11yProps(index: number) {
+ return {
+ id: `simple-tab-${index}`,
+ 'aria-controls': `simple-tabpanel-${index}`
+ };
+}
+const Data = (props: { refresh: boolean }) => {
+ const [tabIndex, setValue] = useState(0);
+
+ const handleChange = (event: SyntheticEvent, newValue: number) => {
+ setValue(newValue);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {tabIndex === 0 && (
+
+
+
+ )}
+ {tabIndex === 1 && (
+
+
+
+ )}
+ {tabIndex === 2 && (
+
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default Data;
diff --git a/frontend/src/views/DataViews/Medications.tsx b/frontend/src/views/DataViews/Medications.tsx
new file mode 100644
index 0000000..373a734
--- /dev/null
+++ b/frontend/src/views/DataViews/Medications.tsx
@@ -0,0 +1,138 @@
+import axios from 'axios';
+import { useEffect, useState, SetStateAction } from 'react';
+import {
+ Box,
+ Button,
+ Card,
+ CardActions,
+ CardContent,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Typography
+} from '@mui/material';
+import { Refresh } from '@mui/icons-material';
+
+export type Medication = {
+ code?: string;
+ codeSystem?: string;
+ name?: string;
+ requirements: Array<{
+ name: string;
+ description: string;
+ resourceId: string;
+ appContext: string;
+ requiredToDispense: boolean;
+ }>;
+ _id: string;
+};
+
+const Medications = (props: { refresh: boolean }) => {
+ const [allData, setAllData] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ if (props.refresh) {
+ getAllMedications();
+ }
+ }, [props.refresh]);
+
+ useEffect(() => {
+ getAllMedications();
+ }, []);
+
+ const getAllMedications = async () => {
+ const url = 'http://localhost:8090/api/all/medications';
+ await axios
+ .get(url)
+ .then(function (response: { data: SetStateAction }) {
+ setAllData(response.data);
+ setIsLoading(false);
+ })
+ .catch((error: any) => {
+ setIsLoading(false);
+ console.log('Error -- > ', error);
+ });
+ };
+
+ const formattedReqs = (row: Medication) => {
+ let reqNames: String[] = [];
+ row.requirements.forEach((req: any) => {
+ reqNames.push(req.name);
+ });
+ return reqNames.join(', ');
+ };
+
+ if (allData.length < 1 && !isLoading) {
+ return (
+
+
+ }
+ onClick={() => {
+ getAllMedications();
+ }}
+ >
+ Refresh
+
+
+ No data
+
+ );
+ } else {
+ return (
+
+
+
+ }
+ sx={{backgroundColor: '#2F6A47'}}
+ onClick={() => {
+ getAllMedications();
+ }}
+ >
+ Refresh
+
+
+
+
+
+
+
+
+ Name
+ Code
+ Code System
+ Requirements
+
+
+
+ {allData.map(row => {
+ const format = formattedReqs(row);
+ return (
+
+ {row.name}
+ {row.code}
+ {row.codeSystem}
+ {format}
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+ }
+};
+
+export default Medications;
diff --git a/frontend/src/views/DataViews/MetRequirements.tsx b/frontend/src/views/DataViews/MetRequirements.tsx
new file mode 100644
index 0000000..98e2d5f
--- /dev/null
+++ b/frontend/src/views/DataViews/MetRequirements.tsx
@@ -0,0 +1,163 @@
+import axios from 'axios';
+import { useEffect, useState, SetStateAction } from 'react';
+import {
+ Box,
+ Button,
+ Card,
+ CardActions,
+ CardContent,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Typography
+} from '@mui/material';
+import IconButton from '@mui/material/IconButton';
+import DeleteIcon from '@mui/icons-material/Delete';
+import { Refresh } from '@mui/icons-material';
+
+export type MetRequirements = {
+ drugName?: string;
+ requirementName?: string;
+ stakeholderId?: string;
+ completed?: boolean;
+ completedQuestionnaire?: { questionnaire: string };
+ case_numbers?: [];
+ _id: string;
+};
+
+const MetRequirements = (props: { refresh: boolean }) => {
+ const [allData, setAllData] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ if (props.refresh) {
+ getAllMetReqs();
+ }
+ }, [props.refresh]);
+
+ useEffect(() => {
+ getAllMetReqs();
+ }, []);
+
+ const getAllMetReqs = async () => {
+ const url = 'http://localhost:8090/api/all/metreqs';
+ await axios
+ .get(url)
+ .then(function (response: { data: any }) {
+ setAllData(response.data);
+ setIsLoading(false);
+ })
+ .catch((error: any) => {
+ setIsLoading(false);
+ console.log('Error -- > ', error);
+ });
+ };
+
+ const deleteSingleRow = async (event: any, row: MetRequirements) => {
+ const url = 'http://localhost:8090/api/metreqs/deleteOne';
+ await axios
+ .post(url, { data: { params: row } })
+ .then(function (response: { data: any; status: number }) {
+ if (response.status === 200) {
+ getAllMetReqs();
+ }
+ })
+ .catch((error: any) => {
+ setIsLoading(false);
+ console.log('Error -- > ', error);
+ });
+ };
+
+ const formattedQuestionnaire = (row: MetRequirements) => {
+ return row?.completedQuestionnaire?.questionnaire.split('http://localhost:8090/4_0_0/')[1];
+ };
+ const formattedCompleted = (row: MetRequirements) => {
+ return row?.completed === true ? 'Yes' : 'No';
+ };
+
+ if (allData.length < 1 && !isLoading) {
+ return (
+
+
+ }
+ onClick={() => {
+ getAllMetReqs();
+ }}
+ >
+ Refresh
+
+
+ No data
+
+ );
+ } else {
+ return (
+
+
+
+ }
+ onClick={() => {
+ getAllMetReqs();
+ }}
+ >
+ Refresh
+
+
+
+
+
+
+
+
+ Drug Name
+ Requirement Name
+ Case Numbers
+ Completed
+ Completed Questionnaire
+ Delete
+
+
+
+ {allData.map(row => {
+ const format = formattedQuestionnaire(row);
+ const complete = formattedCompleted(row);
+ return (
+
+ {row.drugName}
+ {row.requirementName}
+ {row.case_numbers?.join(', ')}
+ {complete}
+ {format}
+
+ deleteSingleRow(event, row)}
+ >
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+ }
+};
+
+export default MetRequirements;
diff --git a/frontend/src/views/Login.tsx b/frontend/src/views/Login.tsx
new file mode 100644
index 0000000..15b7df6
--- /dev/null
+++ b/frontend/src/views/Login.tsx
@@ -0,0 +1,97 @@
+import { SetStateAction, useState } from 'react';
+import axios from 'axios';
+import { Avatar, Box, Button, Container, CssBaseline, TextField, Typography } from '@mui/material';
+import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
+import config from '../../config.json';
+
+const Login = props => {
+ const [showMessage, setShowMessage] = useState(false);
+
+ const handleSubmit = (event: {
+ preventDefault: () => void;
+ currentTarget: HTMLFormElement | undefined;
+ }) => {
+ event.preventDefault();
+ const data = new FormData(event.currentTarget);
+ const user = data.get('username')?.toString();
+ const pass = data.get('password')?.toString();
+ if (user && pass) {
+ const params = new URLSearchParams();
+ params.append('username', user);
+ params.append('password', pass);
+ params.append('grant_type', 'password');
+ params.append('client_id', config.client);
+ axios
+ .post(`${config.auth}/realms/${config.realm}/protocol/openid-connect/token`, params, {
+ withCredentials: true
+ })
+ .then(
+ (result: { data: { scope: string; access_token: SetStateAction } }) => {
+ // do something with the token
+ const scope = result.data.scope;
+ if (scope) {
+ setShowMessage(true);
+ props.tokenCallback(result.data.access_token);
+ } else {
+ console.error('Unauthorized User');
+ }
+ }
+ )
+ .catch(err => {
+ if (err.response.status === 401) {
+ console.error('Unknown user');
+ setShowMessage(true);
+ } else {
+ console.error(err);
+ }
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ Sign in
+
+
+
+
+
+ {showMessage ? Error signing in. Please try again.
: ''}
+
+
+
+ );
+};
+
+export default Login;
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000..f0a2350
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..0d3d714
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..6549182
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()]
+});
diff --git a/src/lib/api_routes.ts b/src/lib/api_routes.ts
new file mode 100644
index 0000000..310e7a8
--- /dev/null
+++ b/src/lib/api_routes.ts
@@ -0,0 +1,83 @@
+import { Router, Request, Response } from 'express';
+import {
+ remsCaseCollection,
+ medicationCollection,
+ metRequirementsCollection
+} from '../fhir/models';
+const router = Router();
+
+router.get('/all/remscase', async (req: Request, res: Response) => {
+ try {
+ console.log('Getting all rems case collection');
+ res.send(await remsCaseCollection.find({}));
+ } catch (error) {
+ console.log('ERROR getting rems case collection --> ', error);
+ throw error;
+ }
+});
+
+router.post('/remsCase/deleteOne', async (req: Request, res: Response) => {
+ try {
+ // delete rems case collection
+ await remsCaseCollection.findByIdAndDelete({ _id: req.body.data.params?._id });
+ // find and delete patient met requirements, either enrollment or status update forms
+ const allMatchedMetReqs = await metRequirementsCollection.find({
+ case_numbers: { $in: req.body.data.params?.case_number }
+ });
+ allMatchedMetReqs.forEach(async matchedReq => {
+ if (matchedReq.requirementName.includes('Patient')) {
+ await metRequirementsCollection.findOneAndDelete({ _id: matchedReq._id });
+ } else {
+ // If not a patient form - remove case number from prescriber forms
+ await metRequirementsCollection.findOneAndUpdate(
+ { _id: matchedReq._id },
+ {
+ case_numbers: matchedReq.case_numbers.filter(
+ num => num !== req.body.data.params?.case_number
+ )
+ },
+ { new: true }
+ );
+ }
+ });
+ res.send(
+ `Deleted REMS Case collection and patient forms with case number - ${req.body.data.params?.case_number}`
+ );
+ } catch (error) {
+ console.log('ERROR deleting data --> ', error);
+ throw error;
+ }
+});
+
+router.get('/all/medications', async (req: Request, res: Response) => {
+ try {
+ console.log('Getting all medications');
+ res.send(await medicationCollection.find({}));
+ } catch (error) {
+ console.log('ERROR getting all medications --> ', error);
+ throw error;
+ }
+});
+
+router.get('/all/metreqs', async (req: Request, res: Response) => {
+ try {
+ console.log('Getting all met requirements');
+ res.send(await metRequirementsCollection.find({}));
+ } catch (error) {
+ console.log('ERROR getting met requirements --> ', error);
+ throw error;
+ }
+});
+
+router.post('/metreqs/deleteOne', async (req: Request, res: Response) => {
+ try {
+ // delete met requirements
+ await metRequirementsCollection.findByIdAndDelete({ _id: req.body.data.params?._id });
+ res.send(`Deleted met requirement with name - ${req.body.data.params?.requirementName}`);
+ } catch (error) {
+ console.log('ERROR deleting met requirement --> ', error);
+ throw error;
+ }
+});
+
+export default router;
diff --git a/src/rems-cds-hooks b/src/rems-cds-hooks
index 51dd2d4..8a51ac5 160000
--- a/src/rems-cds-hooks
+++ b/src/rems-cds-hooks
@@ -1 +1 @@
-Subproject commit 51dd2d4d3dd46313187b4fb2c035c5e9b62d0fdd
+Subproject commit 8a51ac5c6837549e43a30c4f886c0bfd2aeae50d
diff --git a/src/server.ts b/src/server.ts
index a2e2f8f..bfb47ef 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -10,6 +10,7 @@ import encounterStartService from './hooks/rems.encounterstart';
import { Server } from '@projecttacoma/node-fhir-server-core';
import Etasu from './lib/etasu';
import Ncpdp from './ncpdp/script';
+import Api from './lib/api_routes';
import env from 'env-var';
import https from 'https';
import fs from 'fs';
@@ -29,6 +30,7 @@ const initialize = (config: any) => {
.registerCdsHooks(config.server)
.configureEtasuEndpoints()
.configureNCPDPEndpoints()
+ .configureUIEndpoints()
.setErrorRoutes();
};
@@ -135,6 +137,11 @@ class REMSServer extends Server {
return this;
}
+ configureUIEndpoints() {
+ this.app.use('/api/', Api);
+ return this;
+ }
+
/**
* @method listen
* @description Start listening on the configured port