diff --git a/api/QCVOC.Api/Scans/Controller/ScansController.cs b/api/QCVOC.Api/Scans/Controller/ScansController.cs index dac5497a..9ad15256 100644 --- a/api/QCVOC.Api/Scans/Controller/ScansController.cs +++ b/api/QCVOC.Api/Scans/Controller/ScansController.cs @@ -10,7 +10,6 @@ namespace QCVOC.Api.Scans.Controller using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.ModelBinding; using QCVOC.Api.Common; using QCVOC.Api.Common.Data.Repository; using QCVOC.Api.Events.Data.Model; @@ -108,7 +107,7 @@ public IActionResult Scan([FromBody]ScanRequest scan) if (veteran == default(Veteran)) { - return StatusCode(404, "The specified Card Id doesn't match an enrolled Veteran."); + return StatusCode(404, $"Card Number {scan.CardNumber} doesn't match an enrolled Veteran."); } var scanRecord = new Scan() diff --git a/api/QCVOC.Api/Security/Controller/SecurityController.cs b/api/QCVOC.Api/Security/Controller/SecurityController.cs index f3fed6da..11bccbfb 100644 --- a/api/QCVOC.Api/Security/Controller/SecurityController.cs +++ b/api/QCVOC.Api/Security/Controller/SecurityController.cs @@ -77,7 +77,7 @@ public IActionResult CheckCredentials() [AllowAnonymous] [ProducesResponseType(typeof(TokenResponse), 200)] [ProducesResponseType(typeof(string), 400)] - [ProducesResponseType(401)] + [ProducesResponseType(typeof(string), 401)] [ProducesResponseType(typeof(Exception), 500)] public IActionResult Login([FromBody]TokenRequest credentials) { @@ -93,7 +93,7 @@ public IActionResult Login([FromBody]TokenRequest credentials) if (accountRecord == default(Account)) { - return Unauthorized(); + return StatusCode(401, "Login failed."); } PurgeExpiredRefreshTokensFor(accountRecord.Id); diff --git a/mobile-new/app/build.gradle b/mobile-new/app/build.gradle index 494930c8..3b352102 100644 --- a/mobile-new/app/build.gradle +++ b/mobile-new/app/build.gradle @@ -12,7 +12,7 @@ android { } buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } diff --git a/mobile-new/app/proguard-rules.pro b/mobile-new/app/proguard-rules.pro index f1b42451..1849a114 100644 --- a/mobile-new/app/proguard-rules.pro +++ b/mobile-new/app/proguard-rules.pro @@ -8,9 +8,9 @@ # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} +-keepclassmembers class org.qccoders.qcvoc.MainActivity { + public *; +} # Uncomment this to preserve the line number information for # debugging stack traces. diff --git a/mobile-new/app/src/main/ic_launcher-web.png b/mobile-new/app/src/main/ic_launcher-web.png new file mode 100644 index 00000000..587fbadf Binary files /dev/null and b/mobile-new/app/src/main/ic_launcher-web.png differ diff --git a/mobile-new/app/src/main/res/drawable/ic_launcher_background.xml b/mobile-new/app/src/main/res/drawable/ic_launcher_background.xml index d5fccc53..2408e30d 100644 --- a/mobile-new/app/src/main/res/drawable/ic_launcher_background.xml +++ b/mobile-new/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,170 +1,74 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:viewportWidth="108" + xmlns:android="http://schemas.android.com/apk/res/android"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eca70cfe..c4a603d4 100644 --- a/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cfe..c4a603d4 100644 --- a/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/mobile-new/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher.png index a2f59082..2ca7b1f4 100644 Binary files a/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..aa974af0 Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index 1b523998..430b9e24 100644 Binary files a/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/mobile-new/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher.png index ff10afd6..02b002b2 100644 Binary files a/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..df0914d0 Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index 115a4c76..bd6f32fd 100644 Binary files a/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/mobile-new/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher.png index dcd3cd80..cdf714fa 100644 Binary files a/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..30db38eb Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 459ca609..f4bad472 100644 Binary files a/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/mobile-new/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 8ca12fe0..f52bbdf4 100644 Binary files a/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..d5c7e0e0 Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 8e19b410..363df5db 100644 Binary files a/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/mobile-new/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index b824ebdd..55e8f5ae 100644 Binary files a/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..db579a67 Binary files /dev/null and b/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 4c19a13c..9e2fcd92 100644 Binary files a/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/mobile-new/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/web/src/accounts/AccountDialog.js b/web/src/accounts/AccountDialog.js index 0ba9b1f1..8006e369 100644 --- a/web/src/accounts/AccountDialog.js +++ b/web/src/accounts/AccountDialog.js @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import { withStyles } from '@material-ui/core/styles'; import { @@ -22,7 +22,6 @@ import { } from '@material-ui/core'; import CircularProgress from '@material-ui/core/CircularProgress'; -import Snackbar from '@material-ui/core/Snackbar'; import ConfirmDialog from '../shared/ConfirmDialog'; @@ -70,10 +69,6 @@ const initialState = { password: undefined, password2: undefined, }, - snackbar: { - message: '', - open: false, - }, confirmDialog: { open: false, }, @@ -118,14 +113,14 @@ class AccountDialog extends Component { if (result.isValid) { if (this.props.intent === 'add') { this.execute( - () => api.post('/v1/security/accounts', account), + () => this.props.context.api.post('/v1/security/accounts', account), 'addApi', 'Account \'' + account.name + '\' successfully created.' ) } else { this.execute( - () => api.patch('/v1/security/accounts/' + account.id, account), + () => this.props.context.api.patch('/v1/security/accounts/' + account.id, account), 'updateApi', 'Account \'' + account.name + '\' successfully updated.' ); @@ -142,7 +137,7 @@ class AccountDialog extends Component { let account = this.state.account; return this.execute( - () => api.delete('/v1/security/accounts/' + account.id), + () => this.props.context.api.delete('/v1/security/accounts/' + account.id), 'deleteApi', 'Account \'' + account.name + '\' successfully deleted.' ); @@ -152,10 +147,6 @@ class AccountDialog extends Component { this.setState({ confirmDialog: { open: false }}); } - handleSnackbarClose = () => { - this.setState({ snackbar: { open: false }}); - } - execute = (action, api, successMessage) => { return new Promise((resolve, reject) => { this.setState({ [api]: { isExecuting: true }}, () => { @@ -168,22 +159,8 @@ class AccountDialog extends Component { resolve(response); }) }, error => { - var body = error && error.response && error.response.data ? error.response.data : error; - - if (typeof(body) !== 'string') { - var keys = Object.keys(body); - - if (keys.length > 0) { - body = body[keys[0]]; - } - } - this.setState({ - [api]: { isExecuting: false, isErrored: true }, - snackbar: { - message: body, - open: true, - }, + [api]: { isExecuting: false, isErrored: true } }, () => reject(error)); }) }) @@ -337,13 +314,6 @@ class AccountDialog extends Component { >

