Added the files.

This commit is contained in:
Batuhan Berk Başoğlu 2021-03-30 21:50:36 -04:00
commit 38ccdcbfe5
124 changed files with 32079 additions and 0 deletions

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# Hospital-Patient Management System Monorepo
![logo](design/logo.png)
## Run project locally
Open two terminals and run the following sequence in the `server` and `client` folders. Recent version of NodeJS should be installed to your machine.
```shell
# install deps
npm i
# run in development mode
npm run dev
```

25
client/.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*

16
client/README.md Normal file
View file

@ -0,0 +1,16 @@
# HMS client-side code
## Get Started
Get started developing...
```shell
# install deps
npm install
# run in development mode
npm run dev
# run tests
npm run test
```

View file

@ -0,0 +1,11 @@
#!/bin/bash
set -ev
#run only on master
if [[ $TRAVIS_PULL_REQUEST == "false" ]] && [[ $TRAVIS_BRANCH == "master" ]]; then
npm install -g firebase-tools
npm install -g selenium-webdriver
npm install codecov.io coveralls
cd functions
npm install
cd ..
fi

View file

@ -0,0 +1,88 @@
// To run this script use this command
// node bs.js yourBSUserName yourBSKey
var webdriver = require('selenium-webdriver')
var test = require('./bs_test.js')
// Input capabilities
var iPhone = {
browserName: 'iPhone',
device: 'iPhone 7',
realMobile: 'true',
os_version: '10.3',
'browserstack.user': process.argv[2],
'browserstack.key': process.argv[3],
}
var android = {
browserName: 'android',
device: 'Samsung Galaxy S8',
realMobile: 'true',
os_version: '7.0',
'browserstack.user': process.argv[2],
'browserstack.key': process.argv[3],
}
var desktopFF = {
browserName: 'Firefox',
browser_version: '59.0',
os: 'Windows',
os_version: '10',
resolution: '1024x768',
'browserstack.user': process.argv[2],
'browserstack.key': process.argv[3],
}
var desktopEdge = {
browserName: 'Edge',
browser_version: '16.0',
os: 'Windows',
os_version: '10',
resolution: '1024x768',
'browserstack.user': process.argv[2],
'browserstack.key': process.argv[3],
}
var desktopIE = {
browserName: 'Chrome',
browser_version: '69.0',
os: 'Windows',
os_version: '10',
resolution: '1024x768',
'browserstack.user': process.argv[2],
'browserstack.key': process.argv[3],
}
/*
var iPhoneDriver = new webdriver.Builder()
.usingServer('http://hub-cloud.browserstack.com/wd/hub')
.withCapabilities(iPhone)
.build()
var androidDriver = new webdriver.Builder()
.usingServer('http://hub-cloud.browserstack.com/wd/hub')
.withCapabilities(android)
.build()
*/
var desktopFFDriver = new webdriver.Builder()
.usingServer('http://hub-cloud.browserstack.com/wd/hub')
.withCapabilities(desktopFF)
.build()
var desktopEdgeDriver = new webdriver.Builder()
.usingServer('http://hub-cloud.browserstack.com/wd/hub')
.withCapabilities(desktopEdge)
.build()
var desktopIEDriver = new webdriver.Builder()
.usingServer('http://hub-cloud.browserstack.com/wd/hub')
.withCapabilities(desktopIE)
.build()
//test.runTest(iPhoneDriver)
//test.runTest(androidDriver)
test.runTest(desktopFFDriver)
test.runTest(desktopEdgeDriver)
test.runTest(desktopIEDriver)

View file

@ -0,0 +1,62 @@
var webdriver = require('selenium-webdriver')
const sleep = ms => {
return new Promise(resolve => setTimeout(resolve, ms))
}
module.exports.runTest = async driver => {
try {
await driver.get('https://www.react-most-wanted.com')
await sleep(2000)
var signInButton = driver.wait(webdriver.until.elementLocated(webdriver.By.name('signin')))
await signInButton.click()
await sleep(2000)
var passwordButton = driver.wait(
webdriver.until.elementLocated(
webdriver.By.className(
'firebaseui-idp-button mdl-button mdl-js-button mdl-button--raised firebaseui-idp-password firebaseui-id-idp-button'
)
)
)
await passwordButton.click()
await sleep(2000)
var emailInput = driver.wait(webdriver.until.elementLocated(webdriver.By.name('email')))
await emailInput.sendKeys('test@test.com')
await sleep(2000)
var nextButton = driver.wait(
webdriver.until.elementLocated(
webdriver.By.className(
'firebaseui-id-submit firebaseui-button mdl-button mdl-js-button mdl-button--raised mdl-button--colored'
)
)
)
await nextButton.click()
await sleep(2000)
var passwordInput = driver.wait(webdriver.until.elementLocated(webdriver.By.name('password')))
await passwordInput.sendKeys('123456')
await sleep(2000)
await passwordInput.sendKeys(webdriver.Key.ENTER)
await sleep(2000)
driver.quit()
} catch (e) {
console.log('Test Failed')
console.error(e)
process.exitCode = 1
process.abort()
driver.quit()
}
}

View file

@ -0,0 +1,7 @@
#!/bin/bash
set -ev
#run only on master
if [[ $TRAVIS_PULL_REQUEST == "false" ]] && [[ $TRAVIS_BRANCH == "master" ]]; then
npm run build
fi

View file

@ -0,0 +1,8 @@
#!/bin/bash
set -ev
#run only on master
if [[ $TRAVIS_PULL_REQUEST == "false" ]] && [[ $TRAVIS_BRANCH == "master" ]]; then
firebase use prod --token $FIREBASE_TOKEN
firebase deploy --non-interactive --token $FIREBASE_TOKEN
node ./continuous_deployment/bs.js $BSNAME $BSKEY
fi

6
client/jsconfig.json Normal file
View file

@ -0,0 +1,6 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}

17632
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

57
client/package.json Normal file
View file

