436 lines
11 KiB
JavaScript
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>
|
|
)
|
|
}
|
|
}
|