Are you sure you want to delete account '{this.state.account.name}'?

- {this.state.snackbar.message}} - /> ); } @@ -357,4 +327,4 @@ AccountDialog.propTypes = { account: PropTypes.object, }; -export default withStyles(styles)(AccountDialog); \ No newline at end of file +export default withStyles(styles)(withContext(AccountDialog)); \ No newline at end of file diff --git a/web/src/accounts/Accounts.js b/web/src/accounts/Accounts.js index 1c74b62c..2adda4eb 100644 --- a/web/src/accounts/Accounts.js +++ b/web/src/accounts/Accounts.js @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import AccountList from './AccountList'; import ContentWrapper from '../shared/ContentWrapper'; @@ -21,7 +21,6 @@ import { } from '@material-ui/core'; import { Add } from '@material-ui/icons' -import Snackbar from '@material-ui/core/Snackbar'; import CircularProgress from '@material-ui/core/CircularProgress'; const styles = { @@ -69,10 +68,6 @@ class Accounts extends Component { open: false, account: undefined, }, - snackbar: { - message: '', - open: false, - }, }; componentWillMount = () => { @@ -81,7 +76,7 @@ class Accounts extends Component { refresh = (apiType) => { this.setState({ ...this.state, [apiType]: { ...this.state[apiType], isExecuting: true }}, () => { - api.get('/v1/security/accounts') + this.props.context.api.get('/v1/security/accounts') .then(response => { this.setState({ accounts: response.data, @@ -91,7 +86,6 @@ class Accounts extends Component { this.setState({ ...this.state, [apiType]: { isExecuting: false, isErrored: true }, - snackbar: { message: error.response.data, open: true }, }); }); }) @@ -133,7 +127,8 @@ class Accounts extends Component { } }, () => { if (result) { - this.setState({ snackbar: { message: result, open: true }}, () => this.refresh('refreshApi')); + this.props.context.showMessage(result); + this.refresh('refreshApi'); } }) } @@ -146,15 +141,12 @@ class Accounts extends Component { } }, () => { if (result) { - this.setState({ snackbar: { message: result, open: true }}, () => this.props.onPasswordReset()); + this.props.context.showMessage(result); + this.props.onPasswordReset(); } }) } - handleSnackbarClose = () => { - this.setState({ snackbar: { open: false }}); - } - render() { let { accounts, loadApi, refreshApi, accountDialog, passwordResetDialog } = this.state; let { classes } = this.props; @@ -197,13 +189,6 @@ class Accounts extends Component { onClose={this.handlePasswordResetDialogClose} /> - {this.state.snackbar.message}} - /> ); } @@ -213,4 +198,4 @@ Accounts.propTypes = { classes: PropTypes.object.isRequired, }; -export default withStyles(styles)(Accounts); \ No newline at end of file +export default withStyles(styles)(withContext(Accounts)); \ No newline at end of file diff --git a/web/src/api.js b/web/src/api.js index 8e8e5ee8..01244117 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -34,12 +34,12 @@ api.interceptors.response.use(config => { let headers = { headers: { 'Content-Type': 'application/json' }}; // use 'axios' here instead of the 'api' instance we created to bypass our interceptors - // and avoid an endless loop should either of these two calls result in a 401. + // and avoid an endless loop should this call result in a 401. return axios.post('/v1/security/refresh', data, headers) .then(response => { updateCredentials(response.data); request.headers.Authorization = response.data.tokenType + ' ' + response.data.accessToken; - return axios(request); + return api(request); }, error => { deleteCredentials(); window.location.reload(true); @@ -48,8 +48,22 @@ api.interceptors.response.use(config => { ); } else { + logError(error); return Promise.reject(error); } }) +const logError = (error) => { + if (error.response) { + console.log(error.response.data); + console.log(error.response.status); + console.log(error.response.headers); + } else if (error.request) { + console.log(error.request); + } else { + console.log('Error: ', error.message); + } + console.log(error.config); +} + export default api; \ No newline at end of file diff --git a/web/src/app/App.js b/web/src/app/App.js index de927ac6..ae1a3cac 100644 --- a/web/src/app/App.js +++ b/web/src/app/App.js @@ -7,7 +7,7 @@ import React, { Component } from 'react'; import { Route, Switch, withRouter } from 'react-router-dom'; import PropTypes from 'prop-types'; import { getCredentials, saveLocalCredentials, saveSessionCredentials, deleteCredentials, updateCredentials } from '../credentialStore'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import { withStyles } from '@material-ui/core/styles'; import { People, VerifiedUser, Assignment, InsertInvitation, SpeakerPhone } from '@material-ui/icons'; @@ -26,7 +26,7 @@ import LoginForm from '../security/LoginForm'; import LinkList from './LinkList'; import Scanner from '../scans/Scanner'; import { CircularProgress, ListSubheader } from '@material-ui/core'; -import { getEnvironment } from '../util'; +import { getEnvironment, userCanView } from '../util'; const styles = { root: { @@ -73,7 +73,7 @@ class App extends Component { componentDidMount = () => { if (getCredentials()) { this.setState({ api: { ...this.state.api, isExecuting: true }}, () => { - api.get('/v1/security').then(() => { + this.props.context.api.get('/v1/security').then(() => { this.setState({ api: { isExecuting: false, isErrored: false }, credentials: getCredentials() @@ -123,7 +123,7 @@ class App extends Component { render() { let classes = this.props.classes; let { isExecuting, isErrored } = this.state.api; - let { accessToken, role } = this.state.credentials; + let { accessToken } = this.state.credentials; let env = getEnvironment(); @@ -153,7 +153,7 @@ class App extends Component { }>Veterans }>Events }>Scanner - {(role === 'Administrator' || role === 'Supervisor') && + {userCanView() &&
Administration }>Services @@ -164,11 +164,15 @@ class App extends Component { - }/> - + {userCanView() && +
+ + }/> +
+ }
: @@ -182,4 +186,4 @@ App.propTypes = { classes: PropTypes.object.isRequired, }; -export default withRouter(withStyles(styles)(App)); \ No newline at end of file +export default withRouter(withStyles(styles)(withContext(App))); \ No newline at end of file diff --git a/web/src/events/EventDialog.js b/web/src/events/EventDialog.js index e0566226..63a41886 100644 --- a/web/src/events/EventDialog.js +++ b/web/src/events/EventDialog.js @@ -17,10 +17,9 @@ import { TextField, } from '@material-ui/core'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import CircularProgress from '@material-ui/core/CircularProgress'; -import Snackbar from '@material-ui/core/Snackbar'; import ConfirmDialog from '../shared/ConfirmDialog'; import DateTimePicker from 'material-ui-pickers/DateTimePicker'; @@ -63,10 +62,6 @@ const initialState = { startDate: undefined, endDate: undefined, }, - snackbar: { - message: '', - open: false, - }, confirmDialog: { open: false, }, @@ -98,14 +93,14 @@ class EventDialog extends Component { if (result.isValid) { if (this.props.intent === 'add') { this.execute( - () => api.post('/v1/events', event), + () => this.props.context.api.post('/v1/events', event), 'addApi', 'Event \'' + event.name + '\' successfully created.' ); } else { this.execute( - () => api.put('/v1/events/' + event.id, event), + () => this.props.context.api.put('/v1/events/' + event.id, event), 'updateApi', 'Event \'' + event.name + '\' successfully updated.' ); @@ -124,7 +119,7 @@ class EventDialog extends Component { handleDeleteConfirmClick = () => { return this.execute( - () => api.delete('/v1/events/' + this.state.event.id), + () => this.props.context.api.delete('/v1/events/' + this.state.event.id), 'deleteApi', 'Event \'' + this.state.event.name + '\' successfully deleted.' ); @@ -147,10 +142,6 @@ class EventDialog extends Component { this.setState({ confirmDialog: { open: false }}); } - handleSnackbarClose = () => { - this.setState({ snackbar: { open: false }}); - } - execute = (action, api, successMessage) => { return new Promise((resolve, reject) => { this.setState({ [api]: { isExecuting: true }}, () => { @@ -174,11 +165,7 @@ class EventDialog extends Component { } this.setState({ - [api]: { isExecuting: false, isErrored: true }, - snackbar: { - message: body, - open: true, - }, + [api]: { isExecuting: false, isErrored: true } }, () => reject(error)); }) }) @@ -308,13 +295,6 @@ class EventDialog extends Component { >

Are you sure you want to delete Event '{name + ' starting ' + moment(startDate).format('dddd, MMMM Do [at] LT')}?

- {this.state.snackbar.message}} - /> ); } @@ -328,4 +308,4 @@ EventDialog.propTypes = { event: PropTypes.object, }; -export default withStyles(styles)(EventDialog); \ No newline at end of file +export default withStyles(styles)(withContext(EventDialog)); \ No newline at end of file diff --git a/web/src/events/EventList.js b/web/src/events/EventList.js index 9ea0816d..575e85c7 100644 --- a/web/src/events/EventList.js +++ b/web/src/events/EventList.js @@ -7,7 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; -import { sortByProp, userCanView } from '../util'; +import { sortByProp } from '../util'; import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; @@ -36,8 +36,8 @@ const EventList = (props) => { {events.sort(sortByProp('startDate')).map((e, index) => onItemClick(e) : () => {}} + button={onItemClick !== undefined} + onClick={onItemClick !== undefined ? () => onItemClick(e) : () => {}} > {icon} diff --git a/web/src/events/Events.js b/web/src/events/Events.js index 03114c45..a32d99ba 100644 --- a/web/src/events/Events.js +++ b/web/src/events/Events.js @@ -5,11 +5,10 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import { withStyles } from '@material-ui/core/styles'; import ContentWrapper from '../shared/ContentWrapper'; -import Snackbar from '@material-ui/core/Snackbar'; import { Card, CardContent, Typography, CircularProgress, ListSubheader, Button } from '@material-ui/core'; import { Add, EventAvailable, Event, Today } from '@material-ui/icons'; import EventList from './EventList'; @@ -27,7 +26,7 @@ const styles = { zIndex: 1000 }, card: { - minHeight: 272, + minHeight: 273, maxWidth: 800, margin: 'auto', }, @@ -37,7 +36,7 @@ const styles = { right: 0, marginLeft: 'auto', marginRight: 'auto', - marginTop: 68, + marginTop: 80, }, }; @@ -59,10 +58,6 @@ class Events extends Component { intent: 'add', event: undefined, }, - snackbar: { - message: '', - open: false, - }, show: showCount } @@ -102,34 +97,28 @@ class Events extends Component { } }, () => { if (!result) return; - this.setState({ snackbar: { message: result, open: true }}, () => this.refresh('refreshApi')) + this.props.context.showMessage(result); + this.refresh('refreshApi'); }) } - handleSnackbarClose = () => { - this.setState({ snackbar: { open: false }}); - } - refresh = (apiType) => { this.setState({ [apiType]: { ...this.state[apiType], isExecuting: true }}, () => { - api.get('/v1/events?offset=0&limit=100&orderBy=ASC') + this.props.context.api.get('/v1/events?offset=0&limit=100&orderBy=ASC') .then(response => { this.setState({ events: response.data, [apiType]: { isExecuting: false, isErrored: false }, }); }, error => { - this.setState({ - [apiType]: { isExecuting: false, isErrored: true }, - snackbar: { message: error.response.data.Message, open: true }, - }); + this.setState({ [apiType]: { isExecuting: false, isErrored: true } }); }); }) } render() { let classes = this.props.classes; - let { events, loadApi, refreshApi, snackbar, show, eventDialog } = this.state; + let { events, loadApi, refreshApi, show, eventDialog } = this.state; events = events.map(e => ({ ...e, startDate: new Date(e.startDate).getTime(), endDate: new Date(e.endDate).getTime() })) @@ -156,23 +145,23 @@ class Events extends Component { } - onItemClick={this.handleEditClick} + onItemClick={userCanView() ? this.handleEditClick : undefined} /> Upcoming } - onItemClick={this.handleEditClick} + onItemClick={userCanView() ? this.handleEditClick : undefined} /> Past } - onItemClick={this.handleEditClick} + onItemClick={userCanView() ? this.handleEditClick : undefined} /> + {past.length > show && } } - {past.length > show && } { userCanView() && @@ -192,13 +181,6 @@ class Events extends Component { event={eventDialog.event} /> - {snackbar.message}} - /> ); } @@ -208,4 +190,4 @@ Events.propTypes = { classes: PropTypes.object.isRequired, }; -export default withStyles(styles)(Events); \ No newline at end of file +export default withStyles(styles)(withContext(Events)); \ No newline at end of file diff --git a/web/src/index.js b/web/src/index.js index 9ccef0fc..0ce58608 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -7,6 +7,7 @@ import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import MuiPickersUtilsProvider from 'material-ui-pickers/utils/MuiPickersUtilsProvider'; import MomentUtils from 'material-ui-pickers/utils/moment-utils'; +import ContextProvider from './shared/ContextProvider'; import './index.css'; import App from './app/App'; @@ -14,7 +15,9 @@ import App from './app/App'; ReactDOM.render( - + + + , document.getElementById('root') diff --git a/web/src/scans/Scanner.js b/web/src/scans/Scanner.js index 2ec35dfa..3dccfc04 100644 --- a/web/src/scans/Scanner.js +++ b/web/src/scans/Scanner.js @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import moment from 'moment'; import { withStyles } from '@material-ui/core/styles'; @@ -44,24 +44,15 @@ const styles = { right: 0, marginLeft: 'auto', marginRight: 'auto', - marginTop: 68, - }, - scanSpinner: { - position: 'fixed', - top: 0, - bottom: 0, - left: 0, - right: 0, - marginTop: 'auto', - marginBottom: 'auto', - marginLeft: 'auto', - marginRight: 'auto', + marginTop: 27, }, displayBox: { display: 'flex', justifyContent: 'center', alignItems: 'center', + textAlign: 'center', height: 'calc(100vh - 188px)', + width: '100%', }, title: { display: 'inline', @@ -86,8 +77,9 @@ const initialState = { service: undefined, }, scan: { + cardNumber: undefined, status: undefined, - data: undefined, + response: undefined, }, events: [], services: [], @@ -120,7 +112,7 @@ class Scanner extends Component { scanDialog: { open: false }, scanApi: { ...this.state.scanApi, isExecuting: true } }, () => { - api.put('/v1/scans', scan) + this.props.context.api.put('/v1/scans', scan) .then(response => { this.setState({ scanApi: { isExecuting: false, isErrored: false }}, () => { this.handleScanResponse(barcode, response); @@ -160,10 +152,6 @@ class Scanner extends Component { this.setState({ scan: scan, history: history, - }, () => { - setTimeout(() => { - this.setState({ scan: initialState.scan }); - }, 2500); }); } @@ -174,13 +162,17 @@ class Scanner extends Component { }); } + clearLastScan = () => { + this.setState({ scan: initialState.scan }); + } + fetchEvents = (apiType) => { let start = moment().startOf('day').format(); let end = moment().endOf('day').format(); return new Promise((resolve, reject) => { this.setState({ [apiType]: { ...this.state[apiType], isExecuting: true }}, () => { - api.get('/v1/events?dateStart=' + start + '&dateEnd=' + end) + this.props.context.api.get('/v1/events?dateStart=' + start + '&dateEnd=' + end) .then(response => { this.setState({ events: response.data, @@ -196,7 +188,7 @@ class Scanner extends Component { fetchServices = (apiType) => { this.setState({ [apiType]: { ...this.state[apiType], isExecuting: true }}, () => { - api.get('/v1/services') + this.props.context.api.get('/v1/services') .then(response => { this.setState({ services: response.data, @@ -218,7 +210,22 @@ class Scanner extends Component { } getScanDisplay = (scan) => { - return
{JSON.stringify(scan, null, 2)}
; + if (scan === undefined || scan.status === undefined) return; + + let { veteran, plusOne } = scan.response; + let { message, icon } = getScanResult(scan); + + icon = React.cloneElement(icon, { style: { fontSize: 72 }}); + let title = veteran ? veteran : scan.cardNumber; + + return ( +
+ {title} + {plusOne && +1} + {icon} + {message} +
+ ); } getDailyEvent = () => { @@ -239,7 +246,7 @@ class Scanner extends Component { this.setState({ refreshApi: { ...this.state.refreshApi, isExecuting: true } }, () => { - api.post('/v1/events', event) + this.props.context.api.post('/v1/events', event) .then(response => { resolve(response.data); }) @@ -294,36 +301,38 @@ class Scanner extends Component { this.setState({ historyDialog: { open: true }})} /> - {scanApi.isExecuting ? : - refreshApi.isExecuting ? - : -
- {!eventSelected && - } - onItemClick={this.handleEventItemClick} - /> - } - {!serviceSelected && eventSelected && - } - onItemClick={this.handleServiceItemClick} - /> - } - {serviceSelected && eventSelected && -
- {!scan.status ? : + {refreshApi.isExecuting ? + : +
+ {!eventSelected && + } + onItemClick={this.handleEventItemClick} + /> + } + {!serviceSelected && eventSelected && + } + onItemClick={this.handleServiceItemClick} + /> + } + {serviceSelected && eventSelected && +
+ {scanApi.isExecuting ? : + !scan.status ? : display - } -
- } -
+ } +
+ } +
} @@ -354,4 +363,4 @@ Scanner.propTypes = { classes: PropTypes.object.isRequired, }; -export default withStyles(styles)(Scanner); \ No newline at end of file +export default withStyles(styles)(withContext(Scanner)); \ No newline at end of file diff --git a/web/src/scans/ScannerMenu.js b/web/src/scans/ScannerMenu.js index ff13f52a..3aad005b 100644 --- a/web/src/scans/ScannerMenu.js +++ b/web/src/scans/ScannerMenu.js @@ -8,9 +8,9 @@ import PropTypes from 'proptypes'; import { withStyles } from '@material-ui/core/styles'; import IconButton from '@material-ui/core/IconButton'; -import { MoreVert, Replay, History } from '@material-ui/icons'; +import { MoreVert, Replay, History, ArrowBack } from '@material-ui/icons'; import ConfirmDialog from '../shared/ConfirmDialog'; -import { Menu, MenuItem, ListItemIcon, ListItemText } from '@material-ui/core'; +import { Menu, MenuItem, ListItemIcon, ListItemText, Divider } from '@material-ui/core'; const styles = { container: { @@ -40,7 +40,7 @@ class ScannerMenu extends Component { } handleMenuClose = () => { - this.setState({ menu: { open: false }}); + this.close(); } handleConfirmDialogClose = (result) => { @@ -48,13 +48,15 @@ class ScannerMenu extends Component { } handleResetScannerClick = () => { - this.setState({ menu: { open: false }, confirmDialog: { open: true }}); + this.close().then(() => this.setState({ confirmDialog: { open: true }})); } handleHistoryClick = () => { - this.setState({ menu: { open: false }}, () => { - this.props.viewHistory(); - }); + this.close().then(() => this.props.viewHistory()); + } + + handleClearLastScanClick = () => { + this.close().then(() => this.props.clearLastScan()); } resetScanner = () => { @@ -63,8 +65,14 @@ class ScannerMenu extends Component { }); } + close = () => { + return new Promise(resolve => { + this.setState({ menu: { open: false }}, () => resolve()); + }) + } + render() { - let { classes, visible, configured } = this.props; + let { classes, visible, configured, lastScan } = this.props; let { menu } = this.state; return ( @@ -82,18 +90,29 @@ class ScannerMenu extends Component { onClose={this.handleMenuClose} > {configured && - - - - - - View Scan History - - +
+ {lastScan && lastScan.status && + + + + + Clear Last Scan + + } + + + + + + View Scan History + + +
} + - + Reset Scanner @@ -118,6 +137,8 @@ ScannerMenu.propTypes = { visible: PropTypes.bool, configured: PropTypes.bool, resetScanner: PropTypes.func.isRequired, + lastScan: PropTypes.object, + clearLastScan: PropTypes.func.isRequired, viewHistory: PropTypes.func.isRequired, } diff --git a/web/src/security/LoginForm.js b/web/src/security/LoginForm.js index c2034db7..4d089a7a 100644 --- a/web/src/security/LoginForm.js +++ b/web/src/security/LoginForm.js @@ -4,9 +4,8 @@ */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import axios from 'axios'; +import { withContext } from '../shared/ContextProvider'; -import { getEnvironment } from '../util'; import { withStyles } from '@material-ui/core/styles'; import { Card, CardContent, CardActions, CircularProgress } from '@material-ui/core'; @@ -14,7 +13,6 @@ import TextField from '@material-ui/core/TextField'; import Button from '@material-ui/core/Button'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Checkbox from '@material-ui/core/Checkbox'; -import Snackbar from '@material-ui/core/Snackbar'; import logo from '../assets/qcvo.png'; @@ -66,10 +64,6 @@ const initialState = { isExecuting: false, isErrored: false, }, - snackbar: { - message: '', - open: false, - }, } class LoginForm extends Component { @@ -90,16 +84,12 @@ class LoginForm extends Component { }); } - handleSnackbarClose = () => { - this.setState({ snackbar: { open: false }}); - } - handleLoginClick = () => { this.setState({ api: { isExecuting: true }, }, () => { - axios.post(getEnvironment().apiRoot + '/v1/security/login', this.state) + this.props.context.api.post('/v1/security/login', this.state) .then( response => { this.props.onLogin(response.data, this.state.rememberMe); @@ -108,25 +98,10 @@ class LoginForm extends Component { this.setState({ api: { isExecuting: false, isErrored: true }}, () => { if (error.response && (error.response.status === 400 || error.response.status === 401)) { - this.setState({ - password: '', - snackbar: { - message: 'Login failed.', - open: true, - } - }, () => { + this.setState({ password: '', }, () => { this.passwordInput.focus(); }); } - else { - console.log(error) - this.setState({ - snackbar: { - message: 'Error: ' + error.message, - open: true - } - }); - } }); } ); @@ -197,13 +172,6 @@ class LoginForm extends Component { - {this.state.snackbar.message}} - /> ); } @@ -214,4 +182,4 @@ LoginForm.propTypes = { classes: PropTypes.object.isRequired, }; -export default withStyles(styles)(LoginForm); \ No newline at end of file +export default withStyles(styles)(withContext(LoginForm)); \ No newline at end of file diff --git a/web/src/security/PasswordResetDialog.js b/web/src/security/PasswordResetDialog.js index f79a1a9e..98d1880c 100644 --- a/web/src/security/PasswordResetDialog.js +++ b/web/src/security/PasswordResetDialog.js @@ -4,7 +4,7 @@ */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import { withStyles } from '@material-ui/core/styles'; import { @@ -17,7 +17,6 @@ import { } from '@material-ui/core'; import CircularProgress from '@material-ui/core/CircularProgress'; -import Snackbar from '@material-ui/core/Snackbar'; import ConfirmDialog from '../shared/ConfirmDialog'; import { getCredentials } from '../credentialStore'; @@ -49,10 +48,6 @@ const initialState = { password: undefined, password2: undefined, }, - snackbar: { - message: '', - open: false, - }, confirmDialog: { open: false, }, @@ -122,10 +117,6 @@ class PasswordResetDialog extends Component { return this.updatePassword(); } - handleSnackbarClose = () => { - this.setState({ snackbar: { open: false }}); - } - handleDialogClose = () => { this.setState({ confirmDialog: { open: false }}); } @@ -139,7 +130,7 @@ class PasswordResetDialog extends Component { delete account.name; delete account.password2; - api.patch('/v1/security/accounts/' + account.id, account) + this.props.context.api.patch('/v1/security/accounts/' + account.id, account) .then(response => { this.setState({ updateApi: { isExecuting: false, isErrored: false } @@ -148,22 +139,8 @@ class PasswordResetDialog extends Component { resolve(response); }) }, error => { - var body = error && error.response && error.response.data ? error.response.data : error; - - if (typeof(body) !== 'string') { - var keys = Object.keys(body); - - if (keys.length > 0) { - body = body[keys[0]]; - } - } - this.setState({ updateApi: { isExecuting: false, isErrored: true }, - snackbar: { - message: body, - open: true, - }, }, () => reject(error)); }) }) @@ -262,13 +239,6 @@ class PasswordResetDialog extends Component {

The user will be prompted to change their password at the next log in.

- {this.state.snackbar.message}} - /> ); } @@ -281,4 +251,4 @@ PasswordResetDialog.propTypes = { account: PropTypes.object, }; -export default withStyles(styles)(PasswordResetDialog); \ No newline at end of file +export default withStyles(styles)(withContext(PasswordResetDialog)); \ No newline at end of file diff --git a/web/src/security/SecurityMenu.js b/web/src/security/SecurityMenu.js index efad2f90..400b12ad 100644 --- a/web/src/security/SecurityMenu.js +++ b/web/src/security/SecurityMenu.js @@ -2,15 +2,18 @@ Copyright (c) QC Coders (JP Dillingham, Nick Acosta, Will Burklund, et. al.). All rights reserved. Licensed under the GPLv3 license. See LICENSE file in the project root for full license information. */ + import React, { Component } from 'react'; import PropTypes from 'proptypes'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; + +import './style.css'; import IconButton from '@material-ui/core/IconButton'; import Typography from '@material-ui/core/Typography'; import { LockOpen, ExitToApp, AccountCircle } from '@material-ui/icons'; import ConfirmDialog from '../shared/ConfirmDialog'; -import { Badge, Menu, MenuItem, Snackbar, ListItemIcon, ListItemText } from '@material-ui/core'; +import { Badge, Menu, MenuItem, ListItemIcon, ListItemText } from '@material-ui/core'; import PasswordResetDialog from '../security/PasswordResetDialog'; const styles = { @@ -25,9 +28,6 @@ const styles = { icon: { fontSize: 29, }, - menu: { - marginTop: 40, - }, } const initialState = { @@ -42,17 +42,13 @@ const initialState = { anchorEl: undefined, open: false, }, - snackbar: { - open: false, - message: undefined, - }, }; class SecurityMenu extends Component { state = initialState; logout = () => { - return api.post('v1/security/logout') + return this.props.context.api.post('v1/security/logout') .then(() => this.props.onLogout()); } @@ -85,16 +81,13 @@ class SecurityMenu extends Component { this.setState({ confirmDialog: { open: false }}); } - handleSnackbarClose = () => { - this.setState({ snackbar: { open: false }}); - } - handlePasswordResetDialogClose = (result) => { this.setState({ passwordResetDialog: { open: false }, }, () => { if (result) { - this.setState({ snackbar: { message: result, open: true }}, () => this.props.onPasswordReset()); + this.props.context.showMessage(result); + this.props.onPasswordReset(); } }); } @@ -121,7 +114,7 @@ class SecurityMenu extends Component { open={menu.open} anchorEl={menu.anchorEl} onClose={this.handleMenuClose} - style={styles.menu} + className={'menu'} > @@ -160,13 +153,6 @@ class SecurityMenu extends Component { account={passwordResetDialog.account} onClose={this.handlePasswordResetDialogClose} /> - {this.state.snackbar.message}} - /> ); } @@ -178,4 +164,4 @@ SecurityMenu.propTypes = { onPasswordReset: PropTypes.func.isRequired, } -export default SecurityMenu; \ No newline at end of file +export default withContext(SecurityMenu); \ No newline at end of file diff --git a/web/src/security/style.css b/web/src/security/style.css new file mode 100644 index 00000000..5456b12b --- /dev/null +++ b/web/src/security/style.css @@ -0,0 +1,14 @@ +/* + Copyright (c) QC Coders (JP Dillingham, Nick Acosta, Will Burklund, et. al.). All rights reserved. Licensed under the GPLv3 license. See LICENSE file + in the project root for full license information. +*/ + +.menu { + margin-top: 40px; +} + +@media screen and (max-width: 599px) { + .menu { + margin-top: 32px; + } +} \ No newline at end of file diff --git a/web/src/services/ServiceDialog.js b/web/src/services/ServiceDialog.js index 92985b35..7b8d8448 100644 --- a/web/src/services/ServiceDialog.js +++ b/web/src/services/ServiceDialog.js @@ -16,10 +16,9 @@ import { TextField, } from '@material-ui/core'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import CircularProgress from '@material-ui/core/CircularProgress'; -import Snackbar from '@material-ui/core/Snackbar'; import ConfirmDialog from '../shared/ConfirmDialog'; const styles = { @@ -59,10 +58,6 @@ const initialState = { name: undefined, description: undefined, }, - snackbar: { - message: '', - open: false, - }, confirmDialog: { open: false, }, @@ -90,14 +85,14 @@ class ServiceDialog extends Component { if (result.isValid) { if (this.props.intent === 'add') { this.execute( - () => api.post('/v1/services', service), + () => this.props.context.api.post('/v1/services', service), 'addApi', 'Service \'' + service.name + '\' successfully created.' ) } else { this.execute( - () => api.put('/v1/services/' + service.id, service), + () => this.props.context.api.put('/v1/services/' + service.id, service), 'updateApi', 'Service \'' + service.name + '\' successfully updated.' ); @@ -116,7 +111,7 @@ class ServiceDialog extends Component { handleDeleteConfirmClick = () => { return this.execute( - () => api.delete('/v1/services/' + this.state.service.id), + () => this.props.context.api.delete('/v1/services/' + this.state.service.id), 'deleteApi', 'Service \'' + this.state.service.name + '\' successfully deleted.' ); @@ -136,7 +131,7 @@ class ServiceDialog extends Component { } handleDialogClose = (result) => { - this.setState({ snackbar: { open: false }}); + this.setState({ confirmDialog: { open: false }}); } execute = (action, api, successMessage) => { @@ -151,23 +146,7 @@ class ServiceDialog extends Component { resolve(response); }) }, error => { - var body = error && error.response && error.response.data ? error.response.data : error; - - if (typeof(body) !== 'string') { - var keys = Object.keys(body); - - if (keys.length > 0) { - body = body[keys[0]]; - } - } - - this.setState({ - [api]: { isExecuting: false, isErrored: true }, - snackbar: { - message: body, - open: true, - }, - }, () => reject(error)); + this.setState({ [api]: { isExecuting: false, isErrored: true } }, () => reject(error)); }) }) }) @@ -272,13 +251,6 @@ class ServiceDialog extends Component { >

Are you sure you want to delete Service '{name}'?

- {this.state.snackbar.message}} - /> ); } @@ -292,4 +264,4 @@ ServiceDialog.propTypes = { service: PropTypes.object, }; -export default withStyles(styles)(ServiceDialog); \ No newline at end of file +export default withStyles(styles)(withContext(ServiceDialog)); \ No newline at end of file diff --git a/web/src/services/ServiceList.js b/web/src/services/ServiceList.js index 23e979b0..0402d21d 100644 --- a/web/src/services/ServiceList.js +++ b/web/src/services/ServiceList.js @@ -35,7 +35,7 @@ const ServiceList = (props) => { ServiceList.propTypes = { services: PropTypes.array.isRequired, - onItemClick: PropTypes.func.isRequired, + onItemClick: PropTypes.func, icon: PropTypes.object.isRequired, }; diff --git a/web/src/services/Services.js b/web/src/services/Services.js index 6edc7feb..716d40a5 100644 --- a/web/src/services/Services.js +++ b/web/src/services/Services.js @@ -4,12 +4,11 @@ */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import { withStyles } from '@material-ui/core/styles'; import ContentWrapper from '../shared/ContentWrapper'; -import Snackbar from '@material-ui/core/Snackbar'; import { Card, CardContent, Typography, CircularProgress, Button, ListSubheader } from '@material-ui/core'; import { Add } from '@material-ui/icons'; import ServiceList from './ServiceList'; @@ -27,7 +26,7 @@ const styles = { zIndex: 1000 }, card: { - minHeight: 220, + minHeight: 230, maxWidth: 800, margin: 'auto', }, @@ -37,7 +36,7 @@ const styles = { right: 0, marginLeft: 'auto', marginRight: 'auto', - marginTop: 72, + marginTop: 59, }, }; @@ -57,10 +56,6 @@ class Services extends Component { intent: 'add', service: undefined, }, - snackbar: { - message: '', - open: false, - }, } componentWillMount = () => { @@ -95,17 +90,14 @@ class Services extends Component { } }, () => { if (!result) return; - this.setState({ snackbar: { message: result, open: true }}, () => this.refresh('refreshApi')) + this.props.context.showMessage(result); + this.refresh('refreshApi'); }) } - handleSnackbarClose = () => { - this.setState({ snackbar: { open: false }}); - } - refresh = (apiType) => { this.setState({ [apiType]: { ...this.state[apiType], isExecuting: true}}, () => { - api.get('/v1/services?offset=0&limit=5000&orderBy=ASC') + this.props.context.api.get('/v1/services?offset=0&limit=5000&orderBy=ASC') .then(response => { this.setState({ services: response.data, @@ -113,8 +105,7 @@ class Services extends Component { }); }, error => { this.setState({ - [apiType]: { isExecuting: false, isErrored: true }, - snackbar: { message: error.response.data.Message, open: true }, + [apiType]: { isExecuting: false, isErrored: true } }); }); }) @@ -122,7 +113,7 @@ class Services extends Component { render() { let { classes } = this.props; - let { services, loadApi, refreshApi, snackbar, serviceDialog } = this.state; + let { services, loadApi, refreshApi, serviceDialog } = this.state; let userDefined = services.filter(s => s.id !== '00000000-0000-0000-0000-000000000000'); let systemDefined = services.filter(s => s.id === '00000000-0000-0000-0000-000000000000'); @@ -168,13 +159,6 @@ class Services extends Component { service={serviceDialog.service} /> - {snackbar.message}} - /> ); } @@ -184,4 +168,4 @@ Services.propTypes = { classes: PropTypes.object.isRequired, }; -export default withStyles(styles)(Services); \ No newline at end of file +export default withStyles(styles)(withContext(Services)); \ No newline at end of file diff --git a/web/src/shared/ContextProvider.js b/web/src/shared/ContextProvider.js new file mode 100644 index 00000000..3c63ef93 --- /dev/null +++ b/web/src/shared/ContextProvider.js @@ -0,0 +1,94 @@ +/* + Copyright (c) QC Coders. All rights reserved. Licensed under the GPLv3 license. See LICENSE file + in the project root for full license information. +*/ + +import React, { Component } from 'react'; +import Snackbar from '@material-ui/core/Snackbar'; +import api from '../api'; + +const Context = React.createContext(); + +const initialState = { + snackbar: { + message: '', + open: false, + } +} + +class ContextProvider extends Component { + state = initialState; + + showMessage = (message) => { + this.setState({ snackbar: { open: true, message: message }}) + } + + showErrorMessage = (error) => { + if (error.response && error.response.data) { + if (error.response.status === 500) { + this.showMessage(error.response.data.Message); + } else { + this.showMessage(error.response.data); + } + } + } + + apiCall = (command, ...args) => { + return new Promise ((resolve, reject) => + command(...args) + .then( + response => { + resolve(response); + }, + error => { + this.showErrorMessage(error); + reject(error); + } + ) + ); + } + + api = { + delete: (...args) => { return this.apiCall(api.delete, ...args); }, + get: (...args) => { return this.apiCall(api.get, ...args); }, + patch: (...args) => { return this.apiCall(api.patch, ...args); }, + post: (...args) => { return this.apiCall(api.post, ...args); }, + put: (...args) => { return this.apiCall(api.put, ...args); } + } + + handleSnackbarClose = () => { + this.setState({ snackbar: { message: '', open: false }}); + } + + render() { + return ( + +
+ {this.props.children} + {this.state.snackbar.message}} + /> +
+
+ ); + } +} + +export const withContext = (Component) => { + return (props) => { + return ( + + {context => ()} + + ); + }; +} + +export default ContextProvider; diff --git a/web/src/util.js b/web/src/util.js index 5a1b3984..4d54e3a9 100644 --- a/web/src/util.js +++ b/web/src/util.js @@ -58,7 +58,8 @@ export const userCanView = () => { let canView = false const credentials = getCredentials() - if (credentials.role === 'Supervisor' || credentials.role === 'Administrator') { + if (credentials + && (credentials.role === 'Supervisor' || credentials.role === 'Administrator')) { canView = true } diff --git a/web/src/veterans/VeteranDialog.js b/web/src/veterans/VeteranDialog.js index 72f22fdd..56497056 100644 --- a/web/src/veterans/VeteranDialog.js +++ b/web/src/veterans/VeteranDialog.js @@ -21,10 +21,9 @@ import { } from '@material-ui/core'; import { validateEmail, validatePhoneNumber, userCanView } from '../util'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import CircularProgress from '@material-ui/core/CircularProgress'; -import Snackbar from '@material-ui/core/Snackbar'; import ConfirmDialog from '../shared/ConfirmDialog'; const styles = { @@ -77,10 +76,6 @@ const initialState = { email: undefined, verificationMethod: undefined, }, - snackbar: { - message: '', - open: false, - }, confirmDialog: undefined, } @@ -110,7 +105,7 @@ class VeteranDialog extends Component { if (result.isValid) { if (this.props.intent === 'add') { this.execute( - () => api.post('/v1/veterans', veteran), + () => this.props.context.api.post('/v1/veterans', veteran), 'addApi', 'Veteran \'' + fullName + '\' successfully enrolled.' ) @@ -121,7 +116,7 @@ class VeteranDialog extends Component { this.setState({ confirmDialog: 'changeCardNumber' }); } else { this.execute( - () => api.put('/v1/veterans/' + veteran.id, veteran), + () => this.props.context.api.put('/v1/veterans/' + veteran.id, veteran), 'updateApi', 'Veteran \'' + fullName + '\' successfully updated.' ); @@ -141,7 +136,7 @@ class VeteranDialog extends Component { handleDeleteConfirmClick = () => { return this.execute( - () => api.delete('/v1/veterans/' + this.state.veteran.id), + () => this.props.context.api.delete('/v1/veterans/' + this.state.veteran.id), 'deleteApi', 'Veteran \'' + this.state.veteran.firstName + ' ' + this.state.veteran.lastName + '\' successfully deleted.' ); @@ -152,7 +147,7 @@ class VeteranDialog extends Component { let fullName = veteran.firstName + ' ' + veteran.lastName; return this.execute( - () => api.put('/v1/veterans/' + veteran.id, veteran), + () => this.props.context.api.put('/v1/veterans/' + veteran.id, veteran), 'updateApi', 'Veteran \'' + fullName + '\' successfully updated.' ); @@ -175,10 +170,6 @@ class VeteranDialog extends Component { this.setState({ confirmDialog: undefined}); } - handleSnackbarClose = () => { - this.setState({ snackbar: { open: false }}); - } - execute = (action, api, successMessage) => { return new Promise((resolve, reject) => { this.setState({ [api]: { isExecuting: true }}, () => { @@ -191,22 +182,8 @@ class VeteranDialog extends Component { resolve(response); }) }, error => { - var body = error && error.response && error.response.data ? error.response.data : error; - - if (typeof(body) !== 'string') { - var keys = Object.keys(body); - - if (keys.length > 0) { - body = body[keys[0]]; - } - } - this.setState({ [api]: { isExecuting: false, isErrored: true }, - snackbar: { - message: body, - open: true, - }, }, () => reject(error)); }) }) @@ -402,13 +379,6 @@ class VeteranDialog extends Component { >

Are you sure you want to change this Veteran's card number to {cardNumber}? The previous card, {oldCardNumber}, will no longer function.

- {this.state.snackbar.message}} - /> ); } @@ -422,4 +392,4 @@ VeteranDialog.propTypes = { veteran: PropTypes.object }; -export default withStyles(styles)(VeteranDialog); \ No newline at end of file +export default withStyles(styles)(withContext(VeteranDialog)); \ No newline at end of file diff --git a/web/src/veterans/Veterans.js b/web/src/veterans/Veterans.js index bd84b778..d4018d8b 100644 --- a/web/src/veterans/Veterans.js +++ b/web/src/veterans/Veterans.js @@ -4,12 +4,11 @@ */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import api from '../api'; +import { withContext } from '../shared/ContextProvider'; import { withStyles } from '@material-ui/core/styles'; import ContentWrapper from '../shared/ContentWrapper'; -import Snackbar from '@material-ui/core/Snackbar'; import { Card, CardContent, Typography, CircularProgress, Button, TextField, InputAdornment } from '@material-ui/core'; import { Add, Search } from '@material-ui/icons'; import VeteranList from './VeteranList'; @@ -67,10 +66,6 @@ class Veterans extends Component { intent: 'add', veteran: undefined, }, - snackbar: { - message: '', - open: false, - }, filter: '', show: showCount, } @@ -118,17 +113,14 @@ class Veterans extends Component { } }, () => { if (!result) return; - this.setState({ snackbar: { message: result, open: true }}, () => this.refresh('refreshApi')) + this.props.context.showMessage(result); + this.refresh('refreshApi'); }) } - handleSnackbarClose = () => { - this.setState({ snackbar: { open: false }}); - } - refresh = (apiType) => { this.setState({ [apiType]: { ...this.state[apiType], isExecuting: true }}, () => { - api.get('/v1/veterans?offset=0&limit=5000&orderBy=ASC') + this.props.context.api.get('/v1/veterans?offset=0&limit=5000&orderBy=ASC') .then(response => { this.setState({ veterans: response.data.map(p => ({ ...p, cardNumber: p.cardNumber || '' })), @@ -137,7 +129,6 @@ class Veterans extends Component { }, error => { this.setState({ [apiType]: { isExecuting: false, isErrored: true }, - snackbar: { message: error.response.data.Message, open: true }, }); }); }) @@ -145,7 +136,7 @@ class Veterans extends Component { render() { let { classes } = this.props; - let { veterans, loadApi, refreshApi, snackbar, show, veteranDialog } = this.state; + let { veterans, loadApi, refreshApi, show, veteranDialog } = this.state; let searchById = this.state.filter !== undefined && this.state.filter !== '' && !isNaN(this.state.filter); @@ -179,13 +170,15 @@ class Veterans extends Component { /> {refreshApi.isExecuting ? : - +
+ + {list.length > show && } +
} - {list.length > show && }