@ -0,0 +1,57 @@
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@formatjs/intl-relativetimeformat": "7.x",
"@material-ui/core": "4.x",
"@material-ui/icons": "4.x",
"@material-ui/lab": "^4.0.0-alpha.56",
"base-shell": "1.x",
"crypto-js": "^4.0.0",
"github-markdown-css": "4.x",
"immutable": "^4.0.0-rc.12",
"intl": "1.x",
"intl-locales-supported": "1.x",
"material-ui-shell": "1.x",
"notistack": "1.x",
"react": "17.x",
"react-custom-scrollbars": "4.x",
"react-dom": "17.x",
"react-intl": "5.x",
"react-ios-pwa-prompt": "1.x",
"react-markdown": "5.x",
"react-redux": "^7.2.2",
"react-router-dom": "5.x",
"react-router-redux": "^4.0.8",
"react-scripts": "4.0.0",
"react-virtualized-auto-sizer": "1.x",
"react-window": "1.x",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0"
},
"scripts": {
"dev": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

43
client/public/index.html Normal file
View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>HealthGeeks HMS-PMS</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
client/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
client/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
client/public/robots.txt Normal file
View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

13
client/src/App.js Normal file
View file

@ -0,0 +1,13 @@
import React, { Component } from 'react'
import App from 'base-shell/lib'
import MUIConfig from 'material-ui-shell/lib'
import merge from 'base-shell/lib/utils/config'
import _config from './config'
const config = merge(MUIConfig, _config)
export default class Demo extends Component {
render() {
return <App config={config} />
}
}

View file

@ -0,0 +1,11 @@
/* eslint-disable import/no-anonymous-default-export */
export default {
API_URL: 'http://localhost:3001',
user: {
user: "/api/v1/staff",
},
auth: {
register: "/api/v1/staff/register",
login: "/api/v1/staff/login"
},
};

View file

@ -0,0 +1,37 @@
import { lazy } from 'react'
import locales from './locales'
import apiRoutes from './apiRoutes'
import routes from './routes'
import getMenuItems from './menuItems'
import themes from './themes'
import parseLanguages from 'base-shell/lib/utils/locale'
const config = {
auth: {
signInURL: '/',
},
apiRoutes,
routes,
locale: {
locales,
defaultLocale: parseLanguages( 'en'),
onError: (e) => {
//console.warn(e)
return
},
},
menu: {
getMenuItems,
},
theme: {
themes,
defaultThemeID: 'default',
defaultType: 'light',
},
pages: {
LandingPage: lazy(() => import('../pages/LandingPage/LandingPage')),
PageNotFound: lazy(() => import('../pages/PageNotFound/PageNotFound')),
},
}
export default config

View file

@ -0,0 +1 @@
export { default } from './config.js'

View file

@ -0,0 +1,43 @@
import { defineMessages } from 'react-intl'
const messages = defineMessages({
app_name: 'React Meist Gesucht',
sign_in: 'Anmelden',
sign_out: 'Abmelden',
sign_up: 'Anmeldung',
email: 'Email',
username: 'Nutzername',
password: 'Passwort',
about: 'Über',
home: 'Startseite',
page_not_found: 'Seite nicht gefunden',
settings: 'Einstellungen',
theme: 'Thema',
default: 'Standard',
red: 'Rot',
green: 'Grün',
language: 'Sprache',
en: 'Englisch',
de: 'Deutsch',
ru: 'Russisch',
menu: 'Menü',
menu_mini_mode: 'Mini-Menü',
offline: 'Offline',
demos:'Demos',
dialog_demo:'Demo Dialog',
dialog_title:'Dialog titel',
dialog_action:'JA, Löschen',
dialog_message:`Dialognachricht. Sie können hier so viel Text einfügen, wie Sie möchten.
Stellen Sie eine Frage oder zeigen Sie eine Warnung an, bevor Sie etwas löschen.
Sie können den Aktionstext auch auf "JA, Löschen" setzen und diese Aktion ausführen, indem Sie eine "handleAction" -Stütze übergeben.
Dies erhält einen "handleClose" -Rückruf, mit dem Sie den Dialog schließen können, wenn Ihre Aktion abgeschlossen ist.`,
toast_demo:'Demo Toast',
filter_demo:'Demo filter',
list_page_demo:'List Page Demo mit {count} Zeilen',
forgot_password:'Vergessen passwort',
password_reset:'Passwort zurücksetzen',
password_confirm:'Passwortbestätigung',
registration:'Registrierung'
})
export default messages

View file

@ -0,0 +1,43 @@
import { defineMessages } from 'react-intl'
const messages = defineMessages({
app_name: 'React Most Wanted',
sign_in: 'Sign in',
sign_out: 'Sign out',
sign_up: 'Sign up',
email: 'Email',
username: 'Username',
password: 'Password',
about: 'About',
home: 'Home',
page_not_found: 'Page not found',
settings: 'Settings',
theme: 'Theme',
default: 'Default',
red: 'Red',
green: 'Green',
language: 'Language',
en: 'English',
de: 'German',
ru: 'Russian',
menu: 'Menu',
menu_mini_mode: 'Mini menu',
offline: 'Offline',
demos:'Demos',
dialog_demo:'Demo dialog',
dialog_title:'Dialog title',
dialog_action:'YES, Delete',
dialog_message:`Dialog message. You can put as much text as you want here.
Ask a question or show a warning befor deleting something.
You can also set the action text to somerhing like "YES, Delete" and run that action by passing a "handleAction" prop.
This receives a "handleClose" callback with wich you can close the dialog when your action is done.`,
toast_demo:'Demo toast',
filter_demo:'Demo filter',
list_page_demo:'List Page demo with {count} rows',
forgot_password:'Forgot password',
password_reset:'Password reset',
password_confirm:'Password confirm',
registration:'Registration'
})
export default messages

View file

@ -0,0 +1,19 @@
const locales = [
{
locale: 'en',
messages: import('./en'),
//loadData: import(`@formatjs/intl-relativetimeformat/dist/locale-data/en`),
},
{
locale: 'ru',
messages: import('./ru'),
//loadData: import(`@formatjs/intl-relativetimeformat/dist/locale-data/ru`),
},
{
locale: 'de',
messages: import('./de'),
//loadData: import(`@formatjs/intl-relativetimeformat/dist/locale-data/de`),
},
]
export default locales

View file

@ -0,0 +1,43 @@
import { defineMessages } from 'react-intl'
const messages = defineMessages({
app_name: 'React Most Wanted',
sign_in: 'Вход',
sign_out: 'Выход',
sign_up: 'Зарегистрироваться',
email: 'Эл. адрес',
username: 'Имя пользователя',
password: 'Пароль',
about: 'О нас',
home: 'Главная',
page_not_found: 'Страница не найдена',
settings: 'Настройки',
theme: 'Тема',
default: 'По умолчанию',
red: 'Красная',
green: 'Зелёная',
language: 'Язык',
en: 'English',
de: 'Deutsch',
ru: 'Русский',
menu: 'Меню',
menu_mini_mode: 'Мини меню',
offline: 'Офлайн',
demos:'Демонстрации',
dialog_demo:'Демонстрация диалога',
dialog_title:'Заголовок диалога',
dialog_action:'Да, удалить',
dialog_message:`Диалоговое сообщение. Вы можете поместить сюда сколько угодно текста.
Задайте вопрос или покажите предупреждение перед удалением чего-либо.
Вы также можете задать для текста действия значение somerhing, например «Да, Удалить», и запустить это действие, передав опору «handleAction».
Он получает обратный вызов handleClose, с которым вы можете закрыть диалог, когда ваше действие будет выполнено.`,
toast_demo:'Демонстрация toast',
filter_demo:'Демонстрация фильтра',
list_page_demo:'Демонстрация страницы списка с {count} строками',
forgot_password:'Забыли пароль',
password_reset:'Сброс пароля',
password_confirm:'Подтвердить пароль',
registration:'Регистрация',
})
export default messages

View file

@ -0,0 +1,149 @@
import ChatBubble from '@material-ui/icons/ChatBubble'
import DashboardIcon from '@material-ui/icons/Dashboard'
import ExitToAppIcon from '@material-ui/icons/ExitToApp'
import FilterList from '@material-ui/icons/FilterList'
import GetApp from '@material-ui/icons/GetApp'
import InfoOutlined from '@material-ui/icons/InfoOutlined'
import LanguageIcon from '@material-ui/icons/Language'
import LockIcon from '@material-ui/icons/Lock'
import QuestionAnswer from '@material-ui/icons/QuestionAnswer'
import React from 'react'
import SettingsIcon from '@material-ui/icons/SettingsApplications'
import StyleIcon from '@material-ui/icons/Style'
import Tab from '@material-ui/icons/Tab'
import ViewList from '@material-ui/icons/ViewList'
import Web from '@material-ui/icons/Web'
import allLocales from './locales'
import allThemes from './themes'
import { store, Action } from "reducers";
const getMenuItems = (props) => {
const {
intl,
updateLocale,
locale,
menuContext,
themeContext,
a2HSContext,
auth: authData,
} = props
const { isAuthMenuOpen, setMiniMode } = menuContext
const { themeID, setThemeID } = themeContext
const { setAuth } = authData
const { isAppInstallable, isAppInstalled, deferredPrompt } = a2HSContext
const state = store.getState();
let isAuthorised = state.Auth.get('authenticated');
store.subscribe(_ => {
isAuthorised = store.getState().Auth.get('authenticated');
isAuthorised && setAuth({ isAuthenticated: isAuthorised })
})
setMiniMode(false);
const localeItems = allLocales.map((l) => {
return {
value: undefined,
visible: true,
primaryText: intl.formatMessage({ id: l.locale }),
onClick: () => {
updateLocale(l.locale)
},
leftIcon: <LanguageIcon />,
}
})
const themeItems = allThemes.map((t) => {
return {
value: undefined,
visible: true,
primaryText: intl.formatMessage({ id: t.id }),
onClick: () => {
setThemeID(t.id)
},
leftIcon: <StyleIcon style={{ color: t.color }} />,
}
})
if (isAuthMenuOpen || !isAuthorised) {
return [
{
value: '/',
onClick: isAuthorised
? () => {
setAuth({ isAuthenticated: false })
store.dispatch(Action.LogoutUser())
}
: () => {},
visible: true,
primaryText: isAuthorised
? intl.formatMessage({ id: 'sign_out' })
: intl.formatMessage({ id: 'sign_in' }),
leftIcon: isAuthorised ? <ExitToAppIcon /> : <LockIcon />,
},
]
}
return [
{
value: '/home',
visible: isAuthorised,
primaryText: intl.formatMessage({ id: 'home' }),
leftIcon: <DashboardIcon />,
},
{
value: '/staff_context',
visible: true,
primaryText: 'Staff Context',
leftIcon: <Web />,
},
{
value: '/patient_context',
visible: true,
primaryText: 'Patient Context',
leftIcon: <Web />,
},
{
value: '/division_context',
visible: true,
primaryText: 'Division Context',
leftIcon: <Web />,
},
{ divider: true },
{
primaryText: intl.formatMessage({ id: 'settings' }),
primaryTogglesNestedList: true,
leftIcon: <SettingsIcon />,
nestedItems: [
{
primaryText: intl.formatMessage({ id: 'theme' }),
secondaryText: intl.formatMessage({ id: themeID }),
primaryTogglesNestedList: true,
leftIcon: <StyleIcon />,
nestedItems: themeItems,
},
{
primaryText: intl.formatMessage({ id: 'language' }),
secondaryText: intl.formatMessage({ id: locale }),
primaryTogglesNestedList: true,
leftIcon: <LanguageIcon />,
nestedItems: localeItems,
}
],
},
{
value: null,
visible: isAppInstallable && !isAppInstalled,
onClick: () => {
deferredPrompt.prompt()
},
primaryText: intl.formatMessage({
id: 'install',
defaultMessage: 'Install',
}),
leftIcon: <GetApp />,
},
]
}
export default getMenuItems

View file

@ -0,0 +1,42 @@
/* eslint-disable react/jsx-key */
import React, { lazy } from 'react'
import AuthorizedRoute from 'base-shell/lib/components/AuthorizedRoute/AuthorizedRoute'
import UnauthorizedRoute from 'base-shell/lib/components/UnauthorizedRoute/UnauthorizedRoute'
import { Route } from 'react-router-dom'
const SignIn = lazy(() => import('../pages/SignIn/SignIn'))
const SignUp = lazy(() => import('../pages/SignUp/SignUp'))
const PasswordReset = lazy(() => import('../pages/PasswordReset/PasswordReset'))
const About = lazy(() => import('../pages/About'))
const Home = lazy(() => import('../pages/Home/Home'))
const StaffContext = lazy(() => import('../pages/StaffContext/wrapper'))
const PatientContext = lazy ( () => import('../pages/PatientContext'))
const DivisionContext = lazy(() => import('../pages/DivisionContext'))
const routes = [
<UnauthorizedRoute path="/signin" redirectTo="/" exact component={SignIn} />,
<UnauthorizedRoute path="/auth/staff/login" redirectTo="/home" exact component={SignIn} />,
<UnauthorizedRoute path="/auth/medical/login" redirectTo="/home" exact component={SignIn} />,
<UnauthorizedRoute path="/auth/nurse/login" redirectTo="/home" exact component={SignIn} />,
<UnauthorizedRoute path="/signup" redirectTo="/" exact component={SignUp} />,
<UnauthorizedRoute path="/auth/staff/signup" redirectTo="/home" exact component={SignUp} />,
<UnauthorizedRoute path="/auth/medical/signup" redirectTo="/home" exact component={SignUp} />,
<UnauthorizedRoute path="/auth/nurse/signup" redirectTo="/home" exact component={SignUp} />,
<UnauthorizedRoute
path="/password_reset"
redirectTo="/"
exact
component={PasswordReset}
/>,
<Route path="/about" exact component={About} />,
<AuthorizedRoute path="/home" exact component={Home} />,
<AuthorizedRoute path="/patient_context" exact component={PatientContext} />,
<AuthorizedRoute path="/staff_context" exact component={StaffContext} />,
<AuthorizedRoute path="/division_context" exact component={DivisionContext} />,
]
export default routes

View file

@ -0,0 +1,33 @@
import red from '@material-ui/core/colors/red'
import pink from '@material-ui/core/colors/pink'
import green from '@material-ui/core/colors/green'
const themes = [
{
id: 'default',
},
{
id: 'red',
color: red[500],
source: {
palette: {
primary: red,
secondary: pink,
error: red,
},
},
},
{
id: 'green',
color: green[500],
source: {
palette: {
primary: green,
secondary: red,
error: red,
},
},
},
]
export default themes

View file

@ -0,0 +1,64 @@
//TODO: Break up large constants file into smaller constants file
const PUBLIC_URL = process.env.PUBLIC_URL;
const PHOTO_NAMES = {
cloud: `${PUBLIC_URL}/img/icons/cloud.png`,
earth: `${PUBLIC_URL}/img/icons/earth.png`,
heart: `${PUBLIC_URL}/img/icons/heart.png`,
};
// Router's base (i.e. anything after the domain)
const ROUTER_BASE_NAME = "/";
// Various Server URLs
var SERVER_URL = "http://localhost:8081";
if (process && process.env) {
if (process.env.REACT_APP_SERVER_TYPE === "staging") {
SERVER_URL = "https://backend-staging.herokuapp.com";
}
if (process.env.REACT_APP_SERVER_TYPE === "prod") {
SERVER_URL = "https://backend-prod.herokuapp.com";
}
}
// Editor and Output constants
//View Mode
const CODE_AND_OUTPUT = 0;
const CODE_ONLY = 1;
const OUTPUT_ONLY = 2;
//UI
module.exports = {
// photo names
PHOTO_NAMES,
DEFAULT_PHOTO_NAME: "icecream",
// Router Base Name
ROUTER_BASE_NAME,
//Server Host Name
SERVER_URL,
//User value constants
MINIMUM_USERNAME_LENGTH: 6,
MINIMUM_PASSWORD_LENGTH: 6,
MINIMUM_DISPLAY_NAME_LENGTH: 1,
MAXIMUM_USERNAME_LENGTH: 32,
MAXIMUM_PASSWORD_LENGTH: 128,
MAXIMUM_DISPLAY_NAME_LENGTH: 25,
// UI constants
RING_LOADER_SIZE: 50,
OPEN_PANEL_LEFT: 0,
// editor constants:
CODE_AND_OUTPUT,
CODE_ONLY,
OUTPUT_ONLY,
// UI
//Firebase constants
EMAIL_DOMAIN_NAME: "@fake.com",
};

14
client/src/index.js Normal file
View file

@ -0,0 +1,14 @@
import React from "react";
import { render } from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "reducers";
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);

6
client/src/lib/index.js Normal file
View file

@ -0,0 +1,6 @@
import * as storage from "../lib/storage";
export default {
...storage,
};

49
client/src/lib/storage.js Normal file
View file

@ -0,0 +1,49 @@
const storageAvailable = (type) => {
try {
const storage = window[type];
const x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
return false;
}
};
class CookieStore {
static set(key, value) {
document.cookie = `${key}=${value}`;
}
static get(key) {
if (!document.cookie || document.cookie.length === 0) return undefined;
const cookies = {};
document.cookie.split(';').forEach((cookie) => {
cookies[cookie.split('=')[0].trim()] = cookie.split('=')[1].trim();
});
return cookies[key];
}
static remove(key) {
document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
}
}
class LocalStore {
static set(key, value) {
window.localStorage.setItem(key, value);
}
static get(key, value) {
return window.localStorage.getItem(key);
}
static remove(key) {
window.localStorage.removeItem(key);
}
}
export default (storageAvailable('localStorage') ? LocalStore : CookieStore);

View file

