525 lines
15 KiB
JavaScript
525 lines
15 KiB
JavaScript
import React from "react"
|
|
import {
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
SectionList,
|
|
Image,
|
|
View,
|
|
TouchableOpacity,
|
|
TouchableHighlight,
|
|
PermissionsAndroid,
|
|
Platform,
|
|
} from "react-native"
|
|
import MapView, { Marker, Callout } from "react-native-maps"
|
|
import { Icon, Header } from "../ui"
|
|
import { MessageModal } from "../Modal"
|
|
import { api } from "../API"
|
|
import autobind from "autobind-decorator"
|
|
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"
|
|
|
|
const neverAskForLocationPermissionKeyName = "NeverAskForLocationPermission"
|
|
const neverAskForCameraKeyName = "NeverAskForCameraPermission"
|
|
|
|
export class Home extends React.Component {
|
|
constructor(props) {
|
|
super(props)
|
|
this.state = {
|
|
sections: [],
|
|
showWorkItems: true,
|
|
region: config.initialRegion,
|
|
positionInfo: null,
|
|
haveCameraPermission: false,
|
|
workItemDistance: -1,
|
|
}
|
|
|
|
this.watchId = null
|
|
|
|
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 })
|
|
navigator.geolocation.getCurrentPosition(this.handlePositionChange)
|
|
}
|
|
|
|
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.",
|
|
},
|
|
})
|
|
}
|
|
)
|
|
|
|
api
|
|
.listTeams()
|
|
.then((list) => {
|
|
this.teams = list.items
|
|
return api.listWorkItemActivities()
|
|
})
|
|
.then((list) => {
|
|
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,
|
|
region:
|
|
regionContainingPoints(
|
|
list.items.map((item) => item.coordinate),
|
|
0.02
|
|
) || this.state.region,
|
|
})
|
|
})
|
|
.catch((err) => {
|
|
this.setState({
|
|
messageModal: {
|
|
icon: "hand",
|
|
message: "Unable to get a list of work items, activities and teams",
|
|
detail: err.message,
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.watchId) {
|
|
navigator.geolocation.clearWatch(this.watchId)
|
|
this.watchId = null
|
|
}
|
|
}
|
|
|
|
@autobind
|
|
handleMessageDismiss() {
|
|
this.setState({ messageModal: null })
|
|
}
|
|
|
|
@autobind
|
|
handlePositionChange(positionInfo) {
|
|
this.setState({ positionInfo })
|
|
}
|
|
|
|
@autobind
|
|
handleMarkerPress(e, sectionIndex) {
|
|
if (this.sectionList) {
|
|
this.sectionList.scrollToLocation({
|
|
sectionIndex,
|
|
itemIndex: 0,
|
|
viewOffset: 45,
|
|
})
|
|
}
|
|
|
|
if (this.state.positionInfo) {
|
|
const coords = this.state.positionInfo.coords
|
|
const workItem = this.state.sections[sectionIndex]
|
|
const { latitude, longitude } = workItem.coordinate
|
|
|
|
this.setState({
|
|
workItemDistance: geoDistance(
|
|
coords.latitude,
|
|
coords.longitude,
|
|
latitude,
|
|
longitude,
|
|
"K"
|
|
).toFixed(2),
|
|
})
|
|
}
|
|
}
|
|
|
|
@autobind
|
|
handleWorkItemsListPress() {
|
|
const { positionInfo } = this.state
|
|
|
|
if (positionInfo) {
|
|
const { coords } = positionInfo
|
|
|
|
this.props.history.push(
|
|
`/workItemList?latLng=${coords.latitude},${coords.longitude}`
|
|
)
|
|
} else {
|
|
this.props.history.push("/workItemList")
|
|
}
|
|
}
|
|
|
|
@autobind
|
|
handleItemSelect(activity) {
|
|
this.props.history.push(`/activity?id=${activity._id}`)
|
|
}
|
|
|
|
@autobind
|
|
handleSectionSelect(workItem) {
|
|
const { latitude, longitude } = workItem.coordinate
|
|
const region = {
|
|
latitude,
|
|
longitude,
|
|
latitudeDelta: 0.01,
|
|
longitudeDelta: 0.01,
|
|
}
|
|
this.setState({ region })
|
|
}
|
|
|
|
@autobind
|
|
handleLogoutPress() {
|
|
this.props.history.replace("/logout")
|
|
}
|
|
|
|
@autobind
|
|
handleGlassesPress() {
|
|
const { sections: workItems } = this.state
|
|
const {
|
|
latitude: latitude1,
|
|
longitude: longitude1,
|
|
} = this.state.positionInfo.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
|
|
: ""
|
|
}`
|
|
)
|
|
}
|
|
|
|
@autobind
|
|
handleMyLocationPress() {
|
|
if (this.state.positionInfo) {
|
|
const coords = this.state.positionInfo.coords
|
|
|
|
this.map.animateToCoordinate({
|
|
latitude: coords.latitude,
|
|
longitude: coords.longitude,
|
|
})
|
|
}
|
|
}
|
|
|
|
@autobind
|
|
handleToggleWorkItemsList() {
|
|
this.setState({ showWorkItems: !this.state.showWorkItems })
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
sections,
|
|
showWorkItems,
|
|
region,
|
|
positionInfo,
|
|
messageModal,
|
|
haveCameraPermission,
|
|
haveLocationPermission,
|
|
workItemDistance,
|
|
} = this.state
|
|
|
|
if (!this.watchId && haveLocationPermission) {
|
|
this.watchId = navigator.geolocation.watchPosition(
|
|
this.handlePositionChange,
|
|
null,
|
|
{ distanceFilter: 10 }
|
|
)
|
|
}
|
|
|
|
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={
|
|
!(
|
|
positionInfo &&
|
|
positionInfo.coords.accuracy <= config.minGPSAccuracy
|
|
) ||
|
|
!haveCameraPermission ||
|
|
!haveLocationPermission
|
|
}
|
|
/>
|
|
<MapView
|
|
ref={(ref) => {
|
|
this.map = ref
|
|
}}
|
|
style={[
|
|
{
|
|
width: "100%",
|
|
},
|
|
showWorkItems && { height: "40%" },
|
|
!showWorkItems && { flexGrow: 1 },
|
|
]}
|
|
showsUserLocation
|
|
showsBuildings={false}
|
|
showsTraffic={false}
|
|
showsIndoors={false}
|
|
zoomControlEnabled={false}
|
|
showsMyLocationButton={false}
|
|
region={region}>
|
|
{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>
|
|
<View>
|
|
<Text>
|
|
{pad(workItem.ticketNumber, 4) +
|
|
": " +
|
|
workItemTypeText[workItem.workItemType] +
|
|
" (" +
|
|
(workItemDistance > 0
|
|
? workItemDistance.toString()
|
|
: "?") +
|
|
" km)"}
|
|
</Text>
|
|
<Text>{dotify(workItem.address)}</Text>
|
|
</View>
|
|
</Callout>
|
|
</Marker>
|
|
))}
|
|
</MapView>
|
|
<View
|
|
style={{
|
|
display: showWorkItems ? "flex" : "none",
|
|
flexDirection: "column",
|
|
flexGrow: 1,
|
|
flexBasis: 0,
|
|
width: "100%",
|
|
}}>
|
|
{/*
|
|
// TODO: Search feature
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
width: "100%",
|
|
height: 40,
|
|
backgroundColor: "white",
|
|
}}>
|
|
<Icon
|
|
name="search"
|
|
size={16}
|
|
style={{ marginLeft: 10, marginRight: 5, tintColor: "gray" }}
|
|
/>
|
|
<TextInput
|
|
style={{ flexGrow: 1, flexBasis: 0, height: "100%" }}
|
|
underlineColorAndroid="white"
|
|
placeholder="Search"
|
|
/>
|
|
<Icon
|
|
style={{ marginLeft: 5, marginRight: 10 }}
|
|
name="cancel"
|
|
size={16}
|
|
/>
|
|
</View> */}
|
|
<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())}
|
|
</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
|
|
disabled={!positionInfo}
|
|
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>
|
|
<MessageModal
|
|
open={!!messageModal}
|
|
icon={messageModal ? messageModal.icon : ""}
|
|
message={messageModal ? messageModal.message : ""}
|
|
detail={messageModal ? messageModal.detail : ""}
|
|
onDismiss={messageModal && this.handleMessageDismiss}
|
|
/>
|
|
{Platform.OS === "ios" && <KeyboardSpacer />}
|
|
</View>
|
|
)
|
|
}
|
|
}
|