524 lines
15 KiB
JavaScript
524 lines
15 KiB
JavaScript
import React from "react"
|
|
import {
|
|
Text,
|
|
SectionList,
|
|
View,
|
|
TouchableOpacity,
|
|
TouchableHighlight,
|
|
PermissionsAndroid,
|
|
Platform,
|
|
} from "react-native"
|
|
import MapView, { Marker, Callout } from "react-native-maps"
|
|
import { Icon, Header, Geolocation } from "../ui"
|
|
import { MessageModal, WaitModal } from "../Modal"
|
|
import { api } from "../API"
|
|
import { reactAutoBind } from "auto-bind2"
|
|
import { ifIphoneX } from "react-native-iphone-x-helper"
|
|
import {
|
|
geoDistance,
|
|
workItemTypeText,
|
|
pad,
|
|
regionContainingPoints,
|
|
dotify,
|
|
} from "../util"
|
|
import { ensurePermissions } from "../App"
|
|
import { versionInfo } from "../version"
|
|
import { config } from "../config"
|
|
import KeyboardSpacer from "react-native-keyboard-spacer"
|
|
import hardhatPinImage from "./images/hardhat-pin.png"
|
|
import clipboardPinImage from "./images/clipboard-pin.png"
|
|
import questionPinImage from "./images/question-pin.png"
|
|
import moment from "moment"
|
|
|
|
export class Home extends React.Component {
|
|
constructor(props) {
|
|
super(props)
|
|
reactAutoBind(this)
|
|
this.state = {
|
|
waitModal: null,
|
|
messageModal: null,
|
|
sections: [],
|
|
showWorkItems: true,
|
|
haveCameraPermission: false,
|
|
haveLocationPermission: false,
|
|
}
|
|
this.currentPosition = null
|
|
this.isMapReady = false
|
|
this.goToRegionWhenReady = false
|
|
this.region = null
|
|
|
|
if (Platform.OS !== "ios") {
|
|
ensurePermissions(
|
|
[
|
|
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
|
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
|
|
],
|
|
{
|
|
title: versionInfo.title,
|
|
message:
|
|
"This app needs these permissions so that you can " +
|
|
"find, access and photograph geo located items.",
|
|
},
|
|
(results) => {
|
|
if (
|
|
results[PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION] ===
|
|
PermissionsAndroid.RESULTS.GRANTED
|
|
) {
|
|
this.setState({ haveLocationPermission: true })
|
|
}
|
|
|
|
if (
|
|
results[PermissionsAndroid.PERMISSIONS.CAMERA] ===
|
|
PermissionsAndroid.RESULTS.GRANTED
|
|
) {
|
|
this.setState({ haveCameraPermission: true })
|
|
}
|
|
},
|
|
() => {
|
|
this.setState({
|
|
messageModal: {
|
|
icon: "hand",
|
|
message:
|
|
"You have denied the app access to phone features it needs to function. " +
|
|
"Some parts of the app are disabled.",
|
|
detail:
|
|
"To enable these features in future " +
|
|
"please go to Settings.",
|
|
},
|
|
})
|
|
}
|
|
)
|
|
} else {
|
|
navigator.geolocation.requestAuthorization()
|
|
this.state.haveLocationPermission = true
|
|
this.state.haveCameraPermission = true
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.loadWorkItems()
|
|
}
|
|
|
|
loadWorkItems() {
|
|
this.setState({ waitModal: { message: "Loading Work Items..." } })
|
|
api
|
|
.listTeams()
|
|
.then((list) => {
|
|
this.teams = list.items
|
|
return api.listWorkItemActivities()
|
|
})
|
|
.then((list) => {
|
|
this.setState({ waitModal: null })
|
|
|
|
list.items.forEach((item) => {
|
|
item.data.forEach((datum) => {
|
|
const team = this.teams.find((team) => team._id === datum.team)
|
|
|
|
datum.teamName = team ? team.name : "???"
|
|
})
|
|
})
|
|
this.setState({
|
|
sections: list.items,
|
|
})
|
|
|
|
this.region =
|
|
regionContainingPoints(
|
|
list.items.map((item) => item.coordinate),
|
|
config.homeRegionInset
|
|
) || config.initialRegion
|
|
|
|
if (this.isMapReady) {
|
|
this.mapView.animateToRegion(this.region)
|
|
} else {
|
|
this.goToRegionWhenReady = true
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
this.setState({
|
|
waitModal: null,
|
|
messageModal: {
|
|
icon: "hand",
|
|
message: "Unable to get a list of work items, activities and teams",
|
|
detail: err.message,
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
handleMessageDismiss() {
|
|
this.setState({ messageModal: null })
|
|
}
|
|
|
|
handleOnMapReady() {
|
|
if (this.goToRegionWhenReady) {
|
|
this.mapView.animateToRegion(this.region)
|
|
this.goToRegionWhenReady = false
|
|
}
|
|
|
|
this.isMapReady = true
|
|
}
|
|
|
|
handleMarkerPress(e, sectionIndex) {
|
|
if (this.sectionList) {
|
|
this.sectionList.scrollToLocation({
|
|
sectionIndex,
|
|
itemIndex: 0,
|
|
viewOffset: 45,
|
|
})
|
|
}
|
|
}
|
|
|
|
handleWorkItemsListPress() {
|
|
if (this.currentPosition) {
|
|
const { coords } = this.currentPosition
|
|
|
|
this.props.history.push(
|
|
`/workItemList?latLng=${coords.latitude},${coords.longitude}`
|
|
)
|
|
}
|
|
}
|
|
|
|
handleItemSelect(activity) {
|
|
this.props.history.push(`/activity?id=${activity._id}`)
|
|
}
|
|
|
|
handleSectionSelect(workItem) {
|
|
if (!this.isMapReady) {
|
|
return
|
|
}
|
|
|
|
const { latitude, longitude } = workItem.coordinate
|
|
|
|
this.region = {
|
|
latitude,
|
|
longitude,
|
|
latitudeDelta: config.homeRegionDelta,
|
|
longitudeDelta: config.homeRegionDelta,
|
|
}
|
|
this.mapView.animateToRegion(this.region)
|
|
}
|
|
|
|
handleLogoutPress() {
|
|
this.props.history.replace("/logout")
|
|
}
|
|
|
|
handleGlassesPress() {
|
|
const { sections: workItems } = this.state
|
|
|
|
if (this.currentPosition) {
|
|
const {
|
|
latitude: latitude1,
|
|
longitude: longitude1,
|
|
} = this.currentPosition.coords
|
|
let closestWorkItem = config.alwaysShowWorkItemInAR ? workItems[0] : null
|
|
let shortestDistance = config.minDistanceToItem
|
|
|
|
workItems.forEach((workItem) => {
|
|
const {
|
|
latitude: latitude2,
|
|
longitude: longitude2,
|
|
} = workItem.coordinate
|
|
const distance =
|
|
geoDistance(latitude1, longitude1, latitude2, longitude2, "K") * 1000
|
|
|
|
if (distance <= shortestDistance) {
|
|
closestWorkItem = workItem
|
|
shortestDistance = distance
|
|
}
|
|
})
|
|
|
|
this.props.history.push(
|
|
`/arviewer${
|
|
closestWorkItem
|
|
? "?workItemId=" +
|
|
closestWorkItem._id +
|
|
"&workItemType=" +
|
|
closestWorkItem.workItemType
|
|
: ""
|
|
}`
|
|
)
|
|
}
|
|
}
|
|
|
|
handleMyLocationPress() {
|
|
if (this.currentPosition && this.isMapReady) {
|
|
const { coords } = this.currentPosition
|
|
|
|
this.mapView.animateToCoordinate({
|
|
latitude: coords.latitude,
|
|
longitude: coords.longitude,
|
|
})
|
|
}
|
|
}
|
|
|
|
handleToggleWorkItemsList() {
|
|
const newShowWorkItems = !this.state.showWorkItems
|
|
|
|
this.setState({ showWorkItems: newShowWorkItems })
|
|
|
|
if (newShowWorkItems) {
|
|
this.loadWorkItems()
|
|
}
|
|
}
|
|
|
|
handleCalloutPress(workItem) {
|
|
if (api.loggedInUser.administrator) {
|
|
this.props.history.push(
|
|
`/arviewer?workItemId=${workItem._id}&workItemType=${
|
|
workItem.workItemType
|
|
}`
|
|
)
|
|
}
|
|
}
|
|
|
|
handlePositionUpdate(position) {
|
|
this.currentPosition = position
|
|
}
|
|
|
|
getCoordinateDistance(coordinate) {
|
|
if (this.currentPosition) {
|
|
const { coords } = this.currentPosition
|
|
const { latitude, longitude } = coordinate
|
|
|
|
return (
|
|
geoDistance(
|
|
coords.latitude,
|
|
coords.longitude,
|
|
latitude,
|
|
longitude,
|
|
"K"
|
|
).toFixed(2) + " km"
|
|
)
|
|
} else {
|
|
return "?"
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
sections,
|
|
showWorkItems,
|
|
messageModal,
|
|
waitModal,
|
|
haveCameraPermission,
|
|
haveLocationPermission,
|
|
} = this.state
|
|
|
|
return (
|
|
<View
|
|
style={{
|
|
flexDirection: "column",
|
|
height: "100%",
|
|
backgroundColor: "#FFFFFF",
|
|
}}>
|
|
<Header
|
|
title="Work Item Map"
|
|
leftButton={{ icon: "logout", onPress: this.handleLogoutPress }}
|
|
rightButton={{ icon: "glasses", onPress: this.handleGlassesPress }}
|
|
disabled={!haveCameraPermission || !haveLocationPermission}
|
|
/>
|
|
<MapView
|
|
ref={(ref) => {
|
|
this.mapView = ref
|
|
}}
|
|
style={[
|
|
{
|
|
width: "100%",
|
|
},
|
|
showWorkItems && { height: "40%" },
|
|
!showWorkItems && { flexGrow: 1 },
|
|
]}
|
|
showsUserLocation
|
|
showsBuildings={false}
|
|
showsTraffic={false}
|
|
showsIndoors={false}
|
|
zoomControlEnabled={false}
|
|
showsMyLocationButton={false}
|
|
initialRegion={config.initialRegion}
|
|
onMapReady={this.handleOnMapReady}>
|
|
{sections.map((workItem, index) => (
|
|
<Marker
|
|
key={index}
|
|
coordinate={workItem.coordinate}
|
|
anchor={{ x: 0.5, y: 1.0 }}
|
|
image={
|
|
workItem.workItemType === "inspection"
|
|
? clipboardPinImage
|
|
: workItem.workItemType === "complaint"
|
|
? questionPinImage
|
|
: hardhatPinImage
|
|
}
|
|
onPress={(e) => this.handleMarkerPress(e, index)}>
|
|
<Callout onPress={() => this.handleCalloutPress(workItem)}>
|
|
<View>
|
|
<Text>
|
|
{pad(workItem.ticketNumber, 4) +
|
|
": " +
|
|
workItemTypeText[workItem.workItemType] +
|
|
" (" +
|
|
this.getCoordinateDistance(workItem.coordinate) +
|
|
")"}
|
|
</Text>
|
|
<Text>{dotify(workItem.address)}</Text>
|
|
</View>
|
|
</Callout>
|
|
</Marker>
|
|
))}
|
|
</MapView>
|
|
<View
|
|
style={{
|
|
display: showWorkItems ? "flex" : "none",
|
|
flexDirection: "column",
|
|
flexGrow: 1,
|
|
flexBasis: 0,
|
|
width: "100%",
|
|
}}>
|
|
<SectionList
|
|
ref={(ref) => (this.sectionList = ref)}
|
|
style={{ width: "100%", flexGrow: 1 }}
|
|
sections={sections}
|
|
stickySectionHeadersEnabled={true}
|
|
renderSectionHeader={({ section: workItem }) => (
|
|
<TouchableHighlight
|
|
style={{
|
|
paddingLeft: 8,
|
|
height: 45,
|
|
backgroundColor: "#F4F4F4",
|
|
}}
|
|
underlayColor="#EEEEEE"
|
|
onPress={() => this.handleSectionSelect(workItem)}>
|
|
<View
|
|
key={workItem._id}
|
|
style={{
|
|
height: "100%",
|
|
width: "100%",
|
|
flexDirection: "row",
|
|
justifyContent: "flex-start",
|
|
alignItems: "center",
|
|
}}>
|
|
<Icon
|
|
name={
|
|
workItem.workItemType === "order"
|
|
? "hardhat"
|
|
: workItem.workItemType === "complaint"
|
|
? "question"
|
|
: "clipboard"
|
|
}
|
|
size={16}
|
|
style={{ marginRight: 10 }}
|
|
/>
|
|
<Text style={{ fontSize: 16 }}>
|
|
{workItemTypeText[workItem.workItemType].toUpperCase()}{" "}
|
|
{pad(workItem.ticketNumber, 4)}
|
|
</Text>
|
|
</View>
|
|
</TouchableHighlight>
|
|
)}
|
|
keyExtractor={(item) => item._id}
|
|
renderItem={({ item: activity, section }) => {
|
|
return (
|
|
<TouchableHighlight
|
|
style={{
|
|
height: 50,
|
|
paddingLeft: 10,
|
|
paddingRight: 20,
|
|
backgroundColor: "white",
|
|
}}
|
|
underlayColor="#EEEEEE"
|
|
onPress={() => this.handleItemSelect(activity)}>
|
|
<View
|
|
style={{
|
|
height: "100%",
|
|
width: "100%",
|
|
flexDirection: "row",
|
|
}}>
|
|
<Text
|
|
style={{
|
|
fontSize: 9,
|
|
width: 45,
|
|
alignSelf: "center",
|
|
}}>
|
|
{activity.status.toUpperCase()}
|
|
</Text>
|
|
<View
|
|
style={{
|
|
flexGrow: 1,
|
|
flexBasis: 0,
|
|
flexDirection: "column",
|
|
marginLeft: 10,
|
|
}}>
|
|
<Text style={{ fontSize: 20, fontWeight: "bold" }}>
|
|
{activity.resolution}
|
|
</Text>
|
|
<Text style={{ fontSize: 14, color: "gray" }}>
|
|
{activity.teamName +
|
|
" | " +
|
|
dotify(moment(activity.createdAt).format(), -15)}
|
|
</Text>
|
|
</View>
|
|
<Icon
|
|
name="rightArrow"
|
|
size={16}
|
|
style={{ alignSelf: "center" }}
|
|
/>
|
|
</View>
|
|
</TouchableHighlight>
|
|
)
|
|
}}
|
|
/>
|
|
</View>
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
width: "100%",
|
|
height: 55,
|
|
backgroundColor: "#F4F4F4",
|
|
...ifIphoneX({ marginBottom: 22 }, {}),
|
|
}}>
|
|
<TouchableOpacity onPress={this.handleMyLocationPress}>
|
|
<Icon
|
|
name="center"
|
|
size={24}
|
|
style={{ marginLeft: 15, tintColor: "gray" }}
|
|
/>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={this.handleToggleWorkItemsList}>
|
|
<Text style={{ color: "gray", fontSize: 20 }}>
|
|
{showWorkItems ? "Hide List" : "Show List"}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={this.handleWorkItemsListPress}>
|
|
<Icon
|
|
name="settings"
|
|
size={24}
|
|
style={{
|
|
display: api.loggedInUser.administrator ? "flex" : "none",
|
|
marginRight: 15,
|
|
tintColor: "gray",
|
|
}}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
{haveLocationPermission && (
|
|
<Geolocation onUpdate={this.handlePositionUpdate} />
|
|
)}
|
|
<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>
|
|
)
|
|
}
|
|
}
|