@ -0,0 +1,41 @@
import React, { useEffect, useState } from 'react'
import { useIntl } from 'react-intl'
import Page from 'material-ui-shell/lib/containers/Page'
import Scrollbar from 'material-ui-shell/lib/components/Scrollbar'
import ReactMarkdown from 'react-markdown'
import 'github-markdown-css'
export default function () {
const [source, setSource] = useState(null)
const intl = useIntl()
const loadData = async () => {
const data = await fetch(
'https://raw.githubusercontent.com/TarikHuber/react-most-wanted/master/README.md'
)
const text = await data.text()
setSource(text)
}
useEffect(() => {
loadData()
}, [])
return (
<Page
pageTitle={intl.formatMessage({ id: 'about', defaultMessage: 'About' })}
>
<Scrollbar>
<div style={{ backgroundColor: 'white', padding: 12 }}>
{source && (
<ReactMarkdown
className="markdown-body"
source={source}
escapeHtml
/>
)}
</div>
</Scrollbar>
</Page>
)
}

View file

@ -0,0 +1,93 @@
import React from 'react';
import Page from 'material-ui-shell/lib/containers/Page';
import Typography from '@material-ui/core/Typography';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles({
table: {
minWidth: 650,
},
});
function createDivision(id, name, location, numOfBeds, chargeNurse, ext, status) {
return { id, name, location, numOfBeds, chargeNurse, ext, status };
}
const rows = [
createDivision(2421, 'Orthopedic', 'Bay 6', 6, "Ella", 2 , false),
createDivision(1343, 'Emergency', 'Ward 6', 1, 'Johnson', 1, true),
createDivision(5413, 'Psychiatric', 'Ward 11', 3, "Taggart", 5, true),
createDivision(8193, 'Cardiac', 'Ward 4', 5, "Vaquez", 8, false),
];
function DivisionView () {
const classes = useStyles();
React.useEffect ( () => {
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
mode: 'cors',
};
fetch('/', options)
.then(res => res.json())
.then((data) => {
//we feed the data into the function to create division object
//const dataRows = data.map((division) => {
// return createDivision(division.id, division.name, division.location, division.numOfBeds, division.hargeNurse, division.ext, division.status);
//})
}).catch(e => {
console.error(e);
});
})
return (
<Page pageTitle='Division Context' >
<div style={{padding: '32px'}}>
<Typography variant="h4" style={{ marginBottom: "32px" }}> Division Vizualization</Typography>
<TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>ID </TableCell>
<TableCell> Name</TableCell>
<TableCell>Location</TableCell>
<TableCell>Num of Beds</TableCell>
<TableCell> Charge Nurse </TableCell>
<TableCell> Telephone Ext. </TableCell>
<TableCell> Status </TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow key={row.id}>
<TableCell>{row.id}</TableCell>
<TableCell>{row.name}</TableCell>
<TableCell>{row.location}</TableCell>
<TableCell>{row.numOfBeds}</TableCell>
<TableCell>{row.chargeNurse}</TableCell>
<TableCell>{row.ext}</TableCell>
<TableCell>{row.status ? "Complete" : "Incomplete"}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
</Page>
)
}
export default DivisionView;

View file

@ -0,0 +1,3 @@
import DivisionView from './DivisionView';
export default DivisionView;

View file

@ -0,0 +1,74 @@
import HomeComponent from './HomeComponent';
import React from 'react';
import { connect } from 'react-redux';
import { Action } from 'reducers';
class Home extends React.Component {
saveChanges(newprofile) {
this.props.updateUser(newprofile);
}
componentWillMount() {
if (this.props.isAdmin) {
return this.props.redirectHome();
}
if (this.props.authenticated) {
this.props.fetchUser();
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.isAdmin) {
return nextProps.redirectHome();
}
if (nextProps.updated) {
setTimeout(() => {
this.props.updateDone();
}, 1000);
}
}
render() {
return (
<HomeComponent
/>
);
}
}
const mapStateToProps = (state) => {
const profile = {
name: ''
};
if (state.User.get('fetchSuccess')) {
const User = state.User.get('profile');
profile.name = `${User.firstName} ${User.lastName}`;
}
return {
profile,
fetchSuccess: state.User.get('fetchSuccess'),
updated: state.User.get('updated'),
updateSuccess: state.User.get('updateSuccess'),
updateError: state.User.get('error'),
authenticated: state.Auth.get('authenticated'),
isAdmin: state.Auth.get('isAdmin'),
};
};
const mapDispatchToProps = dispatch => ({
fetchUser: () => {
dispatch(Action.FetchUser());
},
updateUser: (newprofile) => {
dispatch(Action.UpdateUser(newprofile));
},
updateDone: () => {
dispatch(Action.UserUpdateDone());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(Home);

View file

@ -0,0 +1,21 @@
import Page from 'material-ui-shell/lib/containers/Page'
import React from 'react'
import Scrollbar from 'material-ui-shell/lib/components/Scrollbar/Scrollbar'
import { useIntl } from 'react-intl'
const HomePage = () => {
const intl = useIntl()
const user = "Bob";
return (
<Page pageTitle={'Welcome Bob'}>
<Scrollbar
style={{ height: '100%', width: '100%', display: 'flex', flex: 1 }}
>
Something here
</Scrollbar>
</Page>
)
}
export default HomePage

View file

@ -0,0 +1,77 @@
import React from "react";
import { Link } from "react-router-dom";
import { makeStyles } from "@material-ui/core/styles";
import logo from "../../pictures/logo.png";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
const useStyles = makeStyles((theme) => ({
link: {
margin: theme.spacing(2, 2, 1),
},
}));
const Landing = () => {
const classes = useStyles();
return (
<div
style={{
position: "absolute",
left: 0,
top: 0,
bottom: 0,
right: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
}}
>
<img src={logo} alt="logo" style={{ width: "25%" }} />
<Typography variant="h5" style={{ paddingBottom: "20px", textAlign: "center" }}>
Welcome to the HealthGeeks Patient-Management-System
</Typography>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
}}
>
<Link
className={classes.link}
to="/auth/staff/login"
style={{ textDecoration: "none" }}
>
<Button fullWidth variant="contained" color="primary">
Staff Member
</Button>
</Link>
<Link
className={classes.link}
to="/auth/medical/login"
style={{ textDecoration: "none" }}
>
<Button fullWidth variant="contained" color="primary">
Medical Staff
</Button>
</Link>
<Link
className={classes.link}
to="/auth/nurse/login"
style={{ textDecoration: "none" }}
>
<Button fullWidth variant="contained" color="primary">
Charge Nurse
</Button>
</Link>
</div>
</div>
);
};
export default Landing;

View file

@ -0,0 +1 @@
export { default } from './LandingPage'

View file

@ -0,0 +1,59 @@
import Button from '@material-ui/core/Button'
import Home from '@material-ui/icons/Home'
import Page from 'material-ui-shell/lib/containers/Page'
import Paper from '@material-ui/core/Paper'
import React from 'react'
import Typography from '@material-ui/core/Typography'
import { makeStyles } from '@material-ui/core/styles'
import { useIntl } from 'react-intl'
const useStyles = makeStyles((theme) => ({
icon: {
width: 192,
height: 192,
color: theme.palette.secondary.main,
},
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: `100%`,
},
paper: {
backgroundColor: theme.palette.background.default,
margin: 0,
height: `calc(100vh - 64px)`,
},
button: {
marginTop: 20,
},
}))
const PageNotFound = () => {
const intl = useIntl()
const classes = useStyles()
return (
<Page pageTitle={intl.formatMessage({ id: 'page_not_found' })}>
<Paper className={classes.paper}>
<div className={classes.container}>
<Typography variant="h4">404</Typography>
<Typography variant="subtitle1">
{intl.formatMessage({ id: 'page_not_found' })}
</Typography>
<Button
color="secondary"
aria-label="home"
href="/"
className={classes.button}
>
<Home />
</Button>
</div>
</Paper>
</Page>
)
}
export default PageNotFound

View file

