Adds initials input screen. Encodes initials into QR Code
This commit is contained in:
parent
dfbdfc2725
commit
632bb0c324
32
App.tsx
32
App.tsx
|
@ -5,14 +5,14 @@ import {format} from 'date-fns';
|
|||
import {BarCodeEvent, BarCodeScanner, PermissionResponse} from 'expo-barcode-scanner';
|
||||
import * as firebase from 'firebase';
|
||||
import 'firebase/firestore';
|
||||
import React, {ReactElement, useCallback, useEffect, useState} from 'react';
|
||||
import React, {ReactElement, useEffect, useState} from 'react';
|
||||
import {AppRegistry, SafeAreaView, View, YellowBox} from 'react-native';
|
||||
import {Appbar, Provider as PaperProvider, Snackbar,} 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 {IdNumberInput, InitialsInput, InputIdButton, ScanButton, Scanner} from './components/Scan';
|
||||
import {SettingsScreen} from './components/Settings';
|
||||
import {styles, theme} from './components/Styles';
|
||||
import {sendDataToFirebase, SyncMessage} from './components/Sync';
|
||||
|
@ -48,6 +48,7 @@ export default function Main() {
|
|||
const [cameraType, setCameraType] = useState<CameraType>('back');
|
||||
const [numCopies, setNumCopies] = useState<number>(0);
|
||||
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||
const [initials, setInitials] = useState<string>('');
|
||||
|
||||
const defaultsInitializers = {
|
||||
'default.cameraType': (s: string) => setCameraType(s as CameraType),
|
||||
|
@ -92,11 +93,11 @@ export default function Main() {
|
|||
};
|
||||
const _inputIdNumber = () => {
|
||||
setErrorMessage('');
|
||||
setAppState(BarcodeScannerAppState.INPUT);
|
||||
setAppState(BarcodeScannerAppState.INPUT_ID);
|
||||
};
|
||||
const _inputLineCount = () => {
|
||||
setErrorMessage('');
|
||||
setAppState(BarcodeScannerAppState.COUNT);
|
||||
setAppState(BarcodeScannerAppState.INPUT_LINE_COUNT);
|
||||
};
|
||||
const _print = () => setAppState(BarcodeScannerAppState.PRINTING);
|
||||
const _printed = () => setAppState(BarcodeScannerAppState.PRINTED);
|
||||
|
@ -113,18 +114,22 @@ export default function Main() {
|
|||
if (pattern.test(barCodeString)) {
|
||||
const cardId = e.data.slice(0, 9);
|
||||
const newSampleDate = new Date();
|
||||
const newSampleId = [cardId, format(newSampleDate, dateFormat), locationStr].join('-');
|
||||
|
||||
setSampleId(newSampleId);
|
||||
setBarCodeId(cardId);
|
||||
setSampleDate(newSampleDate);
|
||||
setAppState(BarcodeScannerAppState.SCANNED);
|
||||
setAppState(BarcodeScannerAppState.INPUT_INITIALS);
|
||||
} else {
|
||||
setErrorMessage(`The barcode data "${e.data}" is not from a valid ID card.`);
|
||||
setAppState(BarcodeScannerAppState.ERROR);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInitialsInput = (newInitials: string) => {
|
||||
setInitials(newInitials);
|
||||
const newSampleId = [barCodeId, newInitials, format(sampleDate, dateFormat), locationStr].join('-');
|
||||
setSampleId(newSampleId);
|
||||
setAppState(BarcodeScannerAppState.SCANNED);
|
||||
};
|
||||
|
||||
const handleLineCountSubmitted = (newCount: number) => {
|
||||
const now = new Date();
|
||||
const newId = `${locationStr}-${format(now, dateFormat)}`;
|
||||
|
@ -192,6 +197,7 @@ export default function Main() {
|
|||
barCodeId={barCodeId}
|
||||
date={sampleDate}
|
||||
location={locationStr}
|
||||
initials={initials}
|
||||
/>
|
||||
</View>;
|
||||
case BarcodeScannerAppState.SCANNED:
|
||||
|
@ -201,6 +207,7 @@ export default function Main() {
|
|||
barCodeId={barCodeId}
|
||||
date={sampleDate}
|
||||
location={locationStr}
|
||||
initials={initials}
|
||||
/>
|
||||
<ActionButtons/>
|
||||
</View>;
|
||||
|
@ -210,13 +217,18 @@ export default function Main() {
|
|||
onCancel={_home}
|
||||
cameraType={cameraType}
|
||||
/>;
|
||||
case BarcodeScannerAppState.INPUT:
|
||||
case BarcodeScannerAppState.INPUT_ID:
|
||||
return <IdNumberInput
|
||||
onScanned={handleBarCodeScanned}
|
||||
onCancel={_home}
|
||||
cameraType={undefined}
|
||||
/>;
|
||||
case BarcodeScannerAppState.COUNT:
|
||||
case BarcodeScannerAppState.INPUT_INITIALS:
|
||||
return <InitialsInput
|
||||
onSave={handleInitialsInput}
|
||||
onCancel={_home}
|
||||
/>;
|
||||
case BarcodeScannerAppState.INPUT_LINE_COUNT:
|
||||
return <InputLineCountScreen
|
||||
onSave={handleLineCountSubmitted}
|
||||
onCancel={_home}
|
||||
|
|
|
@ -16,7 +16,7 @@ public class MainActivity extends ReactActivity {
|
|||
super.onCreate(savedInstanceState);
|
||||
// SplashScreen.show(...) has to be called after super.onCreate(...)
|
||||
// Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually
|
||||
SplashScreen.show(this, SplashScreenImageResizeMode.CONTAIN, false);
|
||||
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, true);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -7,5 +7,7 @@
|
|||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:windowBackground">@drawable/splashscreen</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
4
app.json
4
app.json
|
@ -8,7 +8,7 @@
|
|||
"icon": "./assets/icon.png",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"resizeMode": "cover",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"updates": {
|
||||
|
@ -35,4 +35,4 @@
|
|||
"package": "com.sartography.uvacovid19testingkiosk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import React, {ReactElement, useEffect, useState} from 'react';
|
|||
import {Text, View} from 'react-native';
|
||||
import {Button, Title} from 'react-native-paper';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
import {dateDisplayFormat} from '../config/default';
|
||||
import {BarCodeProps, ButtonProps, PrintingProps} from '../models/ElementProps';
|
||||
import {Sample} from '../models/Sample';
|
||||
import {colors, styles} from './Styles';
|
||||
|
@ -201,6 +202,7 @@ export const PrintingMessage = (props: PrintingProps): ReactElement => {
|
|||
barCodeId={props.barCodeId}
|
||||
date={props.date}
|
||||
location={props.location}
|
||||
initials={props.initials}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.container}>
|
||||
|
@ -218,12 +220,11 @@ export const PrintingMessage = (props: PrintingProps): ReactElement => {
|
|||
</View>;
|
||||
}
|
||||
|
||||
|
||||
export const BarCodeDisplay = (props: BarCodeProps): ReactElement => {
|
||||
return <View style={styles.printPreview}>
|
||||
<Text style={styles.label}>ID#: {props.id}</Text>
|
||||
<Text style={styles.label}>Date: {props.date.toLocaleDateString()}, {props.date.toLocaleTimeString()}</Text>
|
||||
<Text style={styles.label}>Location {props.location}</Text>
|
||||
<Text style={styles.label}>ID #: {props.id}</Text>
|
||||
<Text style={styles.label}>Date: {format(props.date, dateDisplayFormat)}</Text>
|
||||
<Text style={styles.label}>Location #: {props.location}</Text>
|
||||
<QRCode value={props.id}/>
|
||||
</View>;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {BarCodeScanner} from 'expo-barcode-scanner';
|
|||
import React, {ReactElement, useState} from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
import {Button, DefaultTheme, HelperText, Subheading, TextInput, Title} from 'react-native-paper';
|
||||
import {ButtonProps, ScannerProps} from '../models/ElementProps';
|
||||
import {ButtonProps, ElementProps, InputInitialsProps, ScannerProps} from '../models/ElementProps';
|
||||
import {colors, styles} from './Styles';
|
||||
|
||||
|
||||
|
@ -100,3 +100,49 @@ export const IdNumberInput = (props: ScannerProps): ReactElement => {
|
|||
</View>
|
||||
</View>;
|
||||
};
|
||||
|
||||
|
||||
export const InitialsInput = (props: InputInitialsProps): ReactElement => {
|
||||
const [inputStr, setInputStr] = useState<string>('');
|
||||
const pattern = /^[A-Za-z]{1,5}$/;
|
||||
const hasErrors = () => {
|
||||
return !pattern.test(inputStr);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
props.onSave(inputStr);
|
||||
}
|
||||
|
||||
return <View style={styles.settings}>
|
||||
<Title style={styles.headingInverse}>Enter Patient's Initials</Title>
|
||||
<View style={{marginBottom: 10}}>
|
||||
<Subheading style={{color: DefaultTheme.colors.text, marginBottom: 60}}>
|
||||
Prompt the patient to show their name on their ID card. Enter the initials of their first name, middle name (if applicable), and last name.
|
||||
</Subheading>
|
||||
<TextInput
|
||||
label="Initials"
|
||||
value={inputStr}
|
||||
onChangeText={inputStr => setInputStr(inputStr.toLowerCase())}
|
||||
mode="outlined"
|
||||
theme={DefaultTheme}
|
||||
/>
|
||||
<HelperText type="error" visible={hasErrors()}>
|
||||
Please enter letters only.
|
||||
</HelperText>
|
||||
<Button
|
||||
icon="check"
|
||||
mode="contained"
|
||||
color={colors.primary}
|
||||
style={{marginBottom: 10}}
|
||||
disabled={hasErrors()}
|
||||
onPress={onSubmit}
|
||||
>Submit</Button>
|
||||
<Button
|
||||
icon="cancel"
|
||||
mode="outlined"
|
||||
color={colors.primary}
|
||||
onPress={props.onCancel}
|
||||
>Cancel</Button>
|
||||
</View>
|
||||
</View>;
|
||||
};
|
||||
|
|
|
@ -9,7 +9,6 @@ import {LineCount} from '../models/LineCount';
|
|||
import {Sample} from '../models/Sample';
|
||||
import {CancelButton} from './Common';
|
||||
import {styles} from './Styles';
|
||||
// @ts-ignore
|
||||
import * as firebase from 'firebase';
|
||||
import 'firebase/firestore';
|
||||
|
||||
|
@ -22,10 +21,14 @@ export const SyncMessage = (props: SyncProps): ReactElement => {
|
|||
const [syncStatus, setSyncStatus] = useState<string>('Syncing data...');
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Detect when user is online. If online, sync data with Firebase. If not online, just go home. Alternatively, set up a timer that periodically syncs data with the database.
|
||||
// TODO: Alternatively, set up a timer that periodically syncs data with the database.
|
||||
|
||||
// Upload any changes to Firebase
|
||||
// Detect when user is online. If online, sync data with Firebase.
|
||||
if (props.isConnected) {
|
||||
// Get the collection subscription
|
||||
const unsubscribe = props.samplesCollection.onSnapshot(q => {}, e => {});
|
||||
|
||||
// Upload any changes to Firebase
|
||||
AsyncStorage.getAllKeys().then(keys => {
|
||||
const newSamples = keys
|
||||
.filter(s => /^[\d]{9}-[\d]{12}-[\d]{4}$/.test(s))
|
||||
|
@ -40,11 +43,16 @@ export const SyncMessage = (props: SyncProps): ReactElement => {
|
|||
});
|
||||
|
||||
sendDataToFirebase(newSamples, props.samplesCollection).then(() => {
|
||||
// TODO: Delete stored keys in AsyncStorage
|
||||
|
||||
setSyncStatus('Data synced.');
|
||||
props.onSync();
|
||||
});
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
} else {
|
||||
// If not online, just go home.
|
||||
setSyncStatus('Device is not online. Skipping sync...');
|
||||
props.onCancel();
|
||||
}
|
||||
|
|
|
@ -9,3 +9,4 @@ export const firebaseConfig = {
|
|||
};
|
||||
|
||||
export const dateFormat = 'yyyyMMddHHmm';
|
||||
export const dateDisplayFormat = 'MM/dd/yyyy, hh:mm aa';
|
||||
|
|
|
@ -3,8 +3,9 @@ export enum BarcodeScannerAppState {
|
|||
DEFAULT = 'DEFAULT',
|
||||
SCANNING = 'SCANNING',
|
||||
SCANNED = 'SCANNED',
|
||||
INPUT = 'INPUT',
|
||||
COUNT = 'COUNT',
|
||||
INPUT_ID = 'INPUT_ID',
|
||||
INPUT_INITIALS = 'INPUT_INITIALS',
|
||||
INPUT_LINE_COUNT = 'INPUT_LINE_COUNT',
|
||||
PRINTING = 'PRINTING',
|
||||
PRINTED = 'PRINTED',
|
||||
ERROR = 'ERROR',
|
||||
|
|
|
@ -18,12 +18,18 @@ export interface BarCodeProps extends ElementProps {
|
|||
barCodeId: string;
|
||||
date: Date;
|
||||
location: string;
|
||||
initials: string;
|
||||
}
|
||||
|
||||
export interface ButtonProps extends ElementProps {
|
||||
onClicked: () => void;
|
||||
}
|
||||
|
||||
export interface InputInitialsProps extends ElementProps {
|
||||
onSave: (newInitials: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export interface InputLineCountScreenProps extends ElementProps {
|
||||
onSave: (newCount: number) => void;
|
||||
onCancel: () => void;
|
||||
|
|
Loading…
Reference in New Issue