diff --git a/App.tsx b/App.tsx index b328b89..5f74e4a 100644 --- a/App.tsx +++ b/App.tsx @@ -6,25 +6,18 @@ import {BarCodeEvent, BarCodeScanner, PermissionResponse} from 'expo-barcode-sca import * as firebase from 'firebase'; import 'firebase/firestore'; import React, {ReactElement, useCallback, useEffect, useState} from 'react'; -import {AppRegistry, SafeAreaView, Text, View, YellowBox} from 'react-native'; -import { - Appbar, - Button, - DefaultTheme, - HelperText, - Provider as PaperProvider, RadioButton, - Snackbar, - Subheading, - TextInput, - Title, -} from 'react-native-paper'; +import {AppRegistry, SafeAreaView, View, YellowBox} from 'react-native'; +import {Appbar, DefaultTheme, Provider as PaperProvider, Snackbar, Title,} from 'react-native-paper'; import {expo as appExpo} from './app.json'; import {CancelButton} from './components/Common'; +import {InputLineCountButton, InputLineCountScreen} from './components/LineCount'; import {BarCodeDisplay, PrintButton, PrintingMessage} from './components/Print'; import {IdNumberInput, InputIdButton, ScanButton, Scanner} from './components/Scan'; +import {SettingsScreen} from './components/Settings'; import {colors, styles} from './components/Styles'; import {BarcodeScannerAppState} from './models/BarcodeScannerAppState'; import {CameraType, ElementProps, StateProps} from './models/ElementProps'; +import {LineCount} from './models/LineCount'; import {Sample} from './models/Sample'; const firebaseConfig = { @@ -49,6 +42,8 @@ YellowBox.ignoreWarnings([ const db = firebase.firestore(); const samplesCollection = db.collection('samples'); +const countsCollection = db.collection('counts'); +const dateFormat = 'yyyyMMddHHmm'; const theme = { ...DefaultTheme, @@ -63,6 +58,7 @@ export default function Main() { const [locationStr, setLocationStr] = useState('4321'); const [errorMessage, setErrorMessage] = useState(''); const [samples, setSamples] = useState([]); + const [lineCounts, setLineCounts] = useState([]); const [cameraType, setCameraType] = useState('back'); useEffect(() => { @@ -74,7 +70,7 @@ export default function Main() { } }); - const unsubscribe = samplesCollection.onSnapshot(querySnapshot => { + const unsubscribeSamples = samplesCollection.onSnapshot(querySnapshot => { // Transform and sort the data returned from Firebase const samplesFirestore = querySnapshot .docChanges() @@ -88,7 +84,21 @@ export default function Main() { appendSamples(samplesFirestore); }); - return () => unsubscribe() + const unsubscribeCounts = countsCollection.onSnapshot(querySnapshot => { + // Transform and sort the data returned from Firebase + const lineCountsFirestore = querySnapshot + .docChanges() + .filter(({type}) => type === 'added') + .map(({doc}) => { + const lineCount = doc.data(); + return {...lineCount, createdAt: lineCount.createdAt.toDate()} as LineCount; + }) + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + + appendLineCounts(lineCountsFirestore); + }); + + return () => unsubscribeSamples() }, []); const _doNothing = () => { @@ -101,6 +111,10 @@ export default function Main() { setErrorMessage(''); setAppState(BarcodeScannerAppState.INPUT); }; + const _inputLineCount = () => { + setErrorMessage(''); + setAppState(BarcodeScannerAppState.COUNT); + }; const _print = () => setAppState(BarcodeScannerAppState.PRINTING); const _printed = () => setAppState(BarcodeScannerAppState.PRINTED); const _home = () => setAppState(BarcodeScannerAppState.DEFAULT); @@ -116,7 +130,7 @@ export default function Main() { if (pattern.test(barCodeString)) { const cardId = e.data.slice(0, 9); const newSampleDate = new Date(); - const newSampleId = [cardId, format(newSampleDate, 'yyyyMMddHHmm'), locationStr].join('-'); + const newSampleId = [cardId, format(newSampleDate, dateFormat), locationStr].join('-'); setSampleId(newSampleId); setBarCodeId(cardId); @@ -128,16 +142,32 @@ export default function Main() { } }; + const handleLineCountSubmitted = (newCount: number) => { + const now = new Date(); + const newId = `${locationStr}-${format(now, dateFormat)}`; + const newData: LineCount = { + id: newId, + lineCount: newCount, + locationId: locationStr, + createdAt: now, + }; + sendDataToFirebase([newData], countsCollection); + } + const appendSamples = useCallback((newSamples) => { setSamples((previousSamples) => previousSamples.concat(newSamples)); }, [samples]); - const sendDataToFirebase = async (newSamples: Sample[]) => { - const writes = newSamples.map(s => samplesCollection.doc(s.id).set(s)); + const appendLineCounts = useCallback((newLineCounts) => { + setLineCounts((previousLineCounts) => previousLineCounts.concat(newLineCounts)); + }, [lineCounts]); + + const sendDataToFirebase = async (newData: Array, collection: firebase.firestore.CollectionReference) => { + const writes = newData.map((s: Sample|LineCount) => collection.doc(s.id).set(s)); await Promise.all(writes); } - function ErrorMessage(props: ElementProps): ReactElement { + const ErrorMessage = (props: ElementProps): ReactElement => { return @@ -151,94 +181,24 @@ export default function Main() { } - function LoadingMessage(props: ElementProps): ReactElement { + const LoadingMessage = (props: ElementProps): ReactElement => { return Loading...; } - function SuccessMessage(props: ElementProps): ReactElement { + const SuccessMessage = (props: ElementProps): ReactElement => { return Your barcode label has printed successfully.; } - function ActionButtons(props: ElementProps): ReactElement { + const ActionButtons = (props: ElementProps): ReactElement => { return } - function SettingsScreen(props: ElementProps): ReactElement { - const [inputStr, setInputStr] = useState(locationStr); - - const pattern = /^[\d]{4}$/; - const hasErrors = () => { - return !pattern.test(inputStr); - }; - - return - - Which camera to scan bar codes with? - setCameraType(value as CameraType)} - value={cameraType as string} - > - - Front - - - - Back - - - - - - - - Please do NOT change this unless you know what you are doing. Entering an incorrect location number may - prevent patients from getting accurate info about their test results. - - setInputStr(inputStr)} - mode="outlined" - theme={DefaultTheme} - /> - - Location number must be exactly 4 digits. No other characters are allowed. - - - - - - } - function App(props: StateProps): ReactElement { switch (props.appState) { case BarcodeScannerAppState.INITIAL: @@ -247,6 +207,7 @@ export default function Main() { return + ; case BarcodeScannerAppState.PRINTED: // Upload any changes to Firebase @@ -258,15 +219,13 @@ export default function Main() { return { id: s, barcodeId: propsArray[0], - createdAt: parse(propsArray[1], 'yyyyMMddHHmm', new Date()), + createdAt: parse(propsArray[1], dateFormat, new Date()), locationId: propsArray[2], } as Sample; }); - sendDataToFirebase(newSamples); + sendDataToFirebase(newSamples, samplesCollection).then(_home); }); - _home(); - return ; case BarcodeScannerAppState.PRINTING: return @@ -300,8 +259,22 @@ export default function Main() { onCancel={_home} cameraType={undefined} />; + case BarcodeScannerAppState.COUNT: + return ; case BarcodeScannerAppState.SETTINGS: - return ; + return { + setCameraType(newCameraType); + setLocationStr(newLocationStr); + _home(); + }} + onCancel={_home} + />; default: return ; } diff --git a/app.json b/app.json index 6154740..7f1fab2 100644 --- a/app.json +++ b/app.json @@ -4,7 +4,7 @@ "name": "uva-covid19-testing-kiosk", "slug": "uva-covid19-testing-kiosk", "version": "1.0.0", - "orientation": "landscape", + "orientation": "portrait", "icon": "./assets/icon.png", "splash": { "image": "./assets/splash.png", diff --git a/components/LineCount.tsx b/components/LineCount.tsx new file mode 100644 index 0000000..fca3cde --- /dev/null +++ b/components/LineCount.tsx @@ -0,0 +1,66 @@ +import {ButtonProps, InputLineCountScreenProps} from '../models/ElementProps'; +import React, {ReactElement, useState} from 'react'; +import {View} from 'react-native'; +import {DefaultTheme, Subheading, Title, RadioButton, Paragraph, TextInput, HelperText, Button} from 'react-native-paper'; +import {TextInput as NumberInput} from 'react-native'; +import {colors, styles} from './Styles'; + +export const InputLineCountButton = (props: ButtonProps): ReactElement => { + return ; +}; + +export const InputLineCountScreen = (props: InputLineCountScreenProps): ReactElement => { + const [newLineCount, setNewLineCount] = useState('0'); + + const hasErrors = () => { + if (newLineCount !== undefined && newLineCount !== null) { + const newInt = parseInt(newLineCount, 10); + return ( + isNaN(newInt) || + !isFinite(newInt) || + newInt < 0 || + newInt > 14000 + ); + } else { + return true; + } + }; + + return + Enter Line Count + + How many people are waiting in line? + setNewLineCount(inputStr)} + mode="outlined" + theme={DefaultTheme} + keyboardType="numeric" + /> + + Line count must be a whole number greater than 0. + + + + + + } diff --git a/components/Settings.tsx b/components/Settings.tsx new file mode 100644 index 0000000..bb94d9c --- /dev/null +++ b/components/Settings.tsx @@ -0,0 +1,69 @@ +import React, {ReactElement, useState} from 'react'; +import {View} from 'react-native'; +import {DefaultTheme, Subheading, Title, RadioButton, Paragraph, TextInput, HelperText, Button} from 'react-native-paper'; +import {CameraType, SettingsScreenProps} from '../models/ElementProps'; +import {colors, styles} from './Styles'; + +export const SettingsScreen = (props: SettingsScreenProps): ReactElement => { + const [newCameraType, setNewCameraType] = useState(props.cameraType); + const [newLocationStr, setNewLocationStr] = useState(props.locationStr); + + const pattern = /^[\d]{4}$/; + const hasErrors = () => { + return !pattern.test(newLocationStr); + }; + + return + Settings + + Camera to Use + setNewCameraType(value as CameraType)} + value={newCameraType as string} + > + + + + + + + Location Code + + Please do NOT change this unless you know what you are doing. Entering an incorrect location number may + prevent patients from getting accurate info about their test results. + + setNewLocationStr(inputStr)} + mode="outlined" + theme={DefaultTheme} + /> + + Location number must be exactly 4 digits. No other characters are allowed. + + + + + + } diff --git a/components/Styles.tsx b/components/Styles.tsx index b0c00fe..0148276 100644 --- a/components/Styles.tsx +++ b/components/Styles.tsx @@ -136,6 +136,9 @@ export const styles = StyleSheet.create({ safeAreaView: { flex: 1 }, + radio: { + backgroundColor: '#EEEEEE', + }, row: { flex: 1, flexDirection: 'row', @@ -143,10 +146,10 @@ export const styles = StyleSheet.create({ justifyContent: 'center', }, settings: { - flex: 1, - alignItems: 'stretch', - flexDirection: 'column', - justifyContent: 'center', + // flex: 1, + // alignItems: 'stretch', + // flexDirection: 'column', + // justifyContent: 'center', padding: 80, backgroundColor: DefaultTheme.colors.background, color: DefaultTheme.colors.text, diff --git a/models/BarcodeScannerAppState.tsx b/models/BarcodeScannerAppState.tsx index e7a8c2d..6b89a1b 100644 --- a/models/BarcodeScannerAppState.tsx +++ b/models/BarcodeScannerAppState.tsx @@ -4,6 +4,7 @@ export enum BarcodeScannerAppState { SCANNING = 'SCANNING', SCANNED = 'SCANNED', INPUT = 'INPUT', + COUNT = 'COUNT', PRINTING = 'PRINTING', PRINTED = 'PRINTED', ERROR = 'ERROR', diff --git a/models/ElementProps.tsx b/models/ElementProps.tsx index d697916..119957f 100644 --- a/models/ElementProps.tsx +++ b/models/ElementProps.tsx @@ -23,6 +23,18 @@ export interface ButtonProps extends ElementProps { onClicked: () => void; } +export interface InputLineCountScreenProps extends ElementProps { + onSave: (newCount: number) => void; + onCancel: () => void; +} + +export interface SettingsScreenProps extends ElementProps { + cameraType: CameraType; + locationStr: string; + onSave: (newCameraType: CameraType, newLocationStr: string) => void; + onCancel: () => void; +} + export interface ScannerProps extends ElementProps { onScanned: BarCodeScannedCallback; onCancel: () => void; diff --git a/models/LineCount.tsx b/models/LineCount.tsx new file mode 100644 index 0000000..0c8b4f6 --- /dev/null +++ b/models/LineCount.tsx @@ -0,0 +1,6 @@ +export interface LineCount { + id: string; + lineCount: number; + locationId: string; + createdAt: Date; +}