Files
deighton-ar/mobile/src/WorkItem/WorkItem.js
2018-05-16 13:15:56 -07:00

436 lines
11 KiB
JavaScript

import React from "react"
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 {
BoundInput,
BoundButton,
BoundHeader,
Icon,
Header,
BoundOptionStrip,
BoundPhotoPanel,
Geolocation,
} from "../ui"
import { MessageModal, ProgressModal, WaitModal } from "../Modal"
import autobind from "autobind-decorator"
import { ifIphoneX, isIphoneX } from "react-native-iphone-x-helper"
import KeyboardSpacer from "react-native-keyboard-spacer"
import { api } from "../API"
import "url-search-params-polyfill"
import { config } from "../config"
import { workItemTypeEnum, formatLatLng, parseLatLng } from "../util"
import PropTypes from "prop-types"
const styles = StyleSheet.create({
container: {
flexDirection: "column",
flexGrow: 1,
backgroundColor: "#DDDDDD",
},
panel: {
width: "94%",
backgroundColor: "white",
alignSelf: "center",
marginTop: "3%",
shadowColor: "gray",
shadowOffset: { width: 2, height: 2 },
shadowRadius: 2,
shadowOpacity: 0.5,
padding: 10,
},
})
export class WorkItem extends React.Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
static bindings = {
header: {
noValue: true,
isDisabled: (r) => !(r.anyModified && r.allValid),
},
location: {
isValid: (r, v) => v !== "",
isReadOnly: true,
alwaysGet: true,
initValue: null,
pre: (v) =>
v !== null ? formatLatLng(v.coordinates[1], v.coordinates[0]) : "",
post: (v) => parseLatLng(v),
},
address: {
isValid: true,
isReadOnly: true,
},
photos: {
initValue: [],
isValid: (r, v) => v && v.length > 0,
},
details: {
isValid: (r, v) => v !== "",
},
workItemType: {
isValid: (r, v) => v !== "",
alwaysGet: true,
},
addActivity: {
noValue: true,
isDisabled: (r, v) => r.anyModified,
},
}
constructor(props) {
super(props)
this.state = {
binder: new FormBinder({}, WorkItem.bindings),
messageModal: null,
waitModal: null,
progressModal: null,
region: config.initialRegion,
}
const { search } = this.props.location
const params = search ? new URLSearchParams(search) : { get: () => null }
const id = params.get("id")
if (id) {
api
.getWorkItem(id)
.then((workItem) => {
if (workItem) {
const [longitude, latitude] = workItem.location.coordinates
const region = {
latitude,
longitude,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
}
if (this.mapView) {
this.mapView.animateToRegion(region)
} else {
this.goToRegion = region
}
this.setState({
binder: new FormBinder(workItem, WorkItem.bindings),
})
}
})
.catch((err) => {
this.setState({
messageModal: {
icon: "hand",
message: "Unable to get work item details",
detail: err.message,
back: true,
},
})
})
}
}
componentWillUnmount() {
if (this.geoCodeTimer) {
clearTimeout(this.geoCodeTimer)
}
}
@autobind
handleBackPress() {
const { history } = this.props
if (history.length > 1) {
history.goBack()
} else {
history.replace("/home")
}
}
@autobind
handleDonePress() {
const { binder } = this.state
let obj = binder.getModifiedBindingValues()
if (!obj._id) {
api
.createWorkItem(obj)
.then((workItem) => {
this.handleBackPress()
})
.catch((error) => {
this.setState({
messageModal: {
icon: "hand",
message: "Unable to create work item",
detail: error.message,
},
})
})
} else {
api
.updateWorkItem(obj)
.then((workItem) => {
this.handleBackPress()
})
.catch((error) => {
this.setState({
messageModal: {
icon: "hand",
message: "Unable to update work item",
detail: error.message,
},
})
})
}
}
@autobind
handleMessageDismiss() {
const back = this.state.messageModal.back
this.setState({ messageModal: null })
if (back) {
this.handleBackPress()
}
}
@autobind
handleRegionChange(region) {
const { latitude, longitude } = region
if (this.latLngInput) {
this.latLngInput.handleChangeText(formatLatLng(latitude, longitude))
}
if (this.geoCodeTimer) {
clearTimeout(this.geoCodeTimer)
this.geoCodeTimer = null
}
this.geoCodeTimer = setTimeout(
() => this.handleStartAddressLookup({ latitude, longitude }),
config.geocodeDelayMilliseconds
)
}
@autobind
handleOnMapReady() {
if (this.goToRegion && this.mapView) {
this.mapView.animateToRegion(this.goToRegion)
this.goToRegion = null
}
}
@autobind
handleStartAddressLookup(latLng) {
api
.getAddress(latLng)
.then((address) => {
if (this.addressInput) {
this.addressInput.handleChangeText(address)
}
})
.catch(() => {
if (this.addressInput) {
this.addressInput.handleChangeText("")
}
})
}
@autobind
handleUploadStarted() {
this.setState({
progressModal: { message: "Uploading Photo..." },
uploadPercent: 0,
})
}
@autobind
handleUploadProgress(uploadData) {
if (this.state.progressModal) {
this.setState({
uploadPercent: Math.round(
uploadData.uploadedChunks / uploadData.numberOfChunks * 100
),
})
return true
} else {
return false
}
}
@autobind
handleUploadEnded(successful, uploadData) {
this.setState({ progressModal: null })
}
@autobind
handleUploadCanceled() {
this.setState({ progressModal: null })
}
@autobind
handleAddActivity() {
if (this.props.history) {
this.props.history.push(`/activity?workItemId=${this.state.binder._id}`)
}
}
@autobind
handlePositionUpdate(position) {
const { coords } = position
this.setState({
region: {
latitude: coords.latitude,
longitude: coords.longitude,
latitudeDelta: 0.02,
longitudeDelta: 0.02,
},
})
}
render() {
const {
binder,
messageModal,
waitModal,
progressModal,
uploadPercent,
region,
} = this.state
return (
<View style={{ flex: 1 }}>
<BoundHeader
binder={binder}
name="header"
title="Work Item"
leftButton={{ icon: "back", onPress: this.handleBackPress }}
rightButton={{ icon: "done", onPress: this.handleDonePress }}
/>
<ScrollView style={styles.container}>
<View style={styles.panel}>
<BoundOptionStrip
binder={binder}
name="workItemType"
label="Work Item Type:"
options={workItemTypeEnum}
message="Select a work item type"
/>
</View>
<View style={styles.panel}>
<BoundInput
binder={binder}
name="details"
lines={4}
label="Details:"
message="You must supply details for the work item"
/>
</View>
<View style={styles.panel}>
<View style={{ flexDirection: "column", justifyContent: "center" }}>
<MapView
ref={(ref) => (this.mapView = ref)}
style={{
flexDirection: "column",
justifyContent: "center",
width: "100%",
height: 400,
marginBottom: 10,
}}
zoomControlEnabled={false}
zoomEnabled={!binder._id}
scrollEnabled={!binder._id}
rotateEnabled={false}
pitchEnabled={false}
showsUserLocation
showsBuildings={false}
showsTraffic={false}
showsIndoors={false}
region={region}
onRegionChange={this.handleRegionChange}
onMapReady={this.handleOnMapReady}
/>
<Icon
name="target"
size={24}
pointerEvents={false}
style={{
position: "absolute",
alignSelf: "center",
}}
/>
</View>
<BoundInput
ref={(ref) => (this.latLngInput = ref)}
binder={binder}
name="location"
label="Location:"
/>
<BoundInput
ref={(ref) => (this.addressInput = ref)}
binder={binder}
name="address"
label="Address:"
/>
</View>
<View style={styles.panel}>
<BoundPhotoPanel
name="photos"
binder={binder}
onUploadStarted={this.handleUploadStarted}
onUploadEnded={this.handleUploadEnded}
onUploadProgress={this.handleUploadProgress}
/>
</View>
{api.loggedInUser.administrator &&
binder._id && (
<View style={styles.panel}>
<BoundButton
name="addActivity"
title="Add Activity"
binder={binder}
onPress={this.handleAddActivity}
/>
</View>
)}
{isIphoneX ? <View style={{ height: 30, width: "100%" }} /> : null}
</ScrollView>
<Geolocation onUpdate={this.handlePositionUpdate} watch={false} />
<ProgressModal
open={!!progressModal}
message={progressModal ? progressModal.message : ""}
percent={uploadPercent}
onCancel={this.handleUploadCanceled}
/>
<WaitModal
open={!!waitModal}
message={waitModal ? waitModal.message : ""}
/>
<MessageModal
open={!!messageModal}
icon={messageModal ? messageModal.icon : ""}
message={messageModal ? messageModal.message : ""}
detail={messageModal ? messageModal.detail : ""}
onDismiss={messageModal && this.handleMessageDismiss}
/>
{Platform.OS === "ios" && <KeyboardSpacer />}
</View>
)
}
}