Refactors settings into config file. Syncs data with Firebase if connected.
This commit is contained in:
parent
632bb0c324
commit
93c7cdd008
|
@ -668,3 +668,4 @@ buck-out/
|
|||
.expo/*
|
||||
web-build/
|
||||
*.hprof
|
||||
config/default.tsx
|
||||
|
|
30
App.tsx
30
App.tsx
|
@ -16,7 +16,7 @@ import {IdNumberInput, InitialsInput, InputIdButton, ScanButton, Scanner} from '
|
|||
import {SettingsScreen} from './components/Settings';
|
||||
import {styles, theme} from './components/Styles';
|
||||
import {sendDataToFirebase, SyncMessage} from './components/Sync';
|
||||
import {dateFormat, firebaseConfig} from './config/default';
|
||||
import {firebaseConfig, defaults} from './config/default';
|
||||
import {BarcodeScannerAppState} from './models/BarcodeScannerAppState';
|
||||
import {CameraType, ElementProps, StateProps} from './models/ElementProps';
|
||||
import {LineCount} from './models/LineCount';
|
||||
|
@ -33,20 +33,20 @@ if (firebase.apps.length === 0) {
|
|||
}
|
||||
|
||||
const db = firebase.firestore();
|
||||
const samplesCollection = db.collection('samples');
|
||||
const countsCollection = db.collection('counts');
|
||||
const samplesCollection = db.collection(defaults.samplesCollection);
|
||||
const countsCollection = db.collection(defaults.countsCollection);
|
||||
|
||||
export default function Main() {
|
||||
const [appState, setAppState] = useState<BarcodeScannerAppState>(BarcodeScannerAppState.INITIAL);
|
||||
const [sampleId, setSampleId] = useState<string>('');
|
||||
const [barCodeId, setBarCodeId] = useState<string>('');
|
||||
const [sampleDate, setSampleDate] = useState<Date>(new Date());
|
||||
const [locationStr, setLocationStr] = useState<string>('0000');
|
||||
const [locationStr, setLocationStr] = useState<string>(defaults.locationId);
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [samples, setSamples] = useState<Sample[]>([]);
|
||||
const [lineCounts, setLineCounts] = useState<LineCount[]>([]);
|
||||
const [cameraType, setCameraType] = useState<CameraType>('back');
|
||||
const [numCopies, setNumCopies] = useState<number>(0);
|
||||
const [cameraType, setCameraType] = useState<CameraType>(defaults.cameraType);
|
||||
const [numCopies, setNumCopies] = useState<number>(defaults.numCopies);
|
||||
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||
const [initials, setInitials] = useState<string>('');
|
||||
|
||||
|
@ -69,6 +69,7 @@ export default function Main() {
|
|||
});
|
||||
|
||||
// Watch for changes to internet connectivity.
|
||||
// TODO: Set up a timer that periodically syncs data with the database if connected.
|
||||
NetInfo.addEventListener((state: NetInfoState) => {
|
||||
if (state.type === NetInfoStateType.wifi) {
|
||||
setIsConnected(!!(state.isConnected && state.isInternetReachable));
|
||||
|
@ -100,7 +101,7 @@ export default function Main() {
|
|||
setAppState(BarcodeScannerAppState.INPUT_LINE_COUNT);
|
||||
};
|
||||
const _print = () => setAppState(BarcodeScannerAppState.PRINTING);
|
||||
const _printed = () => setAppState(BarcodeScannerAppState.PRINTED);
|
||||
const _sync = () => setAppState(BarcodeScannerAppState.SYNC);
|
||||
const _home = () => setAppState(BarcodeScannerAppState.DEFAULT);
|
||||
const _settings = () => setAppState(BarcodeScannerAppState.SETTINGS);
|
||||
|
||||
|
@ -125,21 +126,22 @@ export default function Main() {
|
|||
|
||||
const handleInitialsInput = (newInitials: string) => {
|
||||
setInitials(newInitials);
|
||||
const newSampleId = [barCodeId, newInitials, format(sampleDate, dateFormat), locationStr].join('-');
|
||||
const newSampleId = [barCodeId, newInitials, format(sampleDate, defaults.dateEncodedFormat), locationStr].join('-');
|
||||
setSampleId(newSampleId);
|
||||
setAppState(BarcodeScannerAppState.SCANNED);
|
||||
};
|
||||
|
||||
const handleLineCountSubmitted = (newCount: number) => {
|
||||
const now = new Date();
|
||||
const newId = `${locationStr}-${format(now, dateFormat)}`;
|
||||
const newId = `${locationStr}-${format(now, defaults.dateEncodedFormat)}`;
|
||||
const newData: LineCount = {
|
||||
id: newId,
|
||||
lineCount: newCount,
|
||||
locationId: locationStr,
|
||||
createdAt: now,
|
||||
};
|
||||
sendDataToFirebase([newData], countsCollection);
|
||||
|
||||
AsyncStorage.setItem(newData.id, JSON.stringify(newData)).then(_sync);
|
||||
}
|
||||
|
||||
const ErrorMessage = (props: ElementProps): ReactElement => {
|
||||
|
@ -170,7 +172,7 @@ export default function Main() {
|
|||
</View>
|
||||
}
|
||||
|
||||
function App(props: StateProps): ReactElement {
|
||||
const AppContent = (props: StateProps): ReactElement => {
|
||||
switch (props.appState) {
|
||||
case BarcodeScannerAppState.INITIAL:
|
||||
return <LoadingMessage/>;
|
||||
|
@ -180,7 +182,7 @@ export default function Main() {
|
|||
<InputIdButton onClicked={_inputIdNumber}/>
|
||||
<InputLineCountButton onClicked={_inputLineCount}/>
|
||||
</View>;
|
||||
case BarcodeScannerAppState.PRINTED:
|
||||
case BarcodeScannerAppState.SYNC:
|
||||
return <SyncMessage
|
||||
isConnected={isConnected}
|
||||
samplesCollection={samplesCollection}
|
||||
|
@ -192,7 +194,7 @@ export default function Main() {
|
|||
return <View style={styles.container}>
|
||||
<PrintingMessage
|
||||
numCopies={numCopies}
|
||||
onCancel={_printed}
|
||||
onCancel={_sync}
|
||||
id={sampleId}
|
||||
barCodeId={barCodeId}
|
||||
date={sampleDate}
|
||||
|
@ -270,7 +272,7 @@ export default function Main() {
|
|||
<Appbar.Action icon="settings" onPress={_settings}/>
|
||||
</Appbar.Header>
|
||||
<SafeAreaView style={styles.safeAreaView}>
|
||||
<App appState={appState}/>
|
||||
<AppContent appState={appState}/>
|
||||
</SafeAreaView>
|
||||
</PaperProvider>
|
||||
);
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
#Fri Sep 11 17:23:10 EDT 2020
|
||||
VERSION_NAME=1.0.7
|
||||
VERSION_BUILD=16
|
||||
VERSION_CODE=11
|
||||
#Sun Sep 13 08:50:53 EDT 2020
|
||||
VERSION_NAME=1.0.10
|
||||
VERSION_BUILD=19
|
||||
VERSION_CODE=14
|
||||
|
|
|
@ -5,7 +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 {defaults} from '../config/default';
|
||||
import {BarCodeProps, ButtonProps, PrintingProps} from '../models/ElementProps';
|
||||
import {Sample} from '../models/Sample';
|
||||
import {colors, styles} from './Styles';
|
||||
|
@ -223,7 +223,7 @@ export const PrintingMessage = (props: PrintingProps): ReactElement => {
|
|||
export const BarCodeDisplay = (props: BarCodeProps): ReactElement => {
|
||||
return <View style={styles.printPreview}>
|
||||
<Text style={styles.label}>ID #: {props.id}</Text>
|
||||
<Text style={styles.label}>Date: {format(props.date, dateDisplayFormat)}</Text>
|
||||
<Text style={styles.label}>Date: {format(props.date, defaults.dateDisplayFormat)}</Text>
|
||||
<Text style={styles.label}>Location #: {props.location}</Text>
|
||||
<QRCode value={props.id}/>
|
||||
</View>;
|
||||
|
|
|
@ -110,7 +110,7 @@ export const InitialsInput = (props: InputInitialsProps): ReactElement => {
|
|||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
props.onSave(inputStr);
|
||||
props.onSave(inputStr.toLowerCase());
|
||||
}
|
||||
|
||||
return <View style={styles.settings}>
|
||||
|
@ -122,7 +122,7 @@ export const InitialsInput = (props: InputInitialsProps): ReactElement => {
|
|||
<TextInput
|
||||
label="Initials"
|
||||
value={inputStr}
|
||||
onChangeText={inputStr => setInputStr(inputStr.toLowerCase())}
|
||||
onChangeText={inputStr => setInputStr(inputStr)}
|
||||
mode="outlined"
|
||||
theme={DefaultTheme}
|
||||
/>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import {parse} from 'date-fns';
|
||||
import React, {ReactElement, useEffect, useState} from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {Title} from 'react-native-paper';
|
||||
import {dateFormat} from '../config/default';
|
||||
import {Snackbar, Title} from 'react-native-paper';
|
||||
import {defaults} from '../config/default';
|
||||
import {CollectionMeta} from '../models/Collection';
|
||||
import {SyncProps} from '../models/ElementProps';
|
||||
import {LineCount} from '../models/LineCount';
|
||||
import {Sample} from '../models/Sample';
|
||||
|
@ -19,38 +19,70 @@ export const sendDataToFirebase = async (newData: Array<Sample | LineCount>, col
|
|||
|
||||
export const SyncMessage = (props: SyncProps): ReactElement => {
|
||||
const [syncStatus, setSyncStatus] = useState<string>('Syncing data...');
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: Alternatively, set up a timer that periodically syncs data with the database.
|
||||
// Display an error message if Firebase sync fails.
|
||||
const _handleError = (error: any, dataTypeLabel: string) => {
|
||||
setIsError(true);
|
||||
setErrorMessage(error);
|
||||
setSyncStatus(`Error occurred while syncing ${dataTypeLabel} data.`);
|
||||
props.onCancel();
|
||||
};
|
||||
|
||||
// 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))
|
||||
.map(s => {
|
||||
const propsArray = s.split('-');
|
||||
return {
|
||||
id: s,
|
||||
barcodeId: propsArray[0],
|
||||
createdAt: parse(propsArray[1], dateFormat, new Date()),
|
||||
locationId: propsArray[2],
|
||||
} as Sample;
|
||||
});
|
||||
|
||||
sendDataToFirebase(newSamples, props.samplesCollection).then(() => {
|
||||
// TODO: Delete stored keys in AsyncStorage
|
||||
|
||||
setSyncStatus('Data synced.');
|
||||
// Delete locally-cached data from AsyncStorage
|
||||
const _clearLocalCache = (keysToRemove: string[], dataTypeLabel: string) => {
|
||||
AsyncStorage.multiRemove(keysToRemove).then(() => {
|
||||
setSyncStatus(`${dataTypeLabel} data synced.`);
|
||||
props.onSync();
|
||||
});
|
||||
}
|
||||
|
||||
// Upload any new locally-stored items to Firebase, then remove them from local storage.
|
||||
const _syncCollection = (localStorageKeys: string[], collection: CollectionMeta) => {
|
||||
const newItemsKeys = localStorageKeys.filter(k => collection.keyRegex.test(k));
|
||||
AsyncStorage.multiGet(newItemsKeys, (errors, dataTuples) => {
|
||||
if (dataTuples && (dataTuples.length > 0)) {
|
||||
const data: Sample[] | LineCount[] = [];
|
||||
dataTuples.forEach(t => {
|
||||
if (t[1] !== null) {
|
||||
const lineCount = JSON.parse(t[1]);
|
||||
data.push(lineCount);
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
sendDataToFirebase(data, collection.firebaseCollection)
|
||||
.then(() => _clearLocalCache(newItemsKeys, collection.label))
|
||||
.catch(error => _handleError(error, collection.label));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Detect when user is online. If online, sync data with Firebase.
|
||||
if (props.isConnected) {
|
||||
const collectionsToSync = [
|
||||
{
|
||||
firebaseCollection: props.countsCollection,
|
||||
keyRegex: defaults.lineCountRegex,
|
||||
label: 'Line Counts',
|
||||
unsubscribe: props.countsCollection.onSnapshot(q => {}, e => {}),
|
||||
},
|
||||
{
|
||||
firebaseCollection: props.samplesCollection,
|
||||
keyRegex: defaults.qrCodeRegex,
|
||||
label: 'QR Codes',
|
||||
unsubscribe: props.samplesCollection.onSnapshot(q => {}, e => {}),
|
||||
},
|
||||
];
|
||||
|
||||
// Upload new data to Firebase
|
||||
AsyncStorage.getAllKeys().then(keys => {
|
||||
collectionsToSync.forEach(c => _syncCollection(keys, c));
|
||||
});
|
||||
|
||||
// Unsubscribe from all collections
|
||||
return () => collectionsToSync.forEach(c => c.unsubscribe());
|
||||
} else {
|
||||
// If not online, just go home.
|
||||
setSyncStatus('Device is not online. Skipping sync...');
|
||||
|
@ -61,6 +93,11 @@ export const SyncMessage = (props: SyncProps): ReactElement => {
|
|||
return <View style={styles.container}>
|
||||
<View style={styles.centerMiddle}>
|
||||
<Title style={styles.heading}>{syncStatus}</Title>
|
||||
<Snackbar
|
||||
visible={isError}
|
||||
onDismiss={props.onCancel}
|
||||
style={styles.error}
|
||||
>{errorMessage === '' ? 'Something went wrong. Try again.' : errorMessage}</Snackbar>
|
||||
<CancelButton onClicked={props.onCancel} />
|
||||
</View>
|
||||
</View>;
|
||||
|
|
|
@ -1,12 +1,26 @@
|
|||
import {AppDefaults} from '../models/Default';
|
||||
import {CameraType} from '../models/ElementProps';
|
||||
|
||||
// Firebase project config from https://console.firebase.google.com > Project Settings > General > Your apps > Web App
|
||||
export const firebaseConfig = {
|
||||
apiKey: 'api_key_goes_here',
|
||||
authDomain: "uva-covid19-testing-kiosk.firebaseapp.com",
|
||||
databaseURL: "https://uva-covid19-testing-kiosk.firebaseio.com",
|
||||
authDomain: 'uva-covid19-testing-kiosk.firebaseapp.com',
|
||||
databaseURL: 'https://uva-covid19-testing-kiosk.firebaseio.com',
|
||||
projectId: 'project_id_goes_here',
|
||||
storageBucket: "uva-covid19-testing-kiosk.appspot.com",
|
||||
storageBucket: 'uva-covid19-testing-kiosk.appspot.com',
|
||||
messagingSenderId: 'sender_id_goes_here',
|
||||
appId: 'app_id_goes_here'
|
||||
appId: 'app_id_goes_here',
|
||||
};
|
||||
|
||||
export const dateFormat = 'yyyyMMddHHmm';
|
||||
export const dateDisplayFormat = 'MM/dd/yyyy, hh:mm aa';
|
||||
// Default form field and data values
|
||||
export const defaults: AppDefaults = {
|
||||
countsCollection: 'counts', // Name of collection for Line Counts in Firebase.
|
||||
samplesCollection: 'samples', // Name of collection for Line Counts in Firebase.
|
||||
dateEncodedFormat: 'yyyyMMddHHmm', // Format for dates when encoded in IDs for database records.
|
||||
dateDisplayFormat: 'MM/dd/yyyy, hh:mm aa', // Format for dates when displayed to user.
|
||||
numCopies: 2, // Default number of copies of labels to print. Can be overridden by user setting.
|
||||
cameraType: 'back' as CameraType, // Which camera to use for capturing bar codes. Can be overridden by user setting.
|
||||
locationId: '0000', // Default location ID. Can be overridden by user setting.
|
||||
lineCountRegex: /^[\d]{4}-[\d]{12}$/, // ID format for Line Count records.
|
||||
qrCodeRegex: /^[\d]{9}-[a-zA-Z]+-[\d]{12}-[\d]{4}$/, // ID format for QR Code records.
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
Example configuration file. Make a copy of this file, name it "default.tsx", and place it in the config directory.
|
||||
Then modify the values below to match the actual Firebase configuration.
|
||||
*/
|
||||
import {AppDefaults} from '../models/Default';
|
||||
import {CameraType} from '../models/ElementProps';
|
||||
|
||||
// Firebase project config from https://console.firebase.google.com > Project Settings > General > Your apps > Web App
|
||||
export const firebaseConfig = {
|
||||
apiKey: 'api_key_goes_here',
|
||||
authDomain: 'uva-covid19-testing-kiosk.firebaseapp.com',
|
||||
databaseURL: 'https://uva-covid19-testing-kiosk.firebaseio.com',
|
||||
projectId: 'project_id_goes_here',
|
||||
storageBucket: 'uva-covid19-testing-kiosk.appspot.com',
|
||||
messagingSenderId: 'sender_id_goes_here',
|
||||
appId: 'app_id_goes_here',
|
||||
};
|
||||
|
||||
// Default form field and data values
|
||||
export const defaults: AppDefaults = {
|
||||
countsCollection: 'counts', // Name of collection for Line Counts in Firebase.
|
||||
samplesCollection: 'samples', // Name of collection for Line Counts in Firebase.
|
||||
dateEncodedFormat: 'yyyyMMddHHmm', // Format for dates when encoded in IDs for database records.
|
||||
dateDisplayFormat: 'MM/dd/yyyy, hh:mm aa', // Format for dates when displayed to user.
|
||||
numCopies: 2, // Default number of copies of labels to print. Can be overridden by user setting.
|
||||
cameraType: 'back' as CameraType, // Which camera to use for capturing bar codes. Can be overridden by user setting.
|
||||
locationId: '0000', // Default location ID. Can be overridden by user setting.
|
||||
lineCountRegex: /^[\d]{4}-[\d]{12}$/, // ID format for Line Count records.
|
||||
qrCodeRegex: /^[\d]{9}-[a-zA-Z]+-[\d]{12}-[\d]{4}$/, // ID format for QR Code records.
|
||||
}
|
|
@ -7,7 +7,7 @@ export enum BarcodeScannerAppState {
|
|||
INPUT_INITIALS = 'INPUT_INITIALS',
|
||||
INPUT_LINE_COUNT = 'INPUT_LINE_COUNT',
|
||||
PRINTING = 'PRINTING',
|
||||
PRINTED = 'PRINTED',
|
||||
SYNC = 'SYNC',
|
||||
ERROR = 'ERROR',
|
||||
SETTINGS = 'SETTINGS',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import * as firebase from 'firebase';
|
||||
import 'firebase/firestore';
|
||||
|
||||
export interface CollectionMeta {
|
||||
firebaseCollection: firebase.firestore.CollectionReference;
|
||||
keyRegex: RegExp;
|
||||
label: string;
|
||||
unsubscribe: () => void;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import {CameraType} from './ElementProps';
|
||||
|
||||
export interface AppDefaults {
|
||||
countsCollection: string;
|
||||
samplesCollection: string;
|
||||
dateEncodedFormat: string;
|
||||
dateDisplayFormat: string;
|
||||
numCopies: number;
|
||||
cameraType: CameraType;
|
||||
locationId: string;
|
||||
lineCountRegex: RegExp;
|
||||
qrCodeRegex: RegExp;
|
||||
}
|
Loading…
Reference in New Issue