@ -0,0 +1,115 @@
import Button from '@material-ui/core/Button'
import Page from 'material-ui-shell/lib/containers/Page'
import Paper from '@material-ui/core/Paper'
import React, { useState } from 'react'
import TextField from '@material-ui/core/TextField'
import Typography from '@material-ui/core/Typography'
import { makeStyles } from '@material-ui/core/styles'
import { useHistory } from 'react-router-dom'
import { useIntl } from 'react-intl'
const useStyles = makeStyles((theme) => ({
paper: {
width: 'auto',
marginLeft: theme.spacing(3),
marginRight: theme.spacing(3),
[theme.breakpoints.up(620 + theme.spacing(6))]: {
width: 400,
marginLeft: 'auto',
marginRight: 'auto',
},
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(
3
)}px`,
},
avatar: {
margin: theme.spacing(1),
width: 192,
height: 192,
color: theme.palette.secondary.main,
},
form: {
marginTop: theme.spacing(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: `100%`,
},
}))
const SignUp = () => {
const classes = useStyles()
const intl = useIntl()
const history = useHistory()
const [username, setUsername] = useState('')
function handleSubmit(event) {
event.preventDefault()
history.replace('/signin')
}
return (
<Page
pageTitle={intl.formatMessage({
id: 'password_reset',
defaultMessage: 'Password reset',
})}
onBackClick={() => {
history.goBack()
}}
>
<Paper className={classes.paper} elevation={6}>
<div className={classes.container}>
<Typography component="h1" variant="h5">
{intl.formatMessage({
id: 'password_reset',
defaultMessage: 'Password reset',
})}
</Typography>
<form className={classes.form} onSubmit={handleSubmit} noValidate>
<TextField
value={username}
onInput={(e) => setUsername(e.target.value)}
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label={intl.formatMessage({
id: 'email',
defaultMessage: 'E-Mail',
})}
name="email"
autoComplete="email"
autoFocus
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
{intl.formatMessage({
id: 'password_reset',
defaultMessage: 'Reset Password',
})}
</Button>
</form>
</div>
</Paper>
</Page>
)
}
export default SignUp

View file

@ -0,0 +1,5 @@
import RegisterPatientButton from './RegisterPatientButton';
import ConsultFileButton from './ConsultFileButton';
import RequestToAdmitButton from './RequestToAdmitButton'
export { RegisterPatientButton, ConsultFileButton, RequestToAdmitButton };

View file

@ -0,0 +1,28 @@
import React from 'react';
import { Button } from '@material-ui/core';
import { useQuestions } from 'material-ui-shell/lib/providers/Dialogs/Question';
import ConsultFileForm from './ConsultFileForm'
//we should have medication somewhere inside here
function ConsultFileButton(props){
const { openDialog, setProcessing } = useQuestions();
return (
<Button
variant="contained"
color="default"
onClick={() => {
openDialog({
title: 'Consult File',
message: <ConsultFileForm firstname= {props.firstname} lastname= {props.lastname} />,
action: null,
})
}}
>
Consult File
</Button>
)
}
export default ConsultFileButton;

View file

@ -0,0 +1,295 @@
import React from 'react';
import TextField from '@material-ui/core/TextField';
import MenuItem from '@material-ui/core/MenuItem';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
const useStyles = makeStyles((theme) => ({
root: {
'& .MuiTextField-root': {
margin: theme.spacing(2),
width: '25ch',
},
},
selectEmpty: {
marginTop: theme.spacing(1),
width: '25ch',
},
}));
function ConsultFileForm (props){
const classes = useStyles();
const [gender, setGender] = React.useState('');
const [maritalStatus, setMaritalStatus] = React.useState('');
const [province, setProvince] = React.useState('');
const [state, setState] = React.useState({
firstname: '',
lastname: '',
telephone: '',
addressLineOne: '',
city: '',
provice: '',
postalCode: '',
dateOfBirth: '',
gender: '',
maritalStatus: '',
externalDoctor: '',
divisionID: '',
//admitted
//requested
});
React.useEffect( () => {
getData(); //we get data right after render
})
//GEt from backend
function getData(){
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
mode: 'cors',
};
fetch('/ ', options)
.then(res => res.json())
.then((data) => {
//What happens after calling express goes here
setState( {
firstname: data.firstname,
lastname: data.lastname,
telephone: data.telephone,
addressLineOne: data.addressLineOne,
city: data.city,
provice: data.province,
postalCode: data.postalCode,
dateOfBirth: data.dateOfBirth,
gender: data.gender,
maritalStatus: data.maritalStatus,
externalDoctor: data.externalDoctor,
divisionID: data.divisionID,
})
}).catch(e => {
//console.error(e);
});
}
const handleChange = (e) => {
const value = e.target.value;
setState({
...state,
[e.target.id]: value
});
}
const handleGender = (e) => {
setGender(e.target.value)
}
const handleMaritalStatus = (e) => {
setMaritalStatus(e.target.value)
}
const handleProvinceChange = (e) => {
setProvince(e.target.value)
}
const [editable, setEditable] = React.useState(false);
const [buttonValue, setButtonValue] = React.useState('Edit');
function saveOrEdit(e){
if ( editable){
setButtonValue( 'Edit');
setEditable(false);
}else{
saveData(e);
setButtonValue('Save');
setEditable(true);
}
}
//Send Data to Backend
function saveData(e){
e.preventDefault();
var data = { // what to send goes here
}
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
mode: 'cors',
body: JSON.stringify(data)
};
fetch('/ ', options)
.then(res => res.json())
.then((data) => {
//What happens after calling express goes here
}).catch(e => {
//console.error(e);
});
}
return(
<div>
<form className={classes.root} autoComplete="off">
<div>
<TextField
disabled = {!editable}
required
label="First Name"
value={state.firstname}
id="firstname"
onChange={handleChange}
/>
<TextField
disabled={!editable}
required
label="Last Name"
value={state.lastname}
id="lastname"
onChange={handleChange}
/>
</div>
<div>
<TextField
disabled={!editable}
required
label="Telephone Number"
value={state.telephone}
id="telephone"
onChange={handleChange}
/>
<TextField
required
disabled={!editable}
type="date"
label="Date of Birth"
value={state.dateOfBirth}
id="dateOfBirth"
onChange={handleChange}
InputLabelProps={{
shrink: true,
}}
/>
</div>
<div>
<TextField
disabled={!editable}
required
label="Address Line One"
value={state.addressLineOne}
id="addressLineOne"
onChange={handleChange}
/>
<TextField
disabled={!editable}
required
label="City"
value={state.city}
id="city"
onChange={handleChange}
/>
</div>
<div>
<TextField
disabled={!editable}
select
id="userProvince"
value={province}
onChange={handleProvinceChange}
label="Province"
>
<MenuItem value={'alberta'}>Alberta </MenuItem>
<MenuItem value={'britishcolumbia'}>British Columbia</MenuItem>
<MenuItem value={'newbrunswick'}>New Brunswick</MenuItem>
<MenuItem value={'newfoundlandandlabrador'}>Newfoundland and Labrador</MenuItem>
<MenuItem value={'novascotia'}>Nova Scotia</MenuItem>
<MenuItem value={'princeedwardisland'}>Prince Edward Island</MenuItem>
<MenuItem value={'ontario'}>Ontario</MenuItem>
<MenuItem value={'quebec'}>Quebec</MenuItem>
<MenuItem value={'saskatchewan'}>Saskatchewan</MenuItem>
</TextField>
<TextField
required
disabled={!editable}
label="Postal Code"
value={state.postalCode}
id="postalCode"
onChange={handleChange}
/>
</div>
<div>
<TextField
select
disabled={!editable}
required
label="Gender"
value={gender}
onChange={handleGender}
className={classes.selectEmpty}
>
<MenuItem value={'male'}> Male </MenuItem>
<MenuItem value={'female'}> Female </MenuItem>
<MenuItem value={'other'}> Other</MenuItem>
<MenuItem value={'none'}>Prefer Not to Identify</MenuItem>
</TextField>
<TextField
select
disabled={!editable}
required
label="Marital Status"
value={maritalStatus}
onChange={handleMaritalStatus}
className={classes.selectEmpty}
>
<MenuItem value={'married'}> Married </MenuItem>
<MenuItem value={'single'}> Single </MenuItem>
<MenuItem value={'commonlaw'}> Common Law</MenuItem>
</TextField>
</div>
<div>
<TextField
disabled={!editable}
required
label="External Doctor"
value={state.externalDoctor}
id="telephone"
onChange={handleChange}
/>
<TextField
disabled={!editable}
label="Division ID"
value={state.divisionID}
id="divisionid"
onChange={handleChange}
/>
</div>
<Button style={{ float: "right", marginRight: "2rem" }}
variant="contained"
color="primary"
onClick={(e) => saveOrEdit(e)}>
{buttonValue}
</Button>
</form>
</div>
)
}
export default ConsultFileForm;

View file

@ -0,0 +1,114 @@
import React from 'react';
import Page from 'material-ui-shell/lib/containers/Page';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import { RegisterPatientButton, ConsultFileButton, RequestToAdmitButton } from './Buttons';
const useStyles = makeStyles({
table: {
minWidth: 650,
},
});
function createData(id, firstname, lastname, division, admitted) {
return { id, firstname, lastname, division, admitted };
}
const rows = [
createData(2421, 'James', 'Iger', 6, false),
createData( 1343, 'Bob', 'Andrews', 1, true),
createData(5413, 'John', 'Dunderson', 3, true),
createData(8193, 'Sam', 'Ellis', 5, false),
];
function PatientList() {
const classes = useStyles();
React.useEffect( () => {
getData();
})
function getData (){
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
mode: 'cors',
};
fetch('/', options)
.then(res => res.json())
.then((data) => {
//we feed the data into the function to create patient object
const dataRows = data.map ( (patient) => {
return createData( patient.id, patient.firstname, patient.lastname, patient.division, patient.admitted);
})
}).catch(e => {
console.error(e);
});
}
return (
<Page pageTitle='Summary' >
<div style={{padding: '32px'}}>
<Typography variant="h3" style={{marginBottom: '16px'}}> Patient List </Typography>
<RegisterPatientButton />
<TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>ID </TableCell>
<TableCell>First Name</TableCell>
<TableCell>Last Name</TableCell>
<TableCell>Division</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow key={row.id}>
<TableCell>{row.id}</TableCell>
<TableCell>{row.firstname}</TableCell>
<TableCell>{row.lastname}</TableCell>
<TableCell>{row.division}</TableCell>
<TableCell>
<ConsultFileButton
firstname = {row.firstname}
lastname = {row.lastname}
/>
</TableCell>
<TableCell >
<div hidden={row.admitted}>
<RequestToAdmitButton
firstname={row.firstname}
lastname={row.lastname}
/>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
</Page>
)
}
export default PatientList;

View file

@ -0,0 +1,29 @@
import React from 'react';
import { Button } from '@material-ui/core';
import { useQuestions } from 'material-ui-shell/lib/providers/Dialogs/Question';
import RegistrationForm from './RegistrationForm';
function RegisterPatientButton (props) {
const { openDialog, setProcessing } = useQuestions();
return (
<Button
variant="contained"
color="primary"
style={{marginBottom: '16px'}}
onClick={() => {
openDialog({
title: 'Register Patient',
message: <RegistrationForm/>,
action: null,
})
}}
>
Register Patient
</Button>
)
}
export default RegisterPatientButton;

View file

@ -0,0 +1,229 @@
import React from 'react';
import TextField from '@material-ui/core/TextField';
import { makeStyles } from '@material-ui/core/styles';
import MenuItem from '@material-ui/core/MenuItem';
import Button from '@material-ui/core/Button';
const useStyles = makeStyles((theme) => ({
root: {
'& .MuiTextField-root': {
margin: theme.spacing(2),
width: '25ch',
},
},
selectEmpty: {
marginTop: theme.spacing(1),
width: '25ch',
},
}));
function RegistrationForm() {
const classes = useStyles();
const [gender, setGender] = React.useState('');
const [maritalStatus, setMaritalStatus] = React.useState('');
const [province, setProvince] = React.useState('alberta');
const [state, setState] = React.useState({
firstname: '',
lastname: '',
telephone: '',
addressLineOne: '',
city: '',
provice: '',
postalCode: '',
dateOfBirth: '',
gender : '',
maritalStatus: '',
externalDoctor: '',
divisionID: '',
//admitted
//requested
});
const handleChange = (e) => {
const value = e.target.value;
setState( {
...state,
[e.target.id] : value
});
}
const handleGender = (e) => {
setGender(e.target.value)
}
const handleMaritalStatus = (e) => {
setMaritalStatus(e.target.value)
}
const handleProvinceChange = (e) => {
setProvince(e.target.value)
}
//We call the API HERE
const register = (e) => {
e.preventDefault();
var data = { // what to send goes here
}
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
mode: 'cors',
body: JSON.stringify(data)
};
fetch('/ ', options)
.then(res => res.json())
.then((data) => {
console.log(data)
//What happens after calling express goes here
}).catch(e => {
console.error(e);
});
}
return (
<div>
<form className={classes.root} autoComplete="off">
<div>
<TextField
required
label="First Name"
value = {state.firstname}
id = "firstname"
onChange = {handleChange}
/>
<TextField
required
label="Last Name"
value={state.lastname}
id="lastname"
onChange={handleChange}
/>
</div>
<div>
<TextField
required
label="Telephone Number"
value={state.telephone}
id="telephone"
onChange={handleChange}
/>
<TextField
required
type="date"
label="Date of Birth"
value={state.dateOfBirth}
id="dateOfBirth"
onChange={handleChange}
InputLabelProps={{
shrink: true,
}}
/>
</div>
<div>
<TextField
required
label="Address Line One"
value={state.addressLineOne}
id="addressLineOne"
onChange={handleChange}
/>
<TextField
required
label="City"
value={state.city}
id="city"
onChange={handleChange}
/>
<TextField
select
id="userProvince"
value={province}
onChange={handleProvinceChange}
label="Province"
>
<MenuItem value={'alberta'}>Alberta </MenuItem>
<MenuItem value={'britishcolumbia'}>British Columbia</MenuItem>
<MenuItem value={'newbrunswick'}>New Brunswick</MenuItem>
<MenuItem value={'newfoundlandandlabrador'}>Newfoundland and Labrador</MenuItem>
<MenuItem value={'novascotia'}>Nova Scotia</MenuItem>
<MenuItem value={'princeedwardisland'}>Prince Edward Island</MenuItem>
<MenuItem value={'ontario'}>Ontario</MenuItem>
<MenuItem value={'quebec'}>Quebec</MenuItem>
<MenuItem value={'saskatchewan'}>Saskatchewan</MenuItem>
</TextField>
<TextField
required
label="Postal Code"
value={state.postalCode}
id="postalCode"
onChange={handleChange}
/>
</div>
<div>
<TextField
select
required
label="Gender"
value={ gender}
onChange={ handleGender }
className={classes.selectEmpty}
>
<MenuItem value={'male'}> Male </MenuItem>
<MenuItem value={'female'}> Female </MenuItem>
<MenuItem value={'other'}> Other</MenuItem>
<MenuItem value={'none'}>Prefer Not to Identify</MenuItem>
</TextField>
<TextField
select
required
label="Marital Status"
value={maritalStatus}
onChange={handleMaritalStatus}
className={classes.selectEmpty}
>
<MenuItem value={'married'}> Married </MenuItem>
<MenuItem value={'single'}> Single </MenuItem>
<MenuItem value={'commonlaw'}> Common Law</MenuItem>
</TextField>
</div>
<div>
<TextField
required
label="External Doctor"
value={state.externalDoctor}
id="telephone"
onChange={handleChange}
/>
<TextField
label="Division ID"
value={state.divisionID}
id="divisionid"
onChange={handleChange}
/>
</div>
<Button style={{ float: "right", marginRight: "3rem" }}
variant="contained"
color="primary"
onClick = { (e) => register(e)}>
Register
</Button>
</form>
</div>
)
}
export default RegistrationForm;

View file

@ -0,0 +1,76 @@
import React from 'react';
import { Button } from '@material-ui/core';
import Snackbar from '@material-ui/core/Snackbar';
import MuiAlert from '@material-ui/lab/Alert';
import { makeStyles } from '@material-ui/core/styles';
function Alert(props) {
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
'& > * + *': {
marginTop: theme.spacing(2),
},
},
}));
function RequestToAdmitButton() {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const handleClick = (e) => {
//send to database
e.preventDefault();
var data = { // what to send goes here
}
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
mode: 'cors',
body: JSON.stringify(data)
};
//create a request for admission here
fetch('/ ', options)
.then(res => res.json())
.catch(e => {
//console.error(e);
});
setOpen(true);
};
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
setOpen(false);
}
return (
<div className={classes.root}>
<Button
variant="contained"
color="default"
onClick={ handleClick } >
Request To Admit
</Button>
<Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="success">
Successfully Requested Patient for Admission
</Alert>
</Snackbar>
</div>
)
}
export default RequestToAdmitButton;

View file

@ -0,0 +1,3 @@
import PatientList from './PatientList';
export default PatientList;

View file

@ -0,0 +1,46 @@
import React from 'react';
import { connect } from 'react-redux';
import { replace } from 'react-router-redux';
import { Action } from '../../reducers';
import SignInComponent from './SignInComponent';
class Login extends React.Component {
handleLogin(email, password) {
this.props.login(email, password);
}
componentWillMount() {
if (this.props.authenticated) {
this.props.redirectHome();
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.authenticated) {
this.props.redirectHome();
}
}
render() {
return <SignInComponent onsubmit={this.handleLogin.bind(this)} error={this.props.error} />;
}
}
const mapStateToProps = state => ({
error: state.Auth.get('error'),
isAdmin: state.Auth.get('isAdmin'),
timestamp: state.Auth.get('timestamp'),
authenticated: state.Auth.get('authenticated'),
});
const mapDispatchToProps = dispatch => ({
login: (email, password) => {
dispatch(Action.LoginUser(email, password));
},
redirectHome: () => {
dispatch(replace('/home'));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Login);

View file

@ -0,0 +1,179 @@
import Button from "@material-ui/core/Button";
import Page from "material-ui-shell/lib/containers/Page";
import Paper from "@material-ui/core/Paper";
import React, { useState } from "react";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { Link } from "react-router-dom";
import { makeStyles } from "@material-ui/core/styles";
import { useAuth } from "base-shell/lib/providers/Auth";
import { useHistory } from "react-router-dom";
import { useIntl } from "react-intl";
import { useMenu } from "material-ui-shell/lib/providers/Menu";
import { getDisplayUserType } from "../../utils/displayUserType";
const useStyles = makeStyles((theme) => ({
paper: {
width: "auto",
marginLeft: theme.spacing(3),
marginRight: theme.spacing(3),
[theme.breakpoints.up(620 + theme.spacing(6))]: {
width: 400,
marginLeft: "auto",
marginRight: "auto",
},
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(
3
)}px`,
},
avatar: {
margin: theme.spacing(1),
width: 192,
height: 192,
color: theme.palette.secondary.main,
},
form: {
marginTop: theme.spacing(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
container: {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: `100%`,
},
}));
const SignInComponent = (props) => {
const classes = useStyles();
const intl = useIntl();
const history = useHistory();
const userType = history.location.pathname.split("/")[2]; // pathname: /auth/staff/login
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [disableForm, setDisableForm] = useState(false);
const { setAuthMenuOpen } = useMenu();
const { setAuth } = useAuth();
const displayUserType = getDisplayUserType(userType);
function handleSubmit(event) {
event.preventDefault();
// authenticate({
// displayName: "User",
// email: email,
// });
const { onsubmit } = props;
if (!disableForm) {
setDisableForm(true);
onsubmit(email, password);
setAuth({ isAuthenticated: true });
setAuthMenuOpen(false);
}
}
const authenticate = (user) => {
setAuth({ isAuthenticated: true, ...user });
setAuthMenuOpen(false);
let _location = history.location;
let _route = "/home";
if (_location.state && _location.state.from) {
_route = _location.state.from.pathname;
history.push(_route);
} else {
history.push(_route);
}
};
return (
<Page pageTitle={intl.formatMessage({ id: "sign_in" })}>
<Paper className={classes.paper} elevation={6}>
<div className={classes.container}>
<Typography
component="h1"
variant="h5"
style={{ textTransform: "capitalize" }}
>
{`${displayUserType} ${intl.formatMessage({ id: "sign_in" })}`}
</Typography>
<form className={classes.form} onSubmit={handleSubmit} noValidate>
<TextField
disabled={disableForm}
value={email}
onInput={(e) => setEmail(e.target.value)}
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label={intl.formatMessage({
id: "email",
defaultMessage: "E-Mail",
})}
name="email"
autoComplete="email"
autoFocus
/>
<TextField
disabled={disableForm}
value={password}
onInput={(e) => setPassword(e.target.value)}
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label={intl.formatMessage({ id: "password" })}
type="password"
id="password"
autoComplete="current-password"
/>
{props.error ? (
<span>
<b>Error</b>: {props.error}
</span>
) : (
<span>&nbsp;</span>
)}
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
{intl.formatMessage({ id: "sign_in" })}
</Button>
</form>
<div
style={{
display: "flex",
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
}}
>
<Link to="/password_reset">
{intl.formatMessage({ id: "forgot_password" })}?
</Link>
<Link to={`/auth/${userType}/signup`}>
{intl.formatMessage({ id: "registration" })}
</Link>
</div>
</div>
</Paper>
</Page>
);
};
export default SignInComponent;

View file

@ -0,0 +1,53 @@
import React from 'react';
import { connect } from 'react-redux';
import SignUpComponent from './SignUpComponent';
import { Action } from 'reducers';
class SignUpContainer extends React.Component {
constructor(props) {
super(props);
this.createUser = this.createUser.bind(this);
}
createUser(profile) {
this.props.registerUser(profile);
}
componentWillReceiveProps(nextProps) {
if (nextProps.registered) {
setTimeout(() => {
this.props.registerDone();
}, 1000);
}
}
render() {
return (
<SignUpComponent
createUser={this.createUser}
created={this.props.registered}
createSuccess={this.props.registerSuccess}
createError={this.props.error}
/>
);
}
}
const mapStateToProps = state => ({
user: state.Registration.get('user'),
error: state.Registration.get('error'),
registered: state.Registration.get('registered'),
registerSuccess: state.Registration.get('registerSuccess'),
});
const mapDispatchToProps = dispatch => ({
registerUser: (newuser) => {
dispatch(Action.RegisterUser(newuser));
},
registerDone: () => {
dispatch(Action.registerDone());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(SignUpContainer);

View file

@ -0,0 +1,209 @@
import Button from "@material-ui/core/Button";
import Page from "material-ui-shell/lib/containers/Page";
import Paper from "@material-ui/core/Paper";
import React, { useState } from "react";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";
import { useAuth } from "base-shell/lib/providers/Auth";
import { useHistory } from "react-router-dom";
import { useIntl } from "react-intl";
import { useMenu } from "material-ui-shell/lib/providers/Menu";
import { getDisplayUserType } from "../../utils/displayUserType";
const useStyles = makeStyles((theme) => ({
paper: {
width: "auto",
marginLeft: theme.spacing(3),
marginRight: theme.spacing(3),
[theme.breakpoints.up(620 + theme.spacing(6))]: {
width: 400,
marginLeft: "auto",
marginRight: "auto",
},
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(
3
)}px`,
},
avatar: {
margin: theme.spacing(1),
width: 192,
height: 192,
color: theme.palette.secondary.main,
},
form: {
marginTop: theme.spacing(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
container: {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: `100%`,
},
}));
const SignUp = (props) => {
const classes = useStyles();
const intl = useIntl();
const history = useHistory();
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [userEmail, setUserEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [disableForm, setDisableForm] = useState(false);
const userType = history.location.pathname.split("/")[2]; // pathname ex: /auth/staff/login
const { setAuthMenuOpen } = useMenu();
const { setAuth } = useAuth();
const displayUserType = getDisplayUserType(userType);
function handleSubmit(event) {
event.preventDefault();
if (!disableForm) {
if (
firstName &&
lastName &&
userEmail &&
password &&
password.length >= 4
) {
setDisableForm(true);
props.createUser({
firstName,
lastName,
email:userEmail,
password,
confirmPassword,
});
}
}
setAuth({ isAuthenticated: true });
setAuthMenuOpen(false);
}
return (
<Page
pageTitle={intl.formatMessage({
id: "sign_up",
defaultMessage: " Sign up",
})}
onBackClick={() => {
history.goBack();
}}
>
<Paper className={classes.paper} elevation={6}>
<div className={classes.container}>
<Typography component="h1" variant="h5">
{`${displayUserType} ${intl.formatMessage({
id: "sign_up",
defaultMessage: "Sign up",
})}`}
</Typography>
<form className={classes.form} onSubmit={handleSubmit}>
<TextField
disabled={disableForm}
value={firstName}
onInput={(e) => setFirstName(e.target.value)}
variant="outlined"
margin="normal"
required
fullWidth
id="first-name"
label="First Name"
name="first-name"
autoComplete="first-name"
/>
<TextField
disabled={disableForm}
value={lastName}
onInput={(e) => setLastName(e.target.value)}
variant="outlined"
margin="normal"
required
fullWidth
id="last-name"
label="Last Name"
name="last-name"
autoComplete="last-name"
/>
<TextField
disabled={disableForm}
value={userEmail}
onInput={(e) => setUserEmail(e.target.value)}
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label={intl.formatMessage({
id: "email",
defaultMessage: "E-Mail",
})}
name="email"
autoComplete="email"
/>
<TextField
disabled={disableForm}
value={password}
onInput={(e) => setPassword(e.target.value)}
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label={intl.formatMessage({
id: "password",
defaultMessage: "Password",
})}
type="password"
id="password"
autoComplete="current-password"
/>
<TextField
disabled={disableForm}
value={confirmPassword}
onInput={(e) => setConfirmPassword(e.target.value)}
variant="outlined"
margin="normal"
required
fullWidth
name="password_confirm"
label={intl.formatMessage({
id: "password_confirm",
defaultMessage: "Confirm Password",
})}
type="password"
id="password_confirm"
autoComplete="current-password"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
{intl.formatMessage({ id: "sign_up", defaultMessage: "Sign up" })}
</Button>
</form>
</div>
</Paper>
</Page>
);
};
export default SignUp;

View file

@ -0,0 +1,100 @@
import React from 'react';
import Page from 'material-ui-shell/lib/containers/Page';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import AdmitButton from './AdmitButton';
const useStyles = makeStyles({
table: {
minWidth: 650,
},
});
function createData(id, firstname, lastname) {
return { id, firstname, lastname };
}
const rows = [
createData(2421, 'James', 'Iger' ),
createData(1343, 'Bob', 'Andrews'),
createData(5413, 'John', 'Dunderson'),
createData(8193, 'Sam', 'Ellis' ),
];
function AdmissionRequests() {
const classes = useStyles();
React.useEffect( () => {
getData();
})
function getData(){
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
mode: 'cors',
};
fetch('/', options)
.then(res => res.json())
.then((data) => {
//we feed the data into the function to create patient object
const dataRows = data.map((patient) => {
return createData(patient.id, patient.firstname, patient.lastname );
})
}).catch(e => {
console.error(e);
});
}
return (
<div>
<Page pageTitle='Summary' >
<Typography variant="h4" style={{marginBottom: 16}}> Admission Requests </Typography>
<TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>ID </TableCell>
<TableCell>First Name</TableCell>
<TableCell>Last Name</TableCell>
<TableCell> Action </TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow key={row.id}>
<TableCell>{row.id}</TableCell>
<TableCell>{row.firstname}</TableCell>
<TableCell>{row.lastname}</TableCell>
<TableCell >
<AdmitButton
firstname = {row.firstname}
lastname = {row.lastname}>
</AdmitButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Page>
</div>
)
}
export default AdmissionRequests;

View file

@ -0,0 +1,27 @@
import React from 'react';
import { useQuestions } from 'material-ui-shell/lib/providers/Dialogs/Question';
import { Button } from '@material-ui/core';
import AdmitForm from './AdmitForm';
function AdmitButton(props) {
const { openDialog, setProcessing } = useQuestions();
return (
<Button
variant="contained"
color="default"
onClick={() => {
openDialog({
title: 'Consult File',
message: <AdmitForm firstname={props.firstname} lastname={props.lastname}/> ,
action: null,
})
}}
>
Admit Patient
</Button>
)
}
export default AdmitButton;

View file

@ -0,0 +1,137 @@
import React from 'react';
import TextField from '@material-ui/core/TextField';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
const useStyles = makeStyles((theme) => ({
root: {
'& .MuiTextField-root': {
margin: theme.spacing(2),
width: '25ch',
},
},
selectEmpty: {
marginTop: theme.spacing(1),
width: '25ch',
},
}));
function AdmitForm(props) {
const classes = useStyles();
const [state, setState] = React.useState({
firstname: props.firstname,
lastname: props.lastname,
division: '',
localdoctor: '',
roomnumber: '',
bednumber: '',
privateinsurancenumber: '',
});
const handleChange = (e) => {
const value = e.target.value;
setState({
...state,
[e.target.id]: value
});
}
//We call the API HERE
const admit = (e) => {
e.preventDefault();
var data = { // what to send goes here
}
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
mode: 'cors',
body: JSON.stringify(data)
};
fetch('/ ', options)
.then(res => res.json())
.then((data) => {
console.log(data)
//What happens after calling express goes here
}).catch(e => {
console.error(e);
});
}
return (
<div>
<form className={classes.root} autoComplete="off">
<div>
<TextField
disabled
label="First Name"
value={state.firstname}
/>
<TextField
disabled
label="Last Name"
value={state.lastname}
/>
</div>
<div>
<TextField
required
label="Division"
value={state.division}
id="division"
onChange={handleChange}
/>
<TextField
required
label="Local Doctor"
value={state.localdoctor}
id="localdoctor"
onChange={handleChange}
/>
</div>
<div>
<TextField
required
label="Room Number"
value={state.roomnumber}
id="roomnumber"
onChange={handleChange}
/>
<TextField
required
label="Bed Number"
value={state.bednumber}
id="bednumber"
onChange={handleChange}
/>
</div>
<div>
<TextField
label="Private Insurance Number"
value={state.privateinsurancenumber}
id="privateinsurancenumber"
onChange={handleChange}
helperText= "Optional"
/>
</div>
<Button style={{ float: "right", marginRight: "3rem" }}
variant="contained"
color="primary"
onClick={(e) => admit(e)}>
Confirm Admission
</Button>
</form>
</div>
)
}
export default AdmitForm;

View file

@ -0,0 +1,113 @@
import React from 'react';
import TextField from '@material-ui/core/TextField';
import { makeStyles } from '@material-ui/core/styles';
import { Button } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
'& .MuiTextField-root': {
margin: theme.spacing(2),
width: '25ch',
},
},
selectEmpty: {
marginTop: theme.spacing(1),
width: '25ch',
},
}));
function DischargeForm() {
const classes = useStyles();
const [state, setState] = React.useState({
id: '',
firstname: '',
lastname: '',
division: '',
});
const handleChange = (e) => {
const value = e.target.value;
setState({
...state,
[e.target.id]: value
});
}
const discharge = (e) => {
e.preventDefault();
var data = { // what to send goes here
}
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
mode: 'cors',
body: JSON.stringify(data)
};
fetch('/ ', options)
.then(res => res.json())
.then((data) => {
console.log(data)
//What happens after calling express goes here
}).catch(e => {
console.error(e);
});
}
return (
<div>
<form className={classes.root} autoComplete="off">
<div>
<TextField
required
label="ID"
value={state.id}
id="id"
onChange={handleChange}
/>
<TextField
required
label="Division"
value={state.division}
id="division"
onChange={handleChange}
/>
</div>
<div>
<TextField
required
label="First Name"
value={state.firstname}
id="firstname"
onChange={handleChange}
/>
<TextField
required
label="First Name"
value={state.firstname}
id="firstname"
onChange={handleChange}
/>
</div>
<Button style={{ float: "right", marginRight: "3rem" }}
variant="contained"
color="primary"
onClick={(e) => discharge(e)}
value = "Confirm Discharge">
Confirm Discharge
</Button>
</form>
</div>
)
}
export default DischargeForm;

View file

@ -0,0 +1,27 @@
import React from 'react';
import { useQuestions } from 'material-ui-shell/lib/providers/Dialogs/Question';
import { Button } from '@material-ui/core';
import DischargeForm from './DischargeForm';
function DischargePatient(props) {
const { openDialog, setProcessing } = useQuestions();
return (
<Button
variant="contained"
color="secondary"
onClick={() => {
openDialog({
title: 'Consult File',
message: <DischargeForm/>,
action: null,
})
}}
>
Discharge Patient
</Button>
)
}
export default DischargePatient;

View file

@ -0,0 +1,229 @@
import React from 'react';
import TextField from '@material-ui/core/TextField';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import { Typography } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
'& .MuiTextField-root': {
margin: theme.spacing(2),
width: '25ch',
},
},
selectEmpty: {
marginTop: theme.spacing(1),
width: '25ch',
},
}));
function PrescribeMedications(){
const classes = useStyles();
const [state, setState] = React.useState({
firstname: '',
lastname: '',
drugnumber: '',
drugname: '',
unitsbyday: '',
numberofadmin: '',
administrationtime1: '',
administrationunits1: '',
administrationtime2: '',
administrationunits2: '',
administrationtime3: '',
administrationunits3: '',
methodofadmin: '',
startdate: '',
finishdate: '',
})
const handleChange = (e) => {
const value = e.target.value;
setState({
...state,
[e.target.id]: value
});
}
const prescribe = (e) => {
e.preventDefault();
var data = { // what to send goes here
}
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
mode: 'cors',
body: JSON.stringify(data)
};
fetch('/ ', options)
.then(res => res.json())
.then((data) => {
console.log(data)
//What happens after calling express goes here
}).catch(e => {
console.error(e);
});
}
return(
<div>
< Typography variant="h5" style={{padding: "1rem"}}> Prescribe Medications </Typography>
<form className={classes.root} autoComplete="off">
<div>
<TextField
required
label="First Name"
value={state.firstname}
id="firstname"
onChange={handleChange}
/>
<TextField
required
label="Last Name"
value={state.lastname}
id="lastname"
onChange={handleChange}
/>
</div>
<div>
<TextField
required
label="Drug Number"
value={state.drugnumber}
id="drugnumber"
onChange={handleChange}
/>
<TextField
required
label="Drug Name"
value={state.drugname}
id="drugname"
onChange={handleChange}
/>
</div>
<div>
<TextField
required
label="Units by Day"
value={state.unitsbyday}
id="unitsbyday"
onChange={handleChange}
/>
</div>
<Typography variant="h6" style={{ padding: "1rem" }}> Admin Information</Typography>
<div>
<TextField
required
label="Number of Administrations"
value={state.numberofadmin}
id="numberofadmin"
onChange={handleChange}
helperText="Per Day"
/>
</div>
<div>
<TextField
type="time"
value={state.adminstrationtime1}
id="adminstrationtime1"
onChange={handleChange}
helperText="Admnistration Time 1"
/>
<TextField
label="Admnistration Units 1"
value={state.administrationunits1}
id="administrationunits1"
onChange={handleChange}
/>
</div>
<div>
<TextField
type="time"
value={state.adminstrationtime2}
id="adminstrationtime1"
onChange={handleChange}
helperText="Admnistration Time 2"
/>
<TextField
label="Admnistration Units 1"
value={state.administrationunits2}
id="administrationunits2"
onChange={handleChange}
/>
</div>
<div>
<TextField
type="time"
value={state.adminstrationtime3}
id="adminstrationtime3"
onChange={handleChange}
helperText="Admnistration Time 3"
/>
<TextField
label="Admnistration Units 3"
value={state.administrationunits3}
id="administrationunits3"
onChange={handleChange}
/>
</div>
<div>
<TextField
required
label ="Method of Administration"
value={state.methodofadmin}
id="methodofadmin"
onChange={handleChange}
/>
</div>
<div>
<TextField
type="date"
value={state.startdate}
id="startdate"
onChange={handleChange}
helperText="Start Date"
/>
<TextField
type="date"
value={state.finishdate}
id="finishdate"
onChange={handleChange}
helperText="Finish Date"
/>
</div>
<Button style={{ float: "right", marginRight: "3rem" }}
variant="contained"
color="primary"
onClick={(e) => prescribe(e)}>
Prescibe Medication
</Button>
</form>
</div>
)
}
export default PrescribeMedications;

View file

@ -0,0 +1,36 @@
import React from 'react';
import Page from 'material-ui-shell/lib/containers/Page';
import Typography from '@material-ui/core/Typography';
import AdmissionRequests from './AdmissionRequests';
import PrescribeMedications from './PrescribeMedications';
import DischargePatient from './DischargePatient';
const StaffContext = (props) => {
//interchange between "CHARGE NURSE" and "MEDICAL" to see different actions
//get this using (props.accessType) or similar. Send props in wrapper
const [accessType, setAccessType] = React.useState('CHARGE NURSE') ;
if ( accessType === 'MEDICAL'){
return (
<Page pageTitle='Staff Context' >
<Typography variant="h4" style={{ padding: "1rem" }}> Available Actions for Doctor</Typography>
<PrescribeMedications/>
</Page>
)
} else if (accessType === "CHARGE NURSE"){
return (
<Page pageTitle='Staff Context' >
<div style={{padding: 64}}>
<Typography variant="h4" style={{marginBottom: 16}}> Available Actions for Charge Nurse</Typography>
<DischargePatient />
<AdmissionRequests/>
</div>
</Page>
)
}
}
export default StaffContext;

View file

@ -0,0 +1,3 @@
import StaffContext from "./StaffContext";
export default StaffContext;

View file

@ -0,0 +1,72 @@
import React from 'react';
import { connect } from 'react-redux';
import { Action } from 'reducers';
import StaffContext from './StaffContext';
class Home extends React.Component {
componentWillMount() {
if (this.props.isAdmin) {
return this.props.redirectHome();
}
if (this.props.authenticated) {
this.props.fetchUser();
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.isAdmin) {
return nextProps.redirectHome();
}
if (nextProps.updated) {
setTimeout(() => {
this.props.updateDone();
}, 1000);
}
}
render() {
return (
<StaffContext accessType = { this.props.profile.role}
/>
);
}
}
const mapStateToProps = (state) => {
const profile = {
name: '',
role: '',
};
if (state.User.get('fetchSuccess')) {
const User = state.User.get('profile');
profile.name = `${User.firstName} ${User.lastName}`;
profile.role = User.accessType;
}
return {
profile,
fetchSuccess: state.User.get('fetchSuccess'),
updated: state.User.get('updated'),
updateSuccess: state.User.get('updateSuccess'),
updateError: state.User.get('error'),
authenticated: state.Auth.get('authenticated'),
isAdmin: state.Auth.get('isAdmin'),
};
};
const mapDispatchToProps = dispatch => ({
fetchUser: () => {
dispatch(Action.FetchUser());
},
updateUser: (newprofile) => {
dispatch(Action.UpdateUser(newprofile));
},
updateDone: () => {
dispatch(Action.UserUpdateDone());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(Home);

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

131
client/src/reducers/auth.js Normal file
View file

@ -0,0 +1,131 @@
import config from '../config/config'
import storage from '../lib/storage';
import Immutable from 'immutable';
import { replace } from 'react-router-redux';
/** ********************************************
** Constants **
******************************************** */
const AUTH_USER = Symbol();
const UNAUTH_USER = Symbol();
const AUTH_ERROR = Symbol();
const initState = () => {
const token = storage.get('token');
return Immutable.fromJS({
error: null,
timestamp: null,
authenticated: !!token,
isAdmin: !!token && tokenIsAdmin(token),
});
};
/** ********************************************
** Helper Functions **
******************************************** */
const tokenGetClaims = (token) => {
if (!token) {
return {};
}
const tokenArray = token.split('.');
if (tokenArray.length !== 3) {
return {};
}
return JSON.parse(window.atob(tokenArray[1].replace('-', '+').replace('_', '/')));
};
const tokenIsAdmin = token => !!tokenGetClaims(token).admin;
/** ********************************************
** Auth States **
******************************************** */
class State {
static Auth(error, token) {
return {
type: error ? AUTH_ERROR : AUTH_USER,
isAdmin: error ? undefined : tokenIsAdmin(token),
error: error || undefined,
};
}
static UnAuth(error) {
return {
type: UNAUTH_USER,
};
}
}
/** ********************************************
** Actions **
******************************************** */
const LoginUser = (email, password) => async (dispatch) => {
try {
const response = await fetch(config.apiRoutes.API_URL + config.apiRoutes.auth.login, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
const status = await response.status;
const data = await response.json();
if (!data) throw new Error('Empty response from server');
if (data.error) throw new Error(data.error.message);
storage.set('token', data.token);
dispatch(State.Auth(null, data.token));
} catch (err) {
dispatch(State.Auth(err.message));
}
};
const LogoutUser = error => async (dispatch) => {
console.log('here')
dispatch(State.UnAuth());
storage.remove('token');
storage.remove('auth');
dispatch(replace('/login'));
};
/** ********************************************
** Auth Reducer **
******************************************** */
const Auth = (state = initState(), action) => {
switch (action.type) {
case AUTH_USER:
return state.withMutations((val) => {
val.set('error', null);
val.set('timestamp', Date.now());
val.set('authenticated', true);
val.set('isAdmin', action.isAdmin);
});
case UNAUTH_USER:
return state.withMutations((val) => {
val.set('authenticated', false);
val.set('isAdmin', false);
});
case AUTH_ERROR:
return state.withMutations((val) => {
val.set('error', action.error);
val.set('timestamp', Date.now());
});
default:
return state;
}
};
export {
Auth, LoginUser, LogoutUser
};

View file

@ -0,0 +1,37 @@
import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import { routerReducer } from "react-router-redux";
import thunk from "redux-thunk";
import { Auth, LoginUser, LogoutUser } from "./auth";
import { Registration, RegisterUser, registerDone } from "./registration";
import {
User,
FetchUser,
UpdateUser,
UserUpdateDone,
} from "./user";
const store = createStore(
combineReducers({
Auth,
Registration,
User,
router: routerReducer,
}),
compose(
applyMiddleware(thunk),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
const Action = {
LoginUser,
LogoutUser,
RegisterUser,
registerDone,
FetchUser,
UpdateUser,
UserUpdateDone,
};
export { store, Action };

View file

@ -0,0 +1,96 @@
import config from '../config/config';
import Immutable from 'immutable';
import { Action } from 'reducers';
/** ********************************************
** Constants **
******************************************** */
const REGISTER_SUCCESS = Symbol();
const REGISTER_ERR = Symbol();
const REGISTER_DONE = Symbol();
const defaultState = Immutable.fromJS({
user: {},
registered: false,
registerSuccess: false,
error: null,
});
/** ********************************************
** Registration States **
******************************************** */
class State {
static Register(error, user) {
return {
type: error ? REGISTER_ERR : REGISTER_SUCCESS,
user: error ? undefined : user,
error: error || undefined,
};
}
}
/** ********************************************
** Actions **
******************************************** */
const RegisterUser = user => async (dispatch) => {
try {
const response = await fetch(config.apiRoutes.API_URL + config.apiRoutes.auth.register, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ user }),
});
const status = await response.status;
const data = await response.json();
if (!data) throw new Error('Empty response from server');
if (data.error) throw new Error(data.error.message);
dispatch(State.Register(null, data.user));
dispatch(Action.LoginUser(user.email, user.password));
} catch (err) {
dispatch(State.Register(err.message));
}
};
/** ********************************************
** Registration Reducer **
******************************************** */
const Registration = (state = defaultState, action) => {
switch (action.type) {
case REGISTER_SUCCESS:
return state.withMutations((val) => {
val.set('user', action.user);
val.set('error', null);
val.set('registered', true);
val.set('registerSuccess', true);
});
case REGISTER_ERR:
return state.withMutations((val) => {
val.set('error', action.error);
val.set('registered', true);
val.set('registerSuccess', false);
});
case REGISTER_DONE:
return state.withMutations((val) => {
val.set('registered', false);
});
default:
return state;
}
};
const registerDone = () => ({ type: REGISTER_DONE });
export {
Registration, RegisterUser, registerDone,
};

150
client/src/reducers/user.js Normal file
View file

@ -0,0 +1,150 @@
import Immutable from "immutable";
import storage from '../lib/storage';
import config from '../config/config';
import { LogoutUser } from "./auth";
/** ********************************************
** Constants **
******************************************** */
const FETCH_USER = Symbol();
const FETCH_USER_ERR = Symbol();
const UPDATE_USER_ERR = Symbol();
const UPDATE_USER_SUCCESS = Symbol();
const UPDATE_COMPLETED = Symbol();
const defaultState = Immutable.fromJS({
profile: {},
updated: false,
updateSuccess: false,
fetchSuccess: false,
error: null,
});
/** ********************************************
** User States **
******************************************** */
class State {
static FetchUser(error, user) {
return {
type: error ? FETCH_USER_ERR : FETCH_USER,
user: error ? undefined : user,
error: error || undefined,
};
}
static UpdateUser(error) {
return {
type: error ? UPDATE_USER_ERR : UPDATE_USER_SUCCESS,
error: error || undefined,
};
}
}
/** ********************************************
** Actions **
******************************************** */
const FetchUser = () => async (dispatch) => {
try {
const response = await fetch(config.apiRoutes.API_URL + config.apiRoutes.user.user, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${storage.get("token")}`,
},
});
const status = await response.status;
if (status === 401 || status === 403) {
return dispatch(LogoutUser());
}
const data = await response.json();
if (!data) throw new Error("Empty response from server");
if (data.error) throw new Error(data.error.message);
dispatch(State.FetchUser(null, data.user));
} catch (err) {
dispatch(State.FetchUser(err.message));
}
};
const UpdateUser = (user) => async (dispatch) => {
try {
const response = await fetch(config.apiRoutes.API_URL + config.apiRoutes.user.user, {
method: "PATCH",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${storage.get("token")}`,
},
body: JSON.stringify({ user }),
});
const status = await response.status;
if (status === 401 || status === 403) {
return dispatch(LogoutUser());
}
const data = await response.json();
if (!data) throw new Error("Empty response from server");
if (data.error) throw new Error(data.error.message);
dispatch(State.FetchUser(null, data.user));
dispatch(State.UpdateUser());
} catch (err) {
dispatch(State.UpdateUser(err.message));
}
};
/** ********************************************
** User Reducer **
******************************************** */
const User = (state = defaultState, action) => {
switch (action.type) {
case FETCH_USER:
return state.withMutations((val) => {
val.set("error", null);
val.set("profile", action.user);
val.set("fetchSuccess", true);
});
case FETCH_USER_ERR:
return state.withMutations((val) => {
val.set("error", action.error);
val.set("profile", {});
val.set("fetchSuccess", false);
});
case UPDATE_USER_ERR:
return state.withMutations((val) => {
val.set("error", action.error);
val.set("updated", true);
val.set("updateSuccess", false);
});
case UPDATE_USER_SUCCESS:
return state.withMutations((val) => {
val.set("error", null);
val.set("updated", true);
val.set("updateSuccess", true);
});
case UPDATE_COMPLETED:
return state.withMutations((val) => {
val.set("updated", false);
});
default:
return state;
}
};
const UserUpdateDone = () => ({ type: UPDATE_COMPLETED });
export { User, FetchUser, UpdateUser, UserUpdateDone };

View file

@ -0,0 +1,17 @@
export const getDisplayUserType = (userType) => {
let displayUserType;
switch (userType) {
case "staff":
displayUserType = "Staff Member";
break;
case "medical":
displayUserType = "Medical Staff";
break;
case "nurse":
displayUserType = "Charge Nurse";
break;
default:
displayUserType = "";
}
return displayUserType;
};

BIN
design/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

376
package-lock.json generated Normal file
View file

@ -0,0 +1,376 @@
{
<<<<<<< HEAD
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"statuses": "~1.5.0",
"unpipe": "~1.0.0"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
},
"mime-types": {
"version": "2.1.27",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"requires": {
"mime-db": "1.44.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.9.1"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"mime": "1.6.0",
"ms": "2.1.1",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}
}
=======
"lockfileVersion": 1
>>>>>>> 21041b6e6cb315ec0afe09e549fc69518757e390
}

16
server/.babelrc Normal file
View file

@ -0,0 +1,16 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
],
"plugins": [
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-optional-chaining"
]
}

3
server/.cfignore Normal file
View file

@ -0,0 +1,3 @@
node_modules
/server
/public

8
server/.env Normal file
View file

@ -0,0 +1,8 @@
APP_ID=server
PORT=3001
LOG_LEVEL=debug
REQUEST_LIMIT=100kb
SESSION_SECRET=mySecret
OPENAPI_SPEC=/api/v1/spec
OPENAPI_ENABLE_RESPONSE_VALIDATION=false

5
server/.eslintignore Normal file
View file

@ -0,0 +1,5 @@
node_modules/
dist/
public/api-explorer/
*.yaml
*.yml

26
server/.eslintrc.json Normal file
View file

@ -0,0 +1,26 @@
{
"parser": "babel-eslint",
"extends": ["eslint:recommended", "plugin:node/recommended", "prettier"],
"env": {
"mocha": true
},
"plugins": ["prettier", "node"],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": 2,
"node/no-unsupported-features/es-syntax": 0,
"node/no-unpublished-import": ["off"],
"prettier/prettier": [
"error",
{
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": true,
"printWidth": 80
}
]
}
}

3
server/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
dist
.DS_Store

7
server/.nodemonrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"watch": [
"server/**/*.*",
".env"
],
"ext": "js,json,mjs,yaml,yml"
}

117
server/README.md Normal file
View file

@ -0,0 +1,117 @@
# HMS server-side code
## Get Started
Get started developing...
```shell
# install deps
npm install
# run in development mode
npm run dev
# run tests
npm run test
```
## PLEASE READ THIS WHEN ADDING YOUR PART TO SERVER
There are two key files that enable you to customize and describe your API:
1. `server/routes.js` - This references the implementation of all of your routes. Add as many routes as you like and point each route your express handler functions.
2. `server/common/api.yaml` - This file contains your [OpenAPI spec](https://swagger.io/specification/). Describe your API here. It's recommended that you to declare any and all validation logic in this YAML. `express-no-stress-typescript` uses [express-openapi-validator](https://github.com/cdimascio/express-openapi-validator) to automatically handle all API validation based on what you've defined in the spec.
## Install Dependencies
Install all package dependencies (one time operation)
```shell
npm install
```
## Run It
#### Run in *development* mode:
Runs the application is development mode. Should not be used in production
```shell
npm run dev
```
or debug it
```shell
npm run dev:debug
```
#### Run in *production* mode:
Compiles the application and starts it in production production mode.
```shell
npm run compile
npm start
```
## Test It
Run the Mocha unit tests
```shell
npm test
```
or debug them
```shell
npm run test:debug
```
## Try It
* Open your browser to [http://localhost:3001](http://localhost:3001)
* Invoke the `/examples` endpoint
```shell
curl http://localhost:3001/api/v1/examples
```
## Debug It
#### Debug the server:
```
npm run dev:debug
```
#### Debug Tests
```
npm run test:debug
```
#### Debug with VSCode
Add these [contents](https://github.com/cdimascio/generator-express-no-stress/blob/next/assets/.vscode/launch.json) to your `.vscode/launch.json` file
## Lint It
View prettier linter output
```
npm run lint
```
Fix all prettier linter errors
```
npm run lint
```
## Deploy It
Deploy to CloudFoundry
```shell
cf push server
```

7557
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

57
server/package.json Normal file
View file

@ -0,0 +1,57 @@
{
"name": "server",
"version": "1.0.0",
"description": "HMS server-side code",
"main": "index.js",
"scripts": {
"start": "node dist/index.js",
"compile": "babel server --out-dir dist --delete-dir-on-start --source-maps inline --copy-files",
"dev": "nodemon server --exec babel-node --config .nodemonrc.json | pino-pretty",
"dev:debug": "nodemon server --exec babel-node --config .nodemonrc.json --inspect | pino-pretty",
"test": "mocha --require @babel/register --exit",
"test:debug": "mocha --require @babel/register --inspect-brk --exit",
"lint": "eslint .",
"lint:fix": "eslint --fix ."
},
"dependencies": {
"bcrypt": "^5.0.0",
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"continuation-local-storage": "^3.2.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-openapi-validator": "^4.5.0",
"jsonwebtoken": "^8.5.1",
"pg": "^8.5.1",
"pg-hstore": "^2.3.3",
"pino": "^6.7.0",
"sequelize": "^6.3.5",
"sequelize-cli": "^6.2.0",
"underscore": "^1.12.0",
"uuid": "^8.3.1"
},
"devDependencies": {
"@babel/cli": "^7.12.1",
"@babel/core": "^7.12.3",
"@babel/node": "^7.12.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"@babel/register": "^7.12.1",
"babel-eslint": "^10.1.0",
"chai": "^4.2.0",
"eslint": "^7.12.1",
"eslint-plugin-import": "^2.22.1",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.4",
"prettier": "^2.1.2",
"mocha": "^8.2.1",
"nodemon": "^2.0.6",
"pino-pretty": "^4.3.0",
"supertest": "^6.0.1"
},
"author": "Carmine DiMascio <cdimascio@gmail.com> (https://github.com/cdimascio)"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

View file

@ -0,0 +1,60 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>server</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js"> </script>
<script src="./swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: window.location.origin + "/api/v1/spec",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>

View file

@ -0,0 +1,68 @@
<!doctype html>
<html lang="en-US">
<title>Swagger UI: OAuth2 Redirect</title>
<body onload="run()">
</body>
</html>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value)
}
) : {}
isValid = qp.state === sentState
if ((
oauth2.auth.schema.get("flow") === "accessCode"||
oauth2.auth.schema.get("flow") === "authorizationCode"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
</script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
server/public/index.html Normal file
View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
server
</title>
</head>
<body>
<h1>Welcome to
server
</h1>
<ul>
<li>
<h3><a href="/api-explorer/">Interactive API Doc!</a></h3>
</li>
</ul>
</body>
</html>

View file

@ -0,0 +1,149 @@
/* eslint-disable no-param-reassign */
/* eslint-disable consistent-return */
/* eslint-disable eqeqeq */
/* eslint-disable no-unused-vars */
const express = require('express');
const jwt = require('jsonwebtoken');
const config = require('../../../common/env');
const error = require('../../../common/error');
const { default: l } = require('../../../common/logger');
const { User } = require('../../../database');
const router = express.Router();
const TOKEN_EXPIRES = 7*86400; // 7 day in seconds
/**
* Middleware function that determines whether or not a user is authenticated
* and assigns the req.user object to their user info from the db
*
* @param req The request object
* @param res The response object
* @param next The next-middleware function
*/
export const authenticated = (req, res, next) => {
// We're looking for a header in the form of:
// Authorization: Bearer <TOKEN>
const referer = req.originalUrl;
// eslint-disable-next-line prettier/prettier
if (referer.includes('login') || referer.includes('signup') ) return next();
const authHeader = req.get('Authorization');
if (!authHeader) return next(new error.Unauthorized());
// authHead should be in the form of ['Bearer', '<TOKEN>']
const authHead = authHeader.split(' ');
if (
authHead.length != 2 ||
authHead[0] !== 'Bearer' ||
authHead[1].length < 1
)
return next(new error.Unauthorized());
const token = authHead[1];
jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => {
if (err) return next(new error.Unauthorized());
// if the user provided a valid token, use it to deserialize the UUID to
// an actual user object
User.findByUUID(decoded.uuid)
.then((user) => {
if (!user) throw new error.Unauthorized();
req.user = user;
})
.then(next)
.catch(next);
});
};
/**
* Login route.
*
* POST body should be in the format of { email, password }
* On success, this route will return a token
*/
router.post('/login', (req, res, next) => {
if (!req.body.email || req.body.email.length < 1)
return next(new error.BadRequest('Email must be provided'));
if (!req.body.password || req.body.password.length < 1)
return next(new error.BadRequest('Password must be provided'));
let userInfo = null;
User.findByEmail(req.body.email.toLowerCase())
.then((user) => {
if (!user) throw new error.UserError('Invalid email or password');
if (user.isPending()) {
throw new error.Unauthorized(
'Please activate your account. Check your email for an activation email'
);
}
if (user.isBlocked())
throw new error.Forbidden('Your account has been blocked');
return user
.verifyPassword(req.body.password)
.then((verified) => {
if (!verified) throw new error.UserError('Invalid email or password');
userInfo = user;
})
.then(
() =>
new Promise((resolve, reject) => {
// create a token with the user's ID and privilege level
jwt.sign(
{
uuid: user.getDataValue('uuid'),
admin: user.isAdmin(),
},
process.env.SESSION_SECRET,
{ expiresIn: TOKEN_EXPIRES },
(err, token) => (err ? reject(err) : resolve(token))
);
})
);
})
.then((token) => {
// respond with the token upon successful login
res.json({ error: null, token });
// register that the user logged in
})
.catch(next);
});
/**
* Registration route.
*
* POST body accepts a user object (see DB schema for user, sanitize function)
* Returns the created user on success
*/
router.post('/register', (req, res, next) => {
l.info(req.body);
if (!req.body.user)
return next(new error.BadRequest('User must be provided'));
if (!req.body.user.password)
return next(new error.BadRequest('Password must be provided'));
if (req.body.user.password.length < 4) {
return next(
new error.BadRequest('Password should be at least 10 characters long')
);
}
// get a sanitized version of the input
const userModel = User.sanitize(req.body.user);
// TODO: implement email auth instead of just active
userModel.state = 'ACTIVE';
// create the password hash
User.generateHash(req.body.user.password)
.then((hash) => {
userModel.hash = hash;
// add the user to the DB
return User.create(userModel);
})
.then((user) => {
// responsd with the newly created user
res.json({ error: null, user: user.getPublicProfile() });
})
.catch(next);
});
export default router;

View file

@ -0,0 +1,72 @@
import Division from '../../models/Division';
import AdmissionRequest from '../../models/AdmissionRequest';
import Patient from '../../models/Patient';
import Room from '../../models/Room';
import Repository from './repository';
module.exports = {
async getDivisionInfo(req, res) {
const { id } = req.body;
const division = await Repository.find(id);
return res.json(division);
},
async getDivisionAdmissionRequestList(req, res) {
const { id } = req.body;
const admissionList = Division.getAllAdmissionRequests(id);
return res.json(admissionList);
},
async createAdmissionRequest(req, res) {
const {divisionId, patientId, rationale, localDoctor} = req.body;
AdmissionRequest.create({divisionId, patientId, rationale, localDoctor});
let patient = await Patient.findById(patientId);
patient.isRequested = 'REQUESTED';
patient = await patient.save();
},
async admitPatient(req, res){
const { patientId, admissionRequestId } = req.body;
let admissionRequest = await AdmissionRequest.findById(admissionRequestId);
admissionRequest.approvalType = 'APPROVED';
admissionRequest = await admissionRequest.save();
let patient = await Patient.findById(patientId);
patient.isRequested = 'REQUEST_COMPLETED';
patient = await patient.save();
},
async admitPatientToRoom(req, res){
const {patientId, roomId, bedId} = req.body;
const room = await Room.findById(roomId);
let bed = await room.getBed(bedId);
bed.patientId = patientId;
bed.bedType = 'OCCUPIED';
bed = await bed.save();
let patient = await Patient.findById(patientId);
patient.isAdmitted = 'ADMITTED';
patient = await patient.save();
},
async dischargePatient(req, res){
const {roomId, bedId} = req.body;
const room = await Room.findById(roomId);
let bed = await room.getBed(bedId);
bed.patientId = 0;
bed.bedType = 'UNOCCUPIED';
bed = await bed.save();
let patient = await Patient.findById(patientId);
patient.isAdmitted = 'DISCHARGED';
patient = await patient.save();
},
};

View file

@ -0,0 +1,21 @@
import l from '../../../common/logger';
// EXAMPLE DATA - Will be replaced by data from DB
const division = ['division1', 'division2', 'division3', 'division4'];
const db = {
find: (id) => division[id],
save: (newDivision) => division.push(newDivision),
};
class DivisionRepository {
find(id) {
l.info(`${this.constructor.name}.byId(${id})`);
return db.find(id);
}
save(division) {
return db.save(division);
}
}
export default new DivisionRepository();

View file

@ -0,0 +1,22 @@
import * as express from 'express';
import facade from './facade';
const router = express.Router();
/**
* Get division information
*/
router.route('/').get(facade.getDivisionInfo);
/**
* Get division admission request list, Post admission request, Post patient admittance
*/
router.route('/AdmissionRequest').get(facade.getDivisionAdmissionRequestList).post(facade.createAdmissionRequest).post(facade.admitPatient);
/**
* Post patient admittance to room, Post patient discharge from a room
*/
router.route('/Room/Bed').post(facade.admitPatientToRoom).post(facade.dischargePatient);
export default router;

View file

@ -0,0 +1,14 @@
import Medication from '../../models/Medication';
module.exports = {
async createPrescription(req, res, next){
const {drugName, unitsByDay, numberOfAdmins, adminTimes, methodOfAdmin, startDate, finishDate} = req.body;
Medication.create({drugName, unitsByDay, numberOfAdmins, adminTimes, methodOfAdmin, startDate, finishDate});
},
};

View file

@ -0,0 +1,13 @@
import * as express from 'express';
import facade from './facade';
const router = express.Router();
router
.route('/')
/**
* Create a prescription for the patient
*/
.post(facade.createPrescription);
export default router;

View file

@ -0,0 +1,65 @@
import Patient from '../../models/Patient';
import Repository from './repository';
import Address from '../../models/Address';
module.exports = {
async registerPatient(req, res) {
const {
firstName,
lastName,
telephone,
dateOfBirth,
gender,
maritalStatus,
externalDoctor,
divisionId,
} = req.body;
Repository.save(
Patient.create({
firstName,
lastName,
telephone,
gender,
maritalStatus,
externalDoctor,
divisionId,
birthDate: dateOfBirth,
})
);
},
async getPatientFile(req, res) {
const { id } = req.body;
const patient = await Repository.find(id);
return res.json(patient);
},
async updatePatientFile(req, res) {
const {
firstName,
id,
lastName,
telephone,
dateOfBirth,
gender,
maritalStatus,
externalDoctor,
divisionId,
address,
} = req.body;
let patient = await Repository.find(id);
patient.firstName = firstName;
patient.lastName = lastName;
patient.telephone = telephone;
patient.dateOfBirth = dateOfBirth;
patient.gender = gender;
patient.maritalStatus = maritalStatus;
patient.externalDoctor = externalDoctor;
patient.divisionId = divisionId;
patient.address = address;
patient = await patient.save();
},
};

Some files were not shown because too many files have changed in this diff Show more