Added the files.
15
README.md
Normal 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
|
@ -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
|
@ -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
|
||||
```
|
11
client/continuous_deployment/before_install.sh
Normal 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
|
88
client/continuous_deployment/bs.js
Normal 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)
|
62
client/continuous_deployment/bs_test.js
Normal 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()
|
||||
}
|
||||
}
|
7
client/continuous_deployment/build.sh
Normal 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
|
8
client/continuous_deployment/deploy.sh
Normal 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
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
17632
client/package-lock.json
generated
Normal file
57
client/package.json
Normal 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
After Width: | Height: | Size: 15 KiB |
43
client/public/index.html
Normal 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
After Width: | Height: | Size: 5.2 KiB |
BIN
client/public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
client/public/manifest.json
Normal 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
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
13
client/src/App.js
Normal 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} />
|
||||
}
|
||||
}
|
11
client/src/config/apiRoutes.js
Normal 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"
|
||||
},
|
||||
};
|
37
client/src/config/config.js
Normal 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
|
1
client/src/config/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './config.js'
|
43
client/src/config/locales/de.js
Normal 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
|
43
client/src/config/locales/en.js
Normal 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
|
19
client/src/config/locales/index.js
Normal 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
|
43
client/src/config/locales/ru.js
Normal 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
|
149
client/src/config/menuItems.js
Normal 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
|
42
client/src/config/routes.js
Normal 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
|
33
client/src/config/themes.js
Normal 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
|
64
client/src/constants/index.js
Normal 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
|
@ -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
|
@ -0,0 +1,6 @@
|
|||
import * as storage from "../lib/storage";
|
||||
|
||||
|
||||
export default {
|
||||
...storage,
|
||||
};
|
49
client/src/lib/storage.js
Normal 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);
|
||||
|
41
client/src/pages/About/index.js
Normal 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>
|
||||
)
|
||||
}
|
93
client/src/pages/DivisionContext/DivisionView.js
Normal 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;
|
3
client/src/pages/DivisionContext/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import DivisionView from './DivisionView';
|
||||
|
||||
export default DivisionView;
|
74
client/src/pages/Home/Home.js
Normal 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);
|
21
client/src/pages/Home/HomeComponent.js
Normal 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
|
77
client/src/pages/LandingPage/LandingPage.js
Normal 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;
|
1
client/src/pages/LandingPage/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { default } from './LandingPage'
|
59
client/src/pages/PageNotFound/PageNotFound.js
Normal 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
|
115
client/src/pages/PasswordReset/PasswordReset.js
Normal 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
|
5
client/src/pages/PatientContext/Buttons.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import RegisterPatientButton from './RegisterPatientButton';
|
||||
import ConsultFileButton from './ConsultFileButton';
|
||||
import RequestToAdmitButton from './RequestToAdmitButton'
|
||||
|
||||
export { RegisterPatientButton, ConsultFileButton, RequestToAdmitButton };
|
28
client/src/pages/PatientContext/ConsultFileButton.js
Normal 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;
|
295
client/src/pages/PatientContext/ConsultFileForm.js
Normal 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;
|
||||
|
114
client/src/pages/PatientContext/PatientList.js
Normal 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;
|
29
client/src/pages/PatientContext/RegisterPatientButton.js
Normal 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;
|
229
client/src/pages/PatientContext/RegistrationForm.js
Normal 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;
|
76
client/src/pages/PatientContext/RequestToAdmitButton.js
Normal 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;
|
3
client/src/pages/PatientContext/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import PatientList from './PatientList';
|
||||
|
||||
export default PatientList;
|
46
client/src/pages/SignIn/SignIn.js
Normal 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);
|
179
client/src/pages/SignIn/SignInComponent.js
Normal 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> </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;
|
53
client/src/pages/SignUp/SignUp.js
Normal 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);
|
209
client/src/pages/SignUp/SignUpComponent.js
Normal 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;
|
100
client/src/pages/StaffContext/AdmissionRequests.js
Normal 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;
|
27
client/src/pages/StaffContext/AdmitButton.js
Normal 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;
|
137
client/src/pages/StaffContext/AdmitForm.js
Normal 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;
|
113
client/src/pages/StaffContext/DischargeForm.js
Normal 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;
|
27
client/src/pages/StaffContext/DischargePatient.js
Normal 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;
|
229
client/src/pages/StaffContext/PrescribeMedications.js
Normal 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;
|
36
client/src/pages/StaffContext/StaffContext.js
Normal 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;
|
3
client/src/pages/StaffContext/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import StaffContext from "./StaffContext";
|
||||
|
||||
export default StaffContext;
|
72
client/src/pages/StaffContext/wrapper.js
Normal 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);
|
BIN
client/src/pictures/logo.png
Normal file
After Width: | Height: | Size: 61 KiB |
131
client/src/reducers/auth.js
Normal 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
|
||||
};
|
37
client/src/reducers/index.js
Normal 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 };
|
96
client/src/reducers/registration.js
Normal 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
|
@ -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 };
|
17
client/src/utils/displayUserType.js
Normal 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
After Width: | Height: | Size: 61 KiB |
376
package-lock.json
generated
Normal 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
|
@ -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
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
/server
|
||||
/public
|
8
server/.env
Normal 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
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
public/api-explorer/
|
||||
*.yaml
|
||||
*.yml
|
26
server/.eslintrc.json
Normal 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
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
.DS_Store
|
7
server/.nodemonrc.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"watch": [
|
||||
"server/**/*.*",
|
||||
".env"
|
||||
],
|
||||
"ext": "js,json,mjs,yaml,yml"
|
||||
}
|
117
server/README.md
Normal 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
57
server/package.json
Normal 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)"
|
||||
}
|
BIN
server/public/api-explorer/favicon-16x16.png
Normal file
After Width: | Height: | Size: 665 B |
BIN
server/public/api-explorer/favicon-32x32.png
Normal file
After Width: | Height: | Size: 628 B |
60
server/public/api-explorer/index.html
Normal 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>
|
68
server/public/api-explorer/oauth2-redirect.html
Normal 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>
|
134
server/public/api-explorer/swagger-ui-bundle.js
Normal file
1
server/public/api-explorer/swagger-ui-bundle.js.map
Normal file
22
server/public/api-explorer/swagger-ui-standalone-preset.js
Normal file
4
server/public/api-explorer/swagger-ui.css
Normal file
1
server/public/api-explorer/swagger-ui.css.map
Normal file
9
server/public/api-explorer/swagger-ui.js
Normal file
1
server/public/api-explorer/swagger-ui.js.map
Normal file
22
server/public/index.html
Normal 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>
|
149
server/server/api/contexts/auth/index.js
Normal 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;
|
72
server/server/api/contexts/division/facade.js
Normal 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();
|
||||
},
|
||||
|
||||
|
||||
};
|
21
server/server/api/contexts/division/repository.js
Normal 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();
|
22
server/server/api/contexts/division/router.js
Normal 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;
|
14
server/server/api/contexts/medication/facade.js
Normal 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});
|
||||
|
||||
},
|
||||
|
||||
|
||||
};
|
13
server/server/api/contexts/medication/router.js
Normal 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;
|
65
server/server/api/contexts/patient/facade.js
Normal 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();
|
||||
},
|
||||
};
|