Closing many tickets
@@ -2,7 +2,7 @@ import EventEmitter from "eventemitter3"
|
||||
import io from "socket.io-client"
|
||||
import { AsyncStorage } from "react-native"
|
||||
import autobind from "autobind-decorator"
|
||||
import { localIPAddr } from "./config"
|
||||
import { config } from "./config"
|
||||
|
||||
const authTokenKeyName = "AuthToken"
|
||||
const backendKeyName = "Backend"
|
||||
@@ -37,7 +37,7 @@ class API extends EventEmitter {
|
||||
static urls = {
|
||||
normal: "https://dar.kss.us.com/api",
|
||||
test: "https://dar-test.kss.us.com/api",
|
||||
local: `http://${localIPAddr || "localhost"}:3001`,
|
||||
local: `http://${config.localIPAddr || "localhost"}:3001`,
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@@ -384,6 +384,45 @@ class API extends EventEmitter {
|
||||
return this.delete("/activities/" + _id)
|
||||
}
|
||||
|
||||
getAddress(coord) {
|
||||
var promise = new Promise((resolve, reject) => {
|
||||
let fetchOptions = {
|
||||
method: "GET",
|
||||
mode: "no-cors",
|
||||
}
|
||||
|
||||
fetch(
|
||||
config.googleGeocodeURL +
|
||||
`?latlng=${coord.latitude},${coord.longitude}&key=${
|
||||
config.googleAPIKey
|
||||
}`,
|
||||
fetchOptions
|
||||
)
|
||||
.then((res) => {
|
||||
return Promise.all([Promise.resolve(res), res.json()])
|
||||
})
|
||||
.then((arr) => {
|
||||
let [res, responseBody] = arr
|
||||
|
||||
if (res.ok) {
|
||||
let address = ""
|
||||
|
||||
if (responseBody.results && responseBody.result.length > 0) {
|
||||
address = responseBody.results[0].formatted_address
|
||||
}
|
||||
|
||||
resolve(address)
|
||||
} else {
|
||||
reject(new APIError(res.status, responseBody.message))
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(new NetworkError(error.message))
|
||||
})
|
||||
})
|
||||
return promise
|
||||
}
|
||||
|
||||
upload(file, progressCallback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunkSize = 32 * 1024
|
||||
|
||||
@@ -14,26 +14,20 @@ import {
|
||||
} from "react-viro"
|
||||
import autobind from "autobind-decorator"
|
||||
import backImage from "./images/back.png"
|
||||
|
||||
const styles = {
|
||||
buttons: {
|
||||
height: 80,
|
||||
width: 80,
|
||||
},
|
||||
}
|
||||
import { config } from "../config"
|
||||
|
||||
const shapes = {
|
||||
hardhat: {
|
||||
shape: require("./models/hardhat_obj.obj"),
|
||||
materials: [require("./models/hardhat.mtl")],
|
||||
order: {
|
||||
source: require("./models/hardhat_obj.obj"),
|
||||
resources: [require("./models/hardhat.mtl")],
|
||||
},
|
||||
question: {
|
||||
shape: require("./models/question_obj.obj"),
|
||||
materials: [require("./models/question.mtl")],
|
||||
complaint: {
|
||||
source: require("./models/question_obj.obj"),
|
||||
resources: [require("./models/question.mtl")],
|
||||
},
|
||||
clipboard: {
|
||||
shape: require("./models/clipboard_obj.obj"),
|
||||
materials: [require("./models/clipboard.mtl")],
|
||||
inspection: {
|
||||
source: require("./models/clipboard_obj.obj"),
|
||||
resources: [require("./models/clipboard.mtl")],
|
||||
},
|
||||
}
|
||||
const distance = (vectorA, vectorB) => {
|
||||
@@ -139,11 +133,16 @@ class WorkItemSceneAR extends React.Component {
|
||||
|
||||
@autobind
|
||||
handleClick(position, source) {
|
||||
// this.props.history.replace("/activity")
|
||||
const { workItemId } = this.props
|
||||
|
||||
this.props.history.replace(
|
||||
`/activity${workItemId ? "?workItemId=" + workItemId : ""}`
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { position, scale, rotation, shouldBillboard } = this.state
|
||||
const shape = shapes[this.props.workItemType]
|
||||
|
||||
return (
|
||||
<ViroARScene ref={(ref) => (this.arScene = ref)}>
|
||||
@@ -165,14 +164,16 @@ class WorkItemSceneAR extends React.Component {
|
||||
shadowFarZ={6}
|
||||
shadowOpacity={0.9}
|
||||
/>
|
||||
<Viro3DObject
|
||||
position={[0, 0, -1]}
|
||||
source={shapes["hardhat"].shape}
|
||||
resources={shapes["hardhat"].materials}
|
||||
type="OBJ"
|
||||
onLoadEnd={this.handleLoadEnd}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
{shape && (
|
||||
<Viro3DObject
|
||||
position={[0, 0, -1]}
|
||||
source={shape.source}
|
||||
resources={shape.resources}
|
||||
type="OBJ"
|
||||
onLoadEnd={this.handleLoadEnd}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
)}
|
||||
<ViroSurface
|
||||
rotation={[-90, 0, 0]}
|
||||
position={[0, -0.001, 0]}
|
||||
@@ -190,6 +191,15 @@ class WorkItemSceneAR extends React.Component {
|
||||
export class ARViewer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const { search } = this.props.location
|
||||
|
||||
if (search) {
|
||||
const params = new URLSearchParams(search)
|
||||
|
||||
this.workItemId = params.get("workItemId")
|
||||
this.workItemType = params.get("workItemType")
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
@@ -202,13 +212,23 @@ export class ARViewer extends React.Component {
|
||||
<View style={{ width: "100%", height: "100%" }}>
|
||||
<ViroARSceneNavigator
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
apiKey="06F37B6A-74DA-4A83-965A-7DE2209A5C46"
|
||||
initialScene={{ scene: WorkItemSceneAR }}
|
||||
apiKey={config.viroAPIKey}
|
||||
initialScene={{
|
||||
scene: WorkItemSceneAR,
|
||||
passProps: {
|
||||
history: this.props.history,
|
||||
workItemId: this.workItemId,
|
||||
workItemType: this.workItemType,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<View style={{ position: "absolute", left: 30, right: 0, top: 50 }}>
|
||||
<TouchableHighlight
|
||||
style={styles.buttons}
|
||||
style={{
|
||||
height: 80,
|
||||
width: 80,
|
||||
}}
|
||||
onPress={this.handleBackPress}
|
||||
underlayColor={"#00000000"}>
|
||||
<Image source={backImage} />
|
||||
|
||||
@@ -15,11 +15,11 @@ import { FormBinder } from "react-form-binder"
|
||||
import {
|
||||
Icon,
|
||||
Header,
|
||||
PhotoButton,
|
||||
BoundInput,
|
||||
BoundButton,
|
||||
BoundOptionStrip,
|
||||
BoundHeader,
|
||||
PhotoPanel,
|
||||
} from "../ui"
|
||||
import { MessageModal } from "../Modal"
|
||||
import autobind from "autobind-decorator"
|
||||
@@ -73,6 +73,10 @@ export class Activity extends React.Component {
|
||||
isValid: (r, v) => v !== "",
|
||||
alwaysGet: true,
|
||||
},
|
||||
location: {
|
||||
isValid: (r, v) => v !== "",
|
||||
isReadOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -218,6 +222,8 @@ export class Activity extends React.Component {
|
||||
<BoundInput binder={binder} name="location" label="Location:" />
|
||||
<MapView
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
height: 400,
|
||||
marginTop: 10,
|
||||
@@ -238,31 +244,20 @@ export class Activity extends React.Component {
|
||||
longitude: -79.384293,
|
||||
latitudeDelta: 0.0922,
|
||||
longitudeDelta: 0.0421,
|
||||
}}
|
||||
/>
|
||||
}}>
|
||||
<Icon
|
||||
name="target"
|
||||
size={24}
|
||||
pointerEvents={false}
|
||||
style={{
|
||||
position: "absolute",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
/>
|
||||
</MapView>
|
||||
</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>
|
||||
<PhotoPanel />
|
||||
</View>
|
||||
{isIphoneX ? <View style={{ height: 30, width: "100%" }} /> : null}
|
||||
</ScrollView>
|
||||
|
||||
@@ -71,11 +71,11 @@ export default class App extends React.Component {
|
||||
<ProtectedRoute exact path="/home" component={Home} />
|
||||
<ProtectedRoute exact path="/arviewer" component={ARViewer} />
|
||||
<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"
|
||||
path="/workItemList"
|
||||
component={WorkItemList}
|
||||
/>
|
||||
<DefaultRoute />
|
||||
|
||||
@@ -3,5 +3,5 @@ import { Route, Redirect } from "react-router-native"
|
||||
|
||||
export const DefaultRoute = () => {
|
||||
// NOTE: When working on the app, change this to the page you are working on
|
||||
return <Route render={() => <Redirect to={"/workitem"} />} />
|
||||
return <Route render={() => <Redirect to={"/activity"} />} />
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Button,
|
||||
TouchableWithoutFeedback,
|
||||
} from "react-native"
|
||||
import { MessageModal, ApiModal } from "../Modal"
|
||||
import { MessageModal, ApiModal, WaitModal } from "../Modal"
|
||||
import logoImage from "./images/deighton.png"
|
||||
import { FormBinder } from "react-form-binder"
|
||||
import { api } from "../API"
|
||||
@@ -19,7 +19,7 @@ import KeyboardSpacer from "react-native-keyboard-spacer"
|
||||
import { versionInfo } from "../version"
|
||||
import autobind from "autobind-decorator"
|
||||
import { isIphoneX } from "react-native-iphone-x-helper"
|
||||
import { defaultUser } from "../config"
|
||||
import { config } from "../config"
|
||||
|
||||
export class Login extends React.Component {
|
||||
static bindings = {
|
||||
@@ -76,9 +76,10 @@ export class Login extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
binder: new FormBinder({ email: defaultUser }, Login.bindings),
|
||||
binder: new FormBinder({ email: config.defaultUser }, Login.bindings),
|
||||
messageModal: null,
|
||||
apiModal: null,
|
||||
waitModal: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +89,7 @@ export class Login extends React.Component {
|
||||
let { history } = this.props
|
||||
|
||||
if (obj) {
|
||||
this.setState({ waitModal: { message: "Loggin In..." } })
|
||||
api
|
||||
.login(obj.email.trim(), obj.password, obj.rememberMe)
|
||||
.then((user) => {
|
||||
@@ -95,6 +97,7 @@ export class Login extends React.Component {
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({
|
||||
waitModal: null,
|
||||
messageModal: {
|
||||
icon: "hand",
|
||||
message: "Unable to login",
|
||||
@@ -178,6 +181,10 @@ export class Login extends React.Component {
|
||||
{versionInfo.fullVersion}
|
||||
</Text>
|
||||
</View>
|
||||
<WaitModal
|
||||
open={!!waitModal}
|
||||
message={waitModal ? waitModal.message : ""}
|
||||
/>
|
||||
<MessageModal
|
||||
open={!!messageModal}
|
||||
icon={messageModal ? messageModal.icon : ""}
|
||||
|
||||
@@ -11,18 +11,25 @@ import {
|
||||
PermissionsAndroid,
|
||||
Platform,
|
||||
} from "react-native"
|
||||
import MapView, { Marker } from "react-native-maps"
|
||||
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 { workItemTypeText, pad, regionContainingPoints } from "../util"
|
||||
import {
|
||||
geoDistance,
|
||||
workItemTypeText,
|
||||
pad,
|
||||
regionContainingPoints,
|
||||
} from "../util"
|
||||
import { ensurePermissions } from "../App"
|
||||
import { versionInfo } from "../version"
|
||||
import { minGPSAccuracy } from "../config"
|
||||
import { config } from "../config"
|
||||
import KeyboardSpacer from "react-native-keyboard-spacer"
|
||||
import pinImage from "./images/pin.png"
|
||||
import hardhatPinImage from "./images/hardhat.png"
|
||||
import clipboardPinImage from "./images/clipboard.png"
|
||||
import questionPinImage from "./images/question.png"
|
||||
|
||||
const neverAskForLocationPermissionKeyName = "NeverAskForLocationPermission"
|
||||
const neverAskForCameraKeyName = "NeverAskForCameraPermission"
|
||||
@@ -40,7 +47,8 @@ export class Home extends React.Component {
|
||||
longitudeDelta: 0.0922,
|
||||
},
|
||||
positionInfo: null,
|
||||
disableARViewer: true,
|
||||
haveCameraPermission: false,
|
||||
workItemDistance: -1,
|
||||
}
|
||||
|
||||
ensurePermissions(
|
||||
@@ -71,7 +79,7 @@ export class Home extends React.Component {
|
||||
results[PermissionsAndroid.PERMISSIONS.CAMERA] ===
|
||||
PermissionsAndroid.RESULTS.GRANTED
|
||||
) {
|
||||
this.setState({ disableARViewer: false })
|
||||
this.setState({ haveCameraPerm: true })
|
||||
}
|
||||
},
|
||||
() => {
|
||||
@@ -80,9 +88,9 @@ export class Home extends React.Component {
|
||||
icon: "hand",
|
||||
message:
|
||||
"You have denied the app access to phone features it needs to function. " +
|
||||
"Some parts of the app are disabled. To enable these features in future " +
|
||||
"please go to Settings.",
|
||||
detail: "",
|
||||
"Some parts of the app are disabled.",
|
||||
detail:
|
||||
"To enable these features in future " + "please go to Settings.",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -130,11 +138,37 @@ export class Home extends React.Component {
|
||||
viewOffset: 45,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.state.positionInfo) {
|
||||
const coords = this.state.positionInfo.coords
|
||||
const workItem = sections[sectionIndex]
|
||||
const [lng, lat] = workItem.location.coordinates
|
||||
|
||||
this.setState({
|
||||
workItemDistance: geoDistance(
|
||||
coords.latitude,
|
||||
coords.longitude,
|
||||
lat,
|
||||
lng,
|
||||
"K"
|
||||
).toFixed(2),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleWorkItemsListPress() {
|
||||
this.props.history.push("/workitemlist")
|
||||
const { positionInfo } = this.state
|
||||
this.props.history.push(
|
||||
`/workItemList${
|
||||
positionInfo
|
||||
? "?latLng=" +
|
||||
coords.latitude.toString() +
|
||||
"," +
|
||||
coords.longitude.toString()
|
||||
: ""
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
@autobind
|
||||
@@ -149,7 +183,30 @@ export class Home extends React.Component {
|
||||
|
||||
@autobind
|
||||
handleGlassesPress() {
|
||||
this.props.history.push("/arviewer")
|
||||
const { lat: lat1, lng: lng1 } = this.state.positionInfo.coords
|
||||
const closestWorkItem = null
|
||||
const shortestDistance = config.minDistanceToItem
|
||||
|
||||
this.state.sections.forEach((workItem) => {
|
||||
const [lng2, lat2] = workItem.location.coordinates
|
||||
const distance = geoDistance(lat1, lng1, lat2, lng2, "K") * 1000
|
||||
|
||||
if (distance <= shortestDistance) {
|
||||
closestWorkItem = workItem
|
||||
shortestDistance = distance
|
||||
}
|
||||
})
|
||||
|
||||
this.props.history.push(
|
||||
`/arviewer${
|
||||
closestWorkItem
|
||||
? "?workItemId=" +
|
||||
closestWorkItem._id +
|
||||
"&workItemType=" +
|
||||
closestWorkItem.workItemType
|
||||
: ""
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
@autobind
|
||||
@@ -176,7 +233,8 @@ export class Home extends React.Component {
|
||||
region,
|
||||
positionInfo,
|
||||
messageModal,
|
||||
disableARViewer,
|
||||
haveCameraPermission,
|
||||
workItemDistance,
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
@@ -191,8 +249,10 @@ export class Home extends React.Component {
|
||||
leftButton={{ icon: "logout", onPress: this.handleLogoutPress }}
|
||||
rightButton={{ icon: "glasses", onPress: this.handleGlassesPress }}
|
||||
disabled={
|
||||
!(positionInfo && positionInfo.coords.accuracy <= minGPSAccuracy) ||
|
||||
disableARViewer
|
||||
!(
|
||||
positionInfo &&
|
||||
positionInfo.coords.accuracy <= config.minGPSAccuracy
|
||||
) || !haveCameraPermission
|
||||
}
|
||||
/>
|
||||
<MapView
|
||||
@@ -216,16 +276,31 @@ export class Home extends React.Component {
|
||||
<Marker
|
||||
key={index}
|
||||
coordinate={workItem.coordinate}
|
||||
title={
|
||||
pad(workItem.ticketNumber, 4) +
|
||||
": " +
|
||||
workItemTypeText[workItem.workItemType]
|
||||
}
|
||||
description={workItem.address}
|
||||
anchor={{ x: 0.5, y: 1.0 }}
|
||||
image={pinImage}
|
||||
onPress={(e) => this.handleMarkerPress(e, index)}
|
||||
/>
|
||||
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]}
|
||||
</Text>
|
||||
<Text>
|
||||
{workItem.address} ({this.workItemDistance > 0
|
||||
? this.workItemDistance.toString()
|
||||
: "?"}{" "}
|
||||
km)
|
||||
</Text>
|
||||
</View>
|
||||
</Callout>
|
||||
</Marker>
|
||||
))}
|
||||
</MapView>
|
||||
<View
|
||||
@@ -329,7 +404,7 @@ export class Home extends React.Component {
|
||||
{activity.resolution}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14, color: "gray" }}>
|
||||
{activity.address || "..."}
|
||||
{activity.when}
|
||||
</Text>
|
||||
</View>
|
||||
<Icon
|
||||
|
||||
BIN
mobile/src/Home/images/clipboard-pin.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
mobile/src/Home/images/clipboard-pin@2x.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
mobile/src/Home/images/clipboard-pin@3x.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
mobile/src/Home/images/hardhat-pin.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
mobile/src/Home/images/hardhat-pin@2x.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
mobile/src/Home/images/hardhat-pin@3x.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
BIN
mobile/src/Home/images/question-pin.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
mobile/src/Home/images/question-pin@2x.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
mobile/src/Home/images/question-pin@3x.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
50
mobile/src/Modal/ImageViewerModal.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { Component } from "react"
|
||||
import Modal from "react-native-modal"
|
||||
import PropTypes from "prop-types"
|
||||
import { View, Image, TouchableOpacity } from "react-native"
|
||||
import { Icon } from "../ui"
|
||||
import autobind from "autobind-decorator"
|
||||
|
||||
export class ImageViewerModal extends Component {
|
||||
static propTypes = {
|
||||
open: PropTypes.bool,
|
||||
imageURL: PropTypes.string.isRequired,
|
||||
onDismiss: PropTypes.func,
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleButtonPress() {
|
||||
const { onDismiss } = this.props
|
||||
|
||||
if (onDismiss) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { open, icon, message, detail } = this.props
|
||||
|
||||
return (
|
||||
<Modal isVisible={open}>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}>
|
||||
<Image source={imageURL} />
|
||||
<View style={{ position: "absolute", left: 30, right: 0, top: 50 }}>
|
||||
<TouchableHighlight
|
||||
style={{
|
||||
height: 80,
|
||||
width: 80,
|
||||
}}
|
||||
onPress={this.handleBackPress}
|
||||
underlayColor={"#00000000"}>
|
||||
<Image source={backImage} />
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
29
mobile/src/Modal/WaitModal.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React, { Component } from "react"
|
||||
import Modal from "react-native-modal"
|
||||
import PropTypes from "prop-types"
|
||||
import { View, ActivityIndicator } from "react-native"
|
||||
|
||||
export class WaitModal extends Component {
|
||||
static propTypes = {
|
||||
open: PropTypes.bool,
|
||||
message: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { open, icon, message, detail } = this.props
|
||||
|
||||
return (
|
||||
<Modal isVisible={open}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#FFFFFF",
|
||||
}}>
|
||||
<ActivityIndicator size="large" color="#0000FF" />
|
||||
<Text style={{ marginTop: 5, fontSize: 18 }}>{message}</Text>
|
||||
</View>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
BIN
mobile/src/Modal/images/back.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,2 +1,4 @@
|
||||
export { MessageModal } from "./MessageModal"
|
||||
export { ApiModal } from "./ApiModal"
|
||||
export { WaitModal } from "./WaitModal"
|
||||
export { ImageViewerModal } from "./ImageViewerModal"
|
||||
|
||||
@@ -59,6 +59,10 @@ export class WorkItem extends React.Component {
|
||||
isValid: (r, v) => v !== "",
|
||||
isReadOnly: true,
|
||||
},
|
||||
address: {
|
||||
isValid: true,
|
||||
isReadOnly: true,
|
||||
},
|
||||
details: {
|
||||
isValid: (r, v) => v !== "",
|
||||
},
|
||||
@@ -118,6 +122,12 @@ export class WorkItem extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.geoCodeTimer) {
|
||||
clearTimeout(this.geoCodeTimer)
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleBackPress() {
|
||||
const { history } = this.props
|
||||
@@ -180,11 +190,32 @@ export class WorkItem extends React.Component {
|
||||
|
||||
@autobind
|
||||
handleRegionChange(region) {
|
||||
const { latitude, longitude } = region
|
||||
|
||||
if (this.latLngInput) {
|
||||
this.latLngInput.handleChangeText(
|
||||
formatLatLng(region.latitude, region.longitude)
|
||||
)
|
||||
this.latLngInput.handleChangeText(formatLatLng(latitude, longitude))
|
||||
}
|
||||
|
||||
if (this.geoCodeTimer) {
|
||||
clearTimeout(this.geoCodeTimer)
|
||||
}
|
||||
|
||||
this.geoCodeTimer = setTimeout(
|
||||
() => this.handleStartAddressLookup({ latitude, longitude }),
|
||||
config.geocodeDelayMilliseconds
|
||||
)
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleStartAddressLookup(latLng) {
|
||||
api
|
||||
.addressLookup(latLng)
|
||||
.then((address) => {
|
||||
this.setState({ address })
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ address: "" })
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -252,6 +283,12 @@ export class WorkItem extends React.Component {
|
||||
name="location"
|
||||
label="Location:"
|
||||
/>
|
||||
<BoundInput
|
||||
ref={(ref) => (this.addressInput = ref)}
|
||||
binder={binder}
|
||||
name="address"
|
||||
label="Address:"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.panel}>
|
||||
<PhotoPanel />
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
formatLatLng,
|
||||
parseLatLng,
|
||||
pad,
|
||||
geoDistance,
|
||||
} from "../util"
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -36,6 +37,25 @@ export class WorkItemList extends React.Component {
|
||||
this.state = {
|
||||
messageModal: null,
|
||||
}
|
||||
|
||||
const { search } = this.props.location
|
||||
|
||||
if (search) {
|
||||
const params = new URLSearchParams(search)
|
||||
const latLng = params.get("latLng")
|
||||
|
||||
if (latLng) {
|
||||
const [lat, lng] = latLng.split(",")
|
||||
|
||||
if (lat && lng) {
|
||||
this.position = {
|
||||
latitude: parseFloat(lat),
|
||||
longitude: parseFloat(lng),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
api
|
||||
.listWorkItems()
|
||||
.then((list) => {
|
||||
@@ -54,7 +74,7 @@ export class WorkItemList extends React.Component {
|
||||
|
||||
@autobind
|
||||
handleItemSelect(item, ref) {
|
||||
this.props.history.push(`/workitem?id=${item._id}`)
|
||||
this.props.history.push(`/workItem?id=${item._id}`)
|
||||
}
|
||||
|
||||
@autobind
|
||||
@@ -95,7 +115,7 @@ export class WorkItemList extends React.Component {
|
||||
|
||||
@autobind
|
||||
handleDonePress() {
|
||||
this.props.history.push("/workitem")
|
||||
this.props.history.push("/workItem")
|
||||
}
|
||||
|
||||
@autobind
|
||||
@@ -159,7 +179,17 @@ export class WorkItemList extends React.Component {
|
||||
{workItemTypeText[item.workItemType]}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14, color: "gray" }}>
|
||||
{`${item.address || "..."} | ??? mi`}
|
||||
{`${item.address || "..."} | ${
|
||||
this.position
|
||||
? geoDistance(
|
||||
this.position.latitude,
|
||||
this.position.longitude,
|
||||
item.location.coordinates[1],
|
||||
item.location.coordinates[0],
|
||||
"K"
|
||||
).toFixed(2)
|
||||
: "?"
|
||||
} km`}
|
||||
</Text>
|
||||
</View>
|
||||
<Icon
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
export const localIPAddr = "192.168.1.175"
|
||||
export const defaultUser = "john@lyon-smith.org"
|
||||
// export const defaultUser = ""
|
||||
// export const minGPSAccuracy = 20
|
||||
export const minGPSAccuracy = 100
|
||||
import React from "react"
|
||||
import { Platform } from "react-native"
|
||||
|
||||
export const config = {
|
||||
localIPAddr: "192.168.1.175",
|
||||
viroAPIKey: "06F37B6A-74DA-4A83-965A-7DE2209A5C46",
|
||||
googleAPIKey:
|
||||
Platform.os === "ios"
|
||||
? "AIzaSyDN4E_vzO4cKjKHkMg_49hX1GBnU34kx4U"
|
||||
: "AIzaSyAC7r1GjMFL1atZdbEcFSdCaXDrPnISqTc",
|
||||
googleGeocodeURL: "https://maps.googleapis.com/maps/api/geocode/json",
|
||||
defaultUser: "john@lyon-smith.org",
|
||||
//defaultUser: "",
|
||||
//minGPSAccuracy: 20,
|
||||
minGPSAccuracy: 100,
|
||||
minDistanceToItem: 10,
|
||||
geocodeDelayMilliseconds: 500,
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 654 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 778 B |
@@ -16,11 +16,6 @@ export let activitySchema = new Schema(
|
||||
},
|
||||
notes: { type: String, required: true },
|
||||
when: { type: Date, required: true },
|
||||
location: {
|
||||
type: { type: String },
|
||||
coordinates: [Number],
|
||||
},
|
||||
address: String,
|
||||
fromStreetNumber: Number,
|
||||
toStreetNumber: Number,
|
||||
photos: [Schema.Types.ObjectId],
|
||||
|
||||