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