Work Item and Activity screens mostly complete
This commit is contained in:
Binary file not shown.
@@ -44,7 +44,8 @@ class APIError extends Error {
|
|||||||
class API extends EventEmitter {
|
class API extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
this.user = null
|
// We don't know if the user has a valid token yet so assume they do and clean-up if they don't
|
||||||
|
this.user = { pending: true }
|
||||||
|
|
||||||
AsyncStorage.getItem(authTokenName).then((token) => {
|
AsyncStorage.getItem(authTokenName).then((token) => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -52,7 +53,6 @@ class API extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.token = token
|
this.token = token
|
||||||
this.user = { pending: true }
|
|
||||||
return this.who()
|
return this.who()
|
||||||
}).then((user) => {
|
}).then((user) => {
|
||||||
this.user = user
|
this.user = user
|
||||||
@@ -61,7 +61,7 @@ class API extends EventEmitter {
|
|||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
AsyncStorage.removeItem(authTokenName)
|
AsyncStorage.removeItem(authTokenName)
|
||||||
this.token = null
|
this.token = null
|
||||||
this.user = null
|
this.user = {}
|
||||||
this.socket = null
|
this.socket = null
|
||||||
this.emit('logout')
|
this.emit('logout')
|
||||||
})
|
})
|
||||||
@@ -83,12 +83,6 @@ class API extends EventEmitter {
|
|||||||
|
|
||||||
// Filter the few massages that affect our cached user data to avoid a server round trip
|
// Filter the few massages that affect our cached user data to avoid a server round trip
|
||||||
switch (eventName) {
|
switch (eventName) {
|
||||||
case 'newThumbnailImage':
|
|
||||||
this.user.thumbnailImageId = eventData.imageId
|
|
||||||
break
|
|
||||||
case 'newProfileImage':
|
|
||||||
this.user.imageId = eventData.imageId
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
// Nothing to see here...
|
// Nothing to see here...
|
||||||
break
|
break
|
||||||
@@ -219,7 +213,7 @@ class API extends EventEmitter {
|
|||||||
// Regardless of response, always logout in the client
|
// Regardless of response, always logout in the client
|
||||||
AsyncStorage.removeItem(authTokenName)
|
AsyncStorage.removeItem(authTokenName)
|
||||||
this.token = null
|
this.token = null
|
||||||
this.user = null
|
this.user = {}
|
||||||
this.disconnectSocket()
|
this.disconnectSocket()
|
||||||
this.emit('logout')
|
this.emit('logout')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,72 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View, TouchableHighlight, Image } from 'react-native'
|
||||||
import {
|
import {
|
||||||
ViroARSceneNavigator, ViroARScene, ViroARPlane, ViroBox, ViroText, ViroAmbientLight
|
ViroARSceneNavigator,
|
||||||
|
ViroARScene,
|
||||||
|
ViroARPlane,
|
||||||
|
ViroBox,
|
||||||
|
ViroNode,
|
||||||
|
ViroAmbientLight,
|
||||||
|
ViroSpotLight,
|
||||||
|
Viro3DObject,
|
||||||
|
ViroSurface,
|
||||||
} from 'react-viro'
|
} from 'react-viro'
|
||||||
import autobind from 'autobind-decorator'
|
import autobind from 'autobind-decorator'
|
||||||
import backImage from './images/back.png'
|
import backImage from './images/back.png'
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
helloWorldTextStyle: {
|
|
||||||
fontFamily: 'Arial',
|
|
||||||
fontSize: 30,
|
|
||||||
color: '#ffffff',
|
|
||||||
textAlignVertical: 'center',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
buttons : {
|
buttons : {
|
||||||
height: 80,
|
height: 80,
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shapes = {
|
||||||
|
hardhat: { shape: require('./models/hardhat.obj'), materials: [ require('./models/hardhat.mtl') ] },
|
||||||
|
question: { shape: require('./models/question.obj'), materials: [ require('./models/question.mtl') ] },
|
||||||
|
clipboard: { shape: require('./models/question.obj'), materials: [ require('./models/clipboard.mtl') ] },
|
||||||
|
}
|
||||||
|
|
||||||
class WorkItemSceneAR extends React.Component {
|
class WorkItemSceneAR extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
text : "Initializing AR..."
|
position: [0, .2, 0],
|
||||||
|
scale: [.2, .2, .2],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<ViroARScene onTrackingInitialized={()=>{this.setState({text : "Hello World!"})}}>
|
<ViroARScene>
|
||||||
<ViroAmbientLight color="#ffffff" intensity={200}/>
|
<ViroAmbientLight color="#ffffff" intensity={200}/>
|
||||||
<ViroText
|
|
||||||
text={this.state.text} scale={[.5, .5, .5]}
|
|
||||||
position={[0, 0, -1]}
|
|
||||||
style={styles.helloWorldTextStyle} />
|
|
||||||
<ViroARPlane>
|
<ViroARPlane>
|
||||||
<ViroBox position={[0, .5, 0]} />
|
<ViroNode
|
||||||
|
visible={true}
|
||||||
|
position={this.state.position}
|
||||||
|
scale={this.state.scale}
|
||||||
|
key='hardhat'>
|
||||||
|
<ViroSpotLight
|
||||||
|
innerAngle={5}
|
||||||
|
outerAngle={20}
|
||||||
|
direction={[0, -1 ,0]}
|
||||||
|
position={[0, 4, 0]}
|
||||||
|
color="#ffffff"
|
||||||
|
castsShadow={true}
|
||||||
|
shadowNearZ={.1}
|
||||||
|
shadowFarZ={6}
|
||||||
|
shadowOpacity={.9} />
|
||||||
|
<Viro3DObject
|
||||||
|
position={[0, 0, 0]}
|
||||||
|
source={shapes['hardhat'].shape}
|
||||||
|
type = "OBJ" />
|
||||||
|
<ViroSurface
|
||||||
|
rotation={[-90, 0, 0]}
|
||||||
|
position={[0, -.001, 0]}
|
||||||
|
width={2.5} height={2.5}
|
||||||
|
arShadowReceiver={true}
|
||||||
|
ignoreEventHandling={true} />
|
||||||
|
</ViroNode>
|
||||||
</ViroARPlane>
|
</ViroARPlane>
|
||||||
</ViroARScene>
|
</ViroARScene>
|
||||||
)
|
)
|
||||||
@@ -62,7 +91,7 @@ export class ARViewer extends React.Component {
|
|||||||
apiKey='06F37B6A-74DA-4A83-965A-7DE2209A5C46'
|
apiKey='06F37B6A-74DA-4A83-965A-7DE2209A5C46'
|
||||||
initialScene={{ scene: WorkItemSceneAR }} />
|
initialScene={{ scene: WorkItemSceneAR }} />
|
||||||
|
|
||||||
<View style={{position: 'absolute', left: 50, right: 0, top: 50}}>
|
<View style={{position: 'absolute', left: 30, right: 0, top: 50}}>
|
||||||
<TouchableHighlight style={styles.buttons} onPress={this._handlePress} underlayColor={'#00000000'} >
|
<TouchableHighlight style={styles.buttons} onPress={this._handlePress} underlayColor={'#00000000'} >
|
||||||
<Image source={backImage} />
|
<Image source={backImage} />
|
||||||
</TouchableHighlight>
|
</TouchableHighlight>
|
||||||
|
|||||||
@@ -1,30 +1,142 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, View, TouchableOpacity, Image, ScrollView } from 'react-native'
|
import {
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
Image,
|
||||||
|
ScrollView,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
TouchableOpacity
|
||||||
|
} from 'react-native'
|
||||||
|
import MapView, { Marker } from 'react-native-maps'
|
||||||
|
import { FormBinder } from 'react-form-binder'
|
||||||
|
import { Icon, Header, PhotoButton, BoundInput, BoundButton, BoundOptionStrip } from '../ui'
|
||||||
import autobind from 'autobind-decorator'
|
import autobind from 'autobind-decorator'
|
||||||
|
import { ifIphoneX, isIphoneX } from 'react-native-iphone-x-helper'
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexGrow: 1,
|
||||||
|
backgroundColor: '#DDDDDD',
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
width: '94%',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
alignSelf: 'center',
|
||||||
|
marginTop: '3%',
|
||||||
|
shadowColor: 'gray',
|
||||||
|
shadowOffset: { width: 2, height: 2 },
|
||||||
|
shadowRadius: 2,
|
||||||
|
shadowOpacity: .5,
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
marginBottom: 4,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export class Activity extends React.Component {
|
export class Activity extends React.Component {
|
||||||
static styles = StyleSheet.create({
|
static bindings = {
|
||||||
container: {
|
dateTime: {
|
||||||
height: '100%',
|
isValid: true,
|
||||||
width: '100%',
|
|
||||||
backgroundColor: '#AAAAAA',
|
|
||||||
},
|
},
|
||||||
})
|
location: {
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
details: {
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
resolution: {
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
isValid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
binder: new FormBinder({}, Activity.bindings),
|
||||||
|
messageModal: null,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handlePushButton(event) {
|
@autobind
|
||||||
this.props.history.goBack()
|
handleBackPress() {
|
||||||
|
const { history } = this.props
|
||||||
|
|
||||||
|
if (history.length > 1) {
|
||||||
|
history.goBack()
|
||||||
|
} else {
|
||||||
|
history.replace('/home')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { binder } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={Activity.styles.container}>
|
<View style={{ width: '100%', height: '100%' }}>
|
||||||
<View style={{ width: '94%', height: 300, backgroundColor: 'white', alignSelf: 'center', marginTop: '3%', shadowColor: 'gray', shadowOffset: { width: 2, height: 2 }, shadowRadius: 2, shadowOpacity: .5 }} />
|
<Header title='Activity' leftButton={{ icon: 'back', onPress: this.handleBackPress }} />
|
||||||
<View style={{ width: '94%', height: 300, backgroundColor: 'white', alignSelf: 'center', marginTop: '3%', shadowColor: 'gray', shadowOffset: { width: 2, height: 2 }, shadowRadius: 2, shadowOpacity: .5 }} />
|
<ScrollView style={styles.container}>
|
||||||
</ScrollView>
|
<View style={styles.panel}>
|
||||||
|
<BoundInput binder={binder} name='resolution' label='Resolution:' />
|
||||||
|
</View>
|
||||||
|
<View style={styles.panel}>
|
||||||
|
<BoundOptionStrip
|
||||||
|
binder={binder}
|
||||||
|
options={[
|
||||||
|
{ value: 'planned', text: 'Planned' },
|
||||||
|
{ value: 'open', text: 'Open' },
|
||||||
|
{ value: 'onHold', text: 'On Hold' },
|
||||||
|
{ value: 'closed', text: 'Closed' },
|
||||||
|
]}
|
||||||
|
label='Status:'
|
||||||
|
name='status' />
|
||||||
|
</View>
|
||||||
|
<View style={styles.panel}>
|
||||||
|
<BoundInput binder={binder} name='notes' label='Notes:' />
|
||||||
|
</View>
|
||||||
|
<View style={styles.panel}>
|
||||||
|
<BoundInput binder={binder} name='dateTime' label='Date & Time:' />
|
||||||
|
<BoundInput binder={binder} name='location' label='Location:' />
|
||||||
|
<MapView
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: 400,
|
||||||
|
marginTop: 10,
|
||||||
|
}}
|
||||||
|
zoomControlEnabled
|
||||||
|
initialRegion={{
|
||||||
|
latitude: 43.653908,
|
||||||
|
longitude: -79.384293,
|
||||||
|
latitudeDelta: 0.0922,
|
||||||
|
longitudeDelta: 0.0421,
|
||||||
|
}} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.panel}>
|
||||||
|
<Text style={styles.label}>Pictures:</Text>
|
||||||
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 5 }}>
|
||||||
|
<PhotoButton />
|
||||||
|
<PhotoButton />
|
||||||
|
<PhotoButton />
|
||||||
|
</View>
|
||||||
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: 5 }}>
|
||||||
|
<PhotoButton />
|
||||||
|
<PhotoButton />
|
||||||
|
<PhotoButton />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
{ isIphoneX ? <View style={{ height: 30, width: '100%' }} /> : null }
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,6 @@
|
|||||||
import React, { Fragment, Component } from 'react'
|
import React, { Fragment, Component } from 'react'
|
||||||
import { api } from '../API'
|
|
||||||
import { Route, Redirect } from 'react-router-native'
|
import { Route, Redirect } from 'react-router-native'
|
||||||
import { View } from 'react-native'
|
|
||||||
import autobind from 'autobind-decorator'
|
|
||||||
|
|
||||||
export class DefaultRoute extends Component {
|
export const DefaultRoute = () => {
|
||||||
@autobind
|
return <Route render={() => (<Redirect to={'/home'} />)} />
|
||||||
updateComponent() {
|
|
||||||
this.forceUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
api.addListener('login', this.updateComponent)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
api.removeListener('login', this.updateComponent)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const user = api.loggedInUser
|
|
||||||
let path = null
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
if (!user.pending) {
|
|
||||||
path = '/home'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
path = '/login'
|
|
||||||
}
|
|
||||||
|
|
||||||
const { location } = this.props
|
|
||||||
|
|
||||||
// Render a redirect or nothing until we finished logging on
|
|
||||||
return (
|
|
||||||
<Route path='/' render={() => (
|
|
||||||
path ? <Redirect to={path} /> : null
|
|
||||||
)} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,25 +17,25 @@ export class ProtectedRoute extends React.Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
api.addListener('login', this.updateComponent)
|
api.addListener('login', this.updateComponent)
|
||||||
|
api.addListener('logout', this.updateComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
api.removeListener('login', this.updateComponent)
|
api.removeListener('login', this.updateComponent)
|
||||||
|
api.removeListener('logout', this.updateComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
render(props) {
|
render(props) {
|
||||||
const user = api.loggedInUser
|
const user = api.loggedInUser
|
||||||
|
|
||||||
if (user) {
|
if (user.pending) {
|
||||||
if (user.pending) {
|
return null
|
||||||
// The API might be in the middle of fetching the user information
|
} else {
|
||||||
return null
|
if (!user._id || (this.props.admin && !user.administrator)) {
|
||||||
} else if (!this.props.admin || (this.props.admin && user.administrator)) {
|
return <Redirect to='/login' />
|
||||||
|
} else {
|
||||||
return <Route {...this.props} />
|
return <Route {...this.props} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Can add redirect back in here - see website
|
|
||||||
return <Redirect to='/login' />
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Icon, Header } from '../ui'
|
|||||||
import { api } from '../API'
|
import { api } from '../API'
|
||||||
import autobind from 'autobind-decorator'
|
import autobind from 'autobind-decorator'
|
||||||
import pinImage from './images/pin.png'
|
import pinImage from './images/pin.png'
|
||||||
|
import { ifIphoneX } from 'react-native-iphone-x-helper'
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@@ -46,24 +47,29 @@ export class Home extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleAdminButton() {
|
handleWorkItemsListPress() {
|
||||||
this.props.history.replace('/admin')
|
this.props.history.push('/workitemlist')
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleItemSelect(item, index) {
|
handleItemSelect(item, index) {
|
||||||
this.props.history.replace('/activity')
|
this.props.history.push('/activity')
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleLogout() {
|
handleLogoutPress() {
|
||||||
this.props.history.replace('/logout')
|
this.props.history.replace('/logout')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleGlassesPress() {
|
||||||
|
this.props.history.push('/arviewer')
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Header title='Work Item Map' leftButton={{ icon: 'logout', onPress: this.handleLogout }} rightButton={{ icon: 'glasses' }} />
|
<Header title='Work Item Map' leftButton={{ icon: 'logout', onPress: this.handleLogoutPress }} rightButton={{ icon: 'glasses', onPress: this.handleGlassesPress }} />
|
||||||
<MapView
|
<MapView
|
||||||
style={{
|
style={{
|
||||||
width: '100%', height: '50%',
|
width: '100%', height: '50%',
|
||||||
@@ -103,7 +109,7 @@ export class Home extends React.Component {
|
|||||||
<Text style={{ fontSize: 20 }}>{item.title}</Text>
|
<Text style={{ fontSize: 20 }}>{item.title}</Text>
|
||||||
<Text style={{ fontSize: 14, color: 'gray' }}>{item.location}</Text>
|
<Text style={{ fontSize: 14, color: 'gray' }}>{item.location}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this._handleItemSelect(item, index))} >
|
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this.handleItemSelect(item, index))} >
|
||||||
<Icon name='rightArrow' size={16} />
|
<Icon name='rightArrow' size={16} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -116,12 +122,13 @@ export class Home extends React.Component {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: 45,
|
height: 45,
|
||||||
backgroundColor: '#F4F4F4',
|
backgroundColor: '#F4F4F4',
|
||||||
|
...ifIphoneX({ marginBottom: 22 }, {}),
|
||||||
}}>
|
}}>
|
||||||
<TouchableOpacity onPress={this._handleMyLocation}>
|
<TouchableOpacity onPress={this.handleMyLocationPress}>
|
||||||
<Icon name='center' size={24} style={{ marginLeft: 15, tintColor: 'gray' }} />
|
<Icon name='center' size={24} style={{ marginLeft: 15, tintColor: 'gray' }} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={{ color: 'gray', fontSize: 20, }}>Hide List</Text>
|
<Text style={{ color: 'gray', fontSize: 20, }}>Hide List</Text>
|
||||||
<TouchableOpacity onPress={this._handleAdminButton}>
|
<TouchableOpacity onPress={this.handleWorkItemsListPress}>
|
||||||
<Icon name='settings' size={24} style={{ marginRight: 15, tintColor: 'gray' }} />
|
<Icon name='settings' size={24} style={{ marginRight: 15, tintColor: 'gray' }} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,32 +1,113 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, View, TouchableOpacity, Image, ScrollView, Picker, Text } from 'react-native'
|
import {
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
Image,
|
||||||
|
ScrollView,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
TouchableOpacity
|
||||||
|
} from 'react-native'
|
||||||
|
import { Icon, Header, PhotoButton } from '../ui'
|
||||||
|
import MapView, { Marker } from 'react-native-maps'
|
||||||
|
import { FormBinder } from 'react-form-binder'
|
||||||
|
import { BoundInput, BoundButton } from '../ui'
|
||||||
|
import autobind from 'autobind-decorator'
|
||||||
|
import { ifIphoneX, isIphoneX } from 'react-native-iphone-x-helper'
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexGrow: 1,
|
||||||
|
backgroundColor: '#DDDDDD',
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
width: '94%',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
alignSelf: 'center',
|
||||||
|
marginTop: '3%',
|
||||||
|
shadowColor: 'gray',
|
||||||
|
shadowOffset: { width: 2, height: 2 },
|
||||||
|
shadowRadius: 2,
|
||||||
|
shadowOpacity: .5,
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 14,
|
||||||
|
marginBottom: 4,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export class WorkItem extends React.Component {
|
export class WorkItem extends React.Component {
|
||||||
static styles = StyleSheet.create({
|
static bindings = {
|
||||||
container: {
|
location: {
|
||||||
height: '100%',
|
isValid: true,
|
||||||
width: '100%',
|
|
||||||
backgroundColor: '#DDDDDD',
|
|
||||||
},
|
},
|
||||||
})
|
details: {
|
||||||
|
isValid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
binder: new FormBinder({}, WorkItem.bindings),
|
||||||
|
messageModal: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleBackPress() {
|
||||||
|
const { history } = this.props
|
||||||
|
|
||||||
|
if (history.length > 1) {
|
||||||
|
history.goBack()
|
||||||
|
} else {
|
||||||
|
history.replace('/home')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { binder } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={WorkItem.styles.container}>
|
<KeyboardAvoidingView
|
||||||
<View style={{ width: '94%', backgroundColor: 'white', alignSelf: 'center', marginTop: '3%', shadowColor: 'gray', shadowOffset: { width: 2, height: 2 }, shadowRadius: 2, shadowOpacity: .5 }}>
|
style={{ width: '100%', height: '100%' }}
|
||||||
<Text>Work Item</Text>
|
behavior='padding'
|
||||||
<Picker selectedValue={this.props.item.type}>
|
keyboardVerticalOffset={Platform.select({ios: 0, android: -220})}>
|
||||||
<Picker.Item label='Work Item' value='work' />
|
<Header title='Work Item' leftButton={{ icon: 'back', onPress: this.handleBackPress }} rightButton={{ icon: 'done' }} />
|
||||||
<Picker.Item label='Inspection' value='inspection' />
|
<ScrollView style={styles.container}>
|
||||||
<Picker.Item label='Complaint' value='complaint' />
|
<View style={styles.panel}>
|
||||||
</Picker>
|
<MapView
|
||||||
</View>
|
style={{
|
||||||
<View style={{ width: '94%', height: 300, backgroundColor: 'white', alignSelf: 'center', marginTop: '3%', shadowColor: 'gray', shadowOffset: { width: 2, height: 2 }, shadowRadius: 2, shadowOpacity: .5 }} />
|
width: '100%',
|
||||||
</ScrollView>
|
height: 400,
|
||||||
);
|
marginBottom: 10,
|
||||||
|
}}
|
||||||
|
zoomControlEnabled
|
||||||
|
initialRegion={{
|
||||||
|
latitude: 43.653908,
|
||||||
|
longitude: -79.384293,
|
||||||
|
latitudeDelta: 0.0922,
|
||||||
|
longitudeDelta: 0.0421,
|
||||||
|
}} />
|
||||||
|
<BoundInput binder={binder} name='location' label='Location:' />
|
||||||
|
</View>
|
||||||
|
<View style={styles.panel}>
|
||||||
|
<Text style={styles.label}>Pictures:</Text>
|
||||||
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||||
|
<PhotoButton />
|
||||||
|
<PhotoButton />
|
||||||
|
<PhotoButton />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.panel}>
|
||||||
|
<BoundInput binder={binder} name='details' label='Details:' message='You must supply details for the work item' />
|
||||||
|
</View>
|
||||||
|
{ isIphoneX ? <View style={{ height: 30, width: '100%' }} /> : null }
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, View, TouchableOpacity, Image, FlatList, Text} from 'react-native'
|
import { StyleSheet, View, TouchableOpacity, Image, FlatList, Text} from 'react-native'
|
||||||
|
import { Icon, Header } from '../ui'
|
||||||
import autobind from 'autobind-decorator'
|
import autobind from 'autobind-decorator'
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
@@ -37,35 +38,51 @@ const inspectionTypes = {
|
|||||||
export class WorkItemList extends React.Component {
|
export class WorkItemList extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this._handleItemSelect = this._handleItemSelect.bind(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
_handleItemSelect(item, index) {
|
handleItemSelect(item, index) {
|
||||||
this.props.history.push('/activity')
|
this.props.history.push('/activity')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleAddPress(item, index) {
|
||||||
|
this.props.history.push('/workitem')
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleBackPress() {
|
||||||
|
const { history } = this.props
|
||||||
|
|
||||||
|
if (history.length > 1) {
|
||||||
|
history.goBack()
|
||||||
|
} else {
|
||||||
|
history.replace('/home')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<FlatList
|
<Header title='Work Items' leftButton={{ icon: 'back', onPress: this.handleBackPress }} rightButton={{ icon: 'add', onPress: this.handleAddPress }} />
|
||||||
style={{ width: '100%', flexGrow: 1, paddingTop: 20, paddingBottom: 20 }}
|
<FlatList
|
||||||
data={data}
|
style={{ width: '100%', flexGrow: 1, paddingTop: 20, paddingBottom: 20 }}
|
||||||
renderItem={({item, index}) => {
|
data={data}
|
||||||
return (
|
renderItem={({item, index}) => {
|
||||||
<View style={{ flexDirection: 'row', height: 50 }}>
|
return (
|
||||||
<Text style={{ fontSize: 8, width: 45, marginLeft: 15, alignSelf: 'center' }}>{item.state.toUpperCase()}</Text>
|
<View style={{ flexDirection: 'row', height: 50 }}>
|
||||||
<View style={{ flexDirection: 'column', width: '75%' }}>
|
<Text style={{ fontSize: 8, width: 45, marginLeft: 15, alignSelf: 'center' }}>{item.state.toUpperCase()}</Text>
|
||||||
<Text style={{ fontSize: 20 }}>{Admin.inspectionTypes[item.type].title}</Text>
|
<View style={{ flexDirection: 'column', width: '75%' }}>
|
||||||
<Text style={{ fontSize: 14, color: 'gray' }}>{item.location}</Text>
|
<Text style={{ fontSize: 20 }}>{inspectionTypes[item.type].title}</Text>
|
||||||
|
<Text style={{ fontSize: 14, color: 'gray' }}>{item.location}</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this._handleItemSelect(item, index))} >
|
||||||
|
<Icon name='rightArrow' size={16} />
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this._handleItemSelect(item, index))} >
|
)
|
||||||
<Image source={rightArrowImage} style={{ width: 16, height: 16 }} />
|
}} />
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}} />
|
|
||||||
</View>
|
</View>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default class App extends React.Component {
|
|||||||
<Route exact path='/login' component={Login}/>
|
<Route exact path='/login' component={Login}/>
|
||||||
<Route exact path='/logout' component={Logout}/>
|
<Route exact path='/logout' component={Logout}/>
|
||||||
<ProtectedRoute exact path='/home' component={Home}/>
|
<ProtectedRoute exact path='/home' component={Home}/>
|
||||||
<ProtectedRoute exact path='/viewer' component={ARViewer}/>
|
<ProtectedRoute exact path='/arviewer' component={ARViewer}/>
|
||||||
<ProtectedRoute exact path='/activity' component={Activity}/>
|
<ProtectedRoute exact path='/activity' component={Activity}/>
|
||||||
<ProtectedRoute exact admin path='/workitem' component={WorkItem}/>
|
<ProtectedRoute exact admin path='/workitem' component={WorkItem}/>
|
||||||
<ProtectedRoute exact admin path='/workitemlist' component={WorkItemList}/>
|
<ProtectedRoute exact admin path='/workitemlist' component={WorkItemList}/>
|
||||||
|
|||||||
53
mobile/src/ui/BoundOptionStrip.js
Normal file
53
mobile/src/ui/BoundOptionStrip.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { View, Text } from 'react-native'
|
||||||
|
import { OptionStrip } from '.'
|
||||||
|
import autobind from 'autobind-decorator'
|
||||||
|
|
||||||
|
export class BoundOptionStrip extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string,
|
||||||
|
binder: PropTypes.object.isRequired,
|
||||||
|
options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = props.binder.getFieldState(props.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleValueChange() {
|
||||||
|
const { binder, name } = this.props
|
||||||
|
const state = binder.getFieldState(name)
|
||||||
|
|
||||||
|
if (!state.readOnly && !state.disabled) {
|
||||||
|
this.setState(binder.updateFieldValue(name, !state.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.binder !== this.props.binder) {
|
||||||
|
this.setState(nextProps.binder.getFieldState(nextProps.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { name, label, options } = this.props
|
||||||
|
const { visible, disabled, value } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{
|
||||||
|
display: visible ? 'flex' : 'none',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<Text style={{ color: 'black', fontSize: 14, marginBottom: 5 }}>{label}</Text>
|
||||||
|
<OptionStrip
|
||||||
|
value={value}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,12 +16,12 @@ export class BoundSwitch extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleValueChange() {
|
handleValueChange(newValue) {
|
||||||
const { binder, name } = this.props
|
const { binder, name } = this.props
|
||||||
const state = binder.getFieldState(name)
|
const state = binder.getFieldState(name)
|
||||||
|
|
||||||
if (!state.readOnly && !state.disabled) {
|
if (!state.readOnly && !state.disabled) {
|
||||||
this.setState(binder.updateFieldValue(name, !state.value))
|
this.setState(binder.updateFieldValue(name, newValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export class Header extends Component {
|
|||||||
rightButton: PropTypes.shape(headerButtonShape),
|
rightButton: PropTypes.shape(headerButtonShape),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
rightButton: { icon: 'none' },
|
||||||
|
leftButton: { icon: 'none' },
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { title, leftButton, rightButton } = this.props
|
const { title, leftButton, rightButton } = this.props
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import { Image } from 'react-native'
|
import { Image, View } from 'react-native'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
const images = {
|
const images = {
|
||||||
@@ -11,6 +11,8 @@ const images = {
|
|||||||
rightArrow: require('./images/right-arrow.png'),
|
rightArrow: require('./images/right-arrow.png'),
|
||||||
search: require('./images/search.png'),
|
search: require('./images/search.png'),
|
||||||
settings: require('./images/settings.png'),
|
settings: require('./images/settings.png'),
|
||||||
|
add: require('./images/add.png'),
|
||||||
|
done: require('./images/done.png'),
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Icon extends Component {
|
export class Icon extends Component {
|
||||||
@@ -27,11 +29,14 @@ export class Icon extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { size, name, margin, style } = this.props
|
let { name, margin, style } = this.props
|
||||||
let source = images[name] || images['hand']
|
let size = this.props.size - (margin * 2)
|
||||||
|
let source = images[name]
|
||||||
|
|
||||||
size -= margin * 2
|
if (!source) {
|
||||||
|
return <View style={{ width: size, height: size, margin }} />
|
||||||
return <Image style={[{ width: size, height: size, margin }, style]} source={source} resizeMode='stretch' />
|
} else {
|
||||||
|
return <Image style={[{ width: size, height: size, margin }, style]} source={source} resizeMode='stretch' />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
70
mobile/src/ui/OptionStrip.js
Normal file
70
mobile/src/ui/OptionStrip.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React, { Component } from 'react'
|
||||||
|
import { View, Text, TouchableHighlight } from 'react-native'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import autobind from 'autobind-decorator';
|
||||||
|
|
||||||
|
export class OptionStrip extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.string, text: PropTypes.string })).isRequired,
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
onValueChanged: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
selectedOption: this.getSelectedOption(props.options, props.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
getSelectedOption(options, value) {
|
||||||
|
return options.find((option) => (value === option.value)) || options[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(newProps) {
|
||||||
|
if (newProps.options !== props.options || newProps.value !== props.value) {
|
||||||
|
this.setState({ selectedIndex: this.getSelectedIndex(newProps.options, newProps.value)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handlePress(option) {
|
||||||
|
const { onValueChanged } = this.props
|
||||||
|
|
||||||
|
this.setState({ selectedOption: option })
|
||||||
|
if (onValueChanged) {
|
||||||
|
onValueChanged(option.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { style, options, value } = this.props
|
||||||
|
const { selectedOption } = this.state
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flexDirection: 'row' }}>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<TouchableHighlight
|
||||||
|
key={index}
|
||||||
|
underlayColor='#3BB0FD'
|
||||||
|
style={[
|
||||||
|
{ flexGrow: 1, flexBasis: 0, height: 40 },
|
||||||
|
option === selectedOption && { backgroundColor: '#3BB0FD' },
|
||||||
|
index === 0 && { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 },
|
||||||
|
index === options.length - 1 && { borderTopRightRadius: 6, borderBottomRightRadius: 6 }
|
||||||
|
]}
|
||||||
|
onPress={() => this.handlePress(option)}>
|
||||||
|
<View style={[
|
||||||
|
{ flex: 1, justifyContent: 'center', borderTopWidth: 1, borderBottomWidth: 1, borderLeftWidth: 1, borderColor: 'black' },
|
||||||
|
index === 0 && { borderTopLeftRadius: 6, borderBottomLeftRadius: 6 },
|
||||||
|
index === options.length - 1 && { borderRightWidth: 1, borderTopRightRadius: 6, borderBottomRightRadius: 6 }
|
||||||
|
]}>
|
||||||
|
<Text style={{ alignSelf: 'center', color: 'black' }}>{option.text}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableHighlight>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
mobile/src/ui/PhotoButton.js
Normal file
31
mobile/src/ui/PhotoButton.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
Image,
|
||||||
|
TouchableOpacity
|
||||||
|
} from 'react-native'
|
||||||
|
import { Icon } from '.'
|
||||||
|
import autobind from 'autobind-decorator'
|
||||||
|
|
||||||
|
// const styles = StyleSheet.create()
|
||||||
|
|
||||||
|
export class PhotoButton extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity>
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
width: 100,
|
||||||
|
height: 60,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: 'gray',
|
||||||
|
borderRadius: 4,
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<Icon name='add' size={24} style={{ alignSelf: 'center' }} />
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
mobile/src/ui/images/add.png
Normal file
BIN
mobile/src/ui/images/add.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 465 B |
BIN
mobile/src/ui/images/done.png
Normal file
BIN
mobile/src/ui/images/done.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 685 B |
@@ -1,5 +1,8 @@
|
|||||||
export { BoundSwitch } from './BoundSwitch'
|
export { BoundSwitch } from './BoundSwitch'
|
||||||
export { BoundInput } from './BoundInput'
|
export { BoundInput } from './BoundInput'
|
||||||
export { BoundButton } from './BoundButton'
|
export { BoundButton } from './BoundButton'
|
||||||
|
export { BoundOptionStrip } from './BoundOptionStrip'
|
||||||
export { Icon } from './Icon'
|
export { Icon } from './Icon'
|
||||||
export { Header } from './Header'
|
export { Header } from './Header'
|
||||||
|
export { PhotoButton } from './PhotoButton'
|
||||||
|
export { OptionStrip } from './OptionStrip'
|
||||||
|
|||||||
Reference in New Issue
